From 52dfaaee324da2aa254abb2ed09cc09aba00f163 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 25 Jun 2026 21:49:44 +0000 Subject: [PATCH 1/2] feat(actor): add app.certified.actor.createdVia lexicon Add a lightweight account-provenance record carrying the OAuth client_id URL of the application that created a Certified account. Lets consumers distinguish records originating from test/staging environments from real ones without requiring cryptographic platform attestations. - Required `clientId` (uri) and `createdAt` (datetime); key literal:self - Optional `signatures` array, consistent with other Certified records - Validation tests, ERD entry, regenerated SCHEMAS.md, changeset (minor) The property is named `clientId` (camelCase) to satisfy the repo style checker; its description and the changeset note it holds the OAuth `client_id` URL. Refs HYPER-502. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/add-actor-created-via.md | 9 +++ ERD.puml | 8 ++ SCHEMAS.md | 16 ++++ lexicons/app/certified/actor/createdVia.json | 32 ++++++++ tests/validate-actor-created-via.test.ts | 84 ++++++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 .changeset/add-actor-created-via.md create mode 100644 lexicons/app/certified/actor/createdVia.json create mode 100644 tests/validate-actor-created-via.test.ts diff --git a/.changeset/add-actor-created-via.md b/.changeset/add-actor-created-via.md new file mode 100644 index 0000000..b6debd2 --- /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 a required `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..4020f50 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..d527f10 --- /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": ["clientId", "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..f8df49f --- /dev/null +++ b/tests/validate-actor-created-via.test.ts @@ -0,0 +1,84 @@ +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 (required clientId and createdAt)", () => { + 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 reject a record missing required clientId", () => { + const result = validate( + { createdAt: "2024-01-01T00:00:00.000Z" }, + ids.AppCertifiedActorCreatedVia, + "main", + false, + ); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBeDefined(); + } + }); + + 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); + }); +}); From bb4a7214037c80db68054c7b54e7979a22040160 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 29 Jun 2026 11:52:16 +0000 Subject: [PATCH 2/2] feat(actor): make createdVia clientId optional Per PR #228 review consensus (holkexyz, Kzoeps, s-adamantine, aspiers), clientId becomes optional. A signature already establishes origin via the signing service DID, so a signature-only record is valid provenance. ATProto lexicons cannot express "at least one of clientId/signatures", so no cross-field constraint is added at the schema layer. createdAt remains required, consistent with all other Certified record lexicons (the lone exception being signature/proof). Update tests, changeset, and SCHEMAS.md to reflect optional clientId. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/add-actor-created-via.md | 2 +- SCHEMAS.md | 2 +- lexicons/app/certified/actor/createdVia.json | 2 +- tests/validate-actor-created-via.test.ts | 17 +++++++++++------ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.changeset/add-actor-created-via.md b/.changeset/add-actor-created-via.md index b6debd2..bf0d5f7 100644 --- a/.changeset/add-actor-created-via.md +++ b/.changeset/add-actor-created-via.md @@ -4,6 +4,6 @@ Add `app.certified.actor.createdVia` record lexicon for lightweight account provenance. -The record carries a required `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. +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/SCHEMAS.md b/SCHEMAS.md index 4020f50..7fc3662 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -441,7 +441,7 @@ A location represented as a string, e.g. coordinates or a small GeoJSON string. | 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'). | +| `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. | diff --git a/lexicons/app/certified/actor/createdVia.json b/lexicons/app/certified/actor/createdVia.json index d527f10..4f7eb0f 100644 --- a/lexicons/app/certified/actor/createdVia.json +++ b/lexicons/app/certified/actor/createdVia.json @@ -8,7 +8,7 @@ "key": "literal:self", "record": { "type": "object", - "required": ["clientId", "createdAt"], + "required": ["createdAt"], "properties": { "clientId": { "type": "string", diff --git a/tests/validate-actor-created-via.test.ts b/tests/validate-actor-created-via.test.ts index f8df49f..80d8a51 100644 --- a/tests/validate-actor-created-via.test.ts +++ b/tests/validate-actor-created-via.test.ts @@ -3,7 +3,15 @@ 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 (required clientId and createdAt)", () => { + 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", @@ -33,17 +41,14 @@ describe("app.certified.actor.createdVia", () => { expect(result.success).toBe(true); }); - it("should reject a record missing required clientId", () => { + 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(false); - if (!result.success) { - expect(result.error).toBeDefined(); - } + expect(result.success).toBe(true); }); it("should reject a record missing required createdAt", () => {