diff --git a/.changeset/add-actor-created-via.md b/.changeset/add-actor-created-via.md new file mode 100644 index 0000000..bf0d5f7 --- /dev/null +++ b/.changeset/add-actor-created-via.md @@ -0,0 +1,9 @@ +--- +"@hypercerts-org/lexicon": minor +--- + +Add `app.certified.actor.createdVia` record lexicon for lightweight account provenance. + +The record carries an optional `clientId` property holding the OAuth `client_id` URL (the client metadata document URL) that uniquely identifies the application which created the Certified account. This lets consumers distinguish records originating from, for example, test or staging environments from real ones, without requiring cryptographic platform attestations. + +Like other Certified records, it includes an optional `signatures` array (`app.certified.signature.defs#list`) so the provenance marker can itself be attested. diff --git a/ERD.puml b/ERD.puml index 01304e9..739dcd7 100644 --- a/ERD.puml +++ b/ERD.puml @@ -56,6 +56,14 @@ dataclass organization { !endif } +' app.certified.actor.createdVia +dataclass createdVia { + !if (SHOW_FIELDS == "true") + clientId + createdAt + !endif +} + ' Contributors are represented by DIDs or human-readable strings ' therefore do not require modelling via a lexicon. entity "contributor (DID/profile)" as contributorEntity #FFD4A3 { diff --git a/SCHEMAS.md b/SCHEMAS.md index 1c16ca9..7fc3662 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -431,6 +431,22 @@ A location represented as a string, e.g. coordinates or a small GeoJSON string. --- +### `app.certified.actor.createdVia` + +**Description:** Records which application created this Certified account, identified by the OAuth client_id URL of that application. A lightweight provenance marker that lets consumers distinguish, for example, records originating from a test or staging environment from real ones, without requiring cryptographic platform attestations. + +**Key:** `literal:self` + +#### Properties + +| Property | Type | Required | Description | +| ------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `clientId` | `string` | ❌ | The OAuth `client_id` URL uniquely identifying the application that created this account. This is the client metadata document URL used during the ATProto OAuth flow (e.g. 'https://app.certified.one/oauth/client-metadata.json'). | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created. | +| `signatures` | `ref` | ❌ | Optional cryptographic signatures attesting to this record's content. | + +--- + ### `app.certified.actor.organization` **Description:** Extended metadata for an organization actor. Complements the base actor profile with organization-specific fields like legal structure and reference links. diff --git a/lexicons/app/certified/actor/createdVia.json b/lexicons/app/certified/actor/createdVia.json new file mode 100644 index 0000000..4f7eb0f --- /dev/null +++ b/lexicons/app/certified/actor/createdVia.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "app.certified.actor.createdVia", + "defs": { + "main": { + "type": "record", + "description": "Records which application created this Certified account, identified by the OAuth client_id URL of that application. A lightweight provenance marker that lets consumers distinguish, for example, records originating from a test or staging environment from real ones, without requiring cryptographic platform attestations.", + "key": "literal:self", + "record": { + "type": "object", + "required": ["createdAt"], + "properties": { + "clientId": { + "type": "string", + "format": "uri", + "description": "The OAuth `client_id` URL uniquely identifying the application that created this account. This is the client metadata document URL used during the ATProto OAuth flow (e.g. 'https://app.certified.one/oauth/client-metadata.json')." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this record was originally created." + }, + "signatures": { + "type": "ref", + "ref": "app.certified.signature.defs#list", + "description": "Optional cryptographic signatures attesting to this record's content." + } + } + } + } + } +} diff --git a/tests/validate-actor-created-via.test.ts b/tests/validate-actor-created-via.test.ts new file mode 100644 index 0000000..80d8a51 --- /dev/null +++ b/tests/validate-actor-created-via.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from "vitest"; +import { validate, ids } from "../generated/lexicons.js"; +import * as CreatedVia from "../generated/types/app/certified/actor/createdVia.js"; + +describe("app.certified.actor.createdVia", () => { + it("should accept a minimal valid record (only required createdAt)", () => { + const result = CreatedVia.validateMain({ + $type: ids.AppCertifiedActorCreatedVia, + createdAt: "2024-01-01T00:00:00.000Z", + }); + expect(result.success).toBe(true); + }); + + it("should accept a record with optional clientId", () => { + const result = CreatedVia.validateMain({ + $type: ids.AppCertifiedActorCreatedVia, + clientId: "https://app.certified.one/oauth/client-metadata.json", + createdAt: "2024-01-01T00:00:00.000Z", + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.value.clientId).toBe( + "https://app.certified.one/oauth/client-metadata.json", + ); + } + }); + + it("should accept a record with inline signatures", () => { + const result = CreatedVia.validateMain({ + $type: ids.AppCertifiedActorCreatedVia, + clientId: "https://app.certified.one/oauth/client-metadata.json", + createdAt: "2024-01-01T00:00:00.000Z", + signatures: [ + { + $type: "app.certified.signature.defs#inline", + signature: new Uint8Array([1, 2, 3]), + key: "did:plc:abc123#atproto", + }, + ], + }); + expect(result.success).toBe(true); + }); + + it("should accept a record missing optional clientId", () => { + const result = validate( + { createdAt: "2024-01-01T00:00:00.000Z" }, + ids.AppCertifiedActorCreatedVia, + "main", + false, + ); + expect(result.success).toBe(true); + }); + + it("should reject a record missing required createdAt", () => { + const result = validate( + { clientId: "https://app.certified.one/oauth/client-metadata.json" }, + ids.AppCertifiedActorCreatedVia, + "main", + false, + ); + expect(result.success).toBe(false); + }); + + it("should reject a record with a non-uri clientId", () => { + const result = validate( + { + clientId: "not a uri", + createdAt: "2024-01-01T00:00:00.000Z", + }, + ids.AppCertifiedActorCreatedVia, + "main", + false, + ); + expect(result.success).toBe(false); + }); + + it("should reject a record with an invalid createdAt format", () => { + const result = validate( + { + clientId: "https://app.certified.one/oauth/client-metadata.json", + createdAt: "not-a-datetime", + }, + ids.AppCertifiedActorCreatedVia, + "main", + false, + ); + expect(result.success).toBe(false); + }); +});