diff --git a/CODEOWNERS b/CODEOWNERS index d5db512aa..352c7c0fc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,27 +1,2 @@ # Default all files to the eng reviewers team * @ceramicnetwork/eng-reviewers - -# Pick two reviewers for each top level directory: -# the owner and someone from the eng-reviewers team. -/.github/ @smrz2001 @ceramicnetwork/eng-reviewers -/anchor-service/ @nathanielc @ceramicnetwork/eng-reviewers -/anchor-remote/ @nathanielc @ceramicnetwork/eng-reviewers -/api-server/ @dav1do @ceramicnetwork/eng-reviewers -/api/ @dav1do @ceramicnetwork/eng-reviewers -/beetle/ @nathanielc @ceramicnetwork/eng-reviewers -/ci-scripts/ @smrz2001 @ceramicnetwork/eng-reviewers -/core/ @nathanielc @ceramicnetwork/eng-reviewers -/event/ @nathanielc @ceramicnetwork/eng-reviewers -/flight/ @nathanielc @ceramicnetwork/eng-reviewers -/fpm/ @smrz2001 @ceramicnetwork/eng-reviewers -/kubo-rpc-server/ @nathanielc @ceramicnetwork/eng-reviewers -/kubo-rpc/ @nathanielc @ceramicnetwork/eng-reviewers -/metrics/ @dav1do @ceramicnetwork/eng-reviewers -/migrations/ @dav1do @ceramicnetwork/eng-reviewers -/olap/ @nathanielc @ceramicnetwork/eng-reviewers -/one/ @stbrody @ceramicnetwork/eng-reviewers -/p2p/ @nathanielc @ceramicnetwork/eng-reviewers -/recon/ @nathanielc @ceramicnetwork/eng-reviewers -/service/ @dav1do @ceramicnetwork/eng-reviewers -/store/ @dav1do @ceramicnetwork/eng-reviewers -/validation/ @dav1do @ceramicnetwork/eng-reviewers diff --git a/sdk/packages/model-instance-client/src/client.ts b/sdk/packages/model-instance-client/src/client.ts index e556bcf55..d54900f69 100644 --- a/sdk/packages/model-instance-client/src/client.ts +++ b/sdk/packages/model-instance-client/src/client.ts @@ -28,8 +28,8 @@ import type { DocumentState, UnknownContent } from './types.js' export type CreateSingletonParams = { /** The model's stream ID */ model: StreamID - /** The controller of the stream (DID string or literal string) */ - controller: DIDString | string + /** The controller of the stream */ + controller: DID /** A unique value to ensure determinism of the event */ uniqueValue?: Uint8Array } diff --git a/sdk/packages/model-instance-client/src/events.ts b/sdk/packages/model-instance-client/src/events.ts index 74ecfe376..2c87578ba 100644 --- a/sdk/packages/model-instance-client/src/events.ts +++ b/sdk/packages/model-instance-client/src/events.ts @@ -12,7 +12,6 @@ import { type EncodedDeterministicInitEventPayload, type JSONPatchOperation, } from '@ceramic-sdk/model-instance-protocol' -import type { DIDString } from '@didtools/codecs' import type { DID } from 'dids' import type { CID } from 'multiformats/cid' @@ -57,7 +56,7 @@ export async function createInitEvent< const { content, controller, ...headerParams } = params const header = createInitHeader({ ...headerParams, - controller: controller.id, + controller, unique: false, // non-deterministic event }) return await createSignedInitEvent(controller, content, header) @@ -77,7 +76,7 @@ export async function createInitEvent< */ export function getDeterministicInitEventPayload( model: StreamID, - controller: DIDString | string, + controller: DID, uniqueValue?: Uint8Array, ): DeterministicInitEventPayload { return { @@ -100,7 +99,7 @@ export function getDeterministicInitEventPayload( */ export function getDeterministicInitEvent( model: StreamID, - controller: DIDString | string, + controller: DID, uniqueValue?: Uint8Array, ): EncodedDeterministicInitEventPayload { const { header } = getDeterministicInitEventPayload( diff --git a/sdk/packages/model-instance-client/src/utils.ts b/sdk/packages/model-instance-client/src/utils.ts index 66e8233f2..157f331e1 100644 --- a/sdk/packages/model-instance-client/src/utils.ts +++ b/sdk/packages/model-instance-client/src/utils.ts @@ -3,7 +3,8 @@ import { DocumentInitEventHeader, type JSONPatchOperation, } from '@ceramic-sdk/model-instance-protocol' -import { type DIDString, asDIDString } from '@didtools/codecs' +import { asDIDString } from '@didtools/codecs' +import type { DID } from 'dids' import jsonpatch from 'fast-json-patch' import type { CID } from 'multiformats/cid' @@ -38,8 +39,8 @@ export type CreateInitHeaderParams = { /** CID of specific model version to use when validating this instance. * When empty the the init commit of the model is used */ modelVersion?: CID - /** The DID string or literal string representing the controller of the document. */ - controller: DIDString | string + /** The controller of the document. */ + controller: DID /** A unique value to ensure determinism, or a boolean to indicate uniqueness type. */ unique?: Uint8Array | boolean /** Optional context for the document. */ @@ -68,8 +69,11 @@ export type CreateInitHeaderParams = { export function createInitHeader( params: CreateInitHeaderParams, ): DocumentInitEventHeader { + const did = params.controller.hasParent + ? params.controller.parent + : params.controller.id const header: DocumentInitEventHeader = { - controllers: [asDIDString(params.controller)], + controllers: [asDIDString(did)], model: params.model, sep: 'model', } diff --git a/sdk/packages/model-instance-client/test/lib.test.ts b/sdk/packages/model-instance-client/test/lib.test.ts index aa83432c5..ed86c1ba7 100644 --- a/sdk/packages/model-instance-client/test/lib.test.ts +++ b/sdk/packages/model-instance-client/test/lib.test.ts @@ -29,9 +29,11 @@ const authenticatedDID = await getAuthenticatedDID(new Uint8Array(32)) describe('getDeterministicInitEventPayload()', () => { test('returns the deterministic event payload without unique value by default', () => { const model = randomStreamID() - const event = getDeterministicInitEventPayload(model, 'did:key:123') + const event = getDeterministicInitEventPayload(model, authenticatedDID) expect(event.data).toBeNull() - expect(event.header.controllers).toEqual(['did:key:123']) + expect(event.header.controllers).toEqual([ + 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', + ]) expect(event.header.model).toBe(model) expect(event.header.unique).toBeUndefined() }) @@ -39,9 +41,15 @@ describe('getDeterministicInitEventPayload()', () => { test('returns the deterministic event payload with the provided unique value', () => { const model = randomStreamID() const unique = new Uint8Array([0, 1, 2]) - const event = getDeterministicInitEventPayload(model, 'did:key:123', unique) + const event = getDeterministicInitEventPayload( + model, + authenticatedDID, + unique, + ) expect(event.data).toBeNull() - expect(event.header.controllers).toEqual(['did:key:123']) + expect(event.header.controllers).toEqual([ + 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', + ]) expect(event.header.model).toBe(model) expect(event.header.unique).toBe(unique) }) @@ -50,9 +58,11 @@ describe('getDeterministicInitEventPayload()', () => { describe('getDeterministicInitEvent()', () => { test('returns the deterministic event without unique value by default', () => { const model = randomStreamID() - const event = getDeterministicInitEvent(model, 'did:key:123') + const event = getDeterministicInitEvent(model, authenticatedDID) expect(event.data).toBeNull() - expect(event.header.controllers).toEqual(['did:key:123']) + expect(event.header.controllers).toEqual([ + 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', + ]) expect(equals(event.header.model, model.bytes)).toBe(true) expect(event.header.unique).toBeUndefined() }) @@ -60,9 +70,11 @@ describe('getDeterministicInitEvent()', () => { test('returns the deterministic event with the provided unique value', () => { const model = randomStreamID() const unique = new Uint8Array([0, 1, 2]) - const event = getDeterministicInitEvent(model, 'did:key:123', unique) + const event = getDeterministicInitEvent(model, authenticatedDID, unique) expect(event.data).toBeNull() - expect(event.header.controllers).toEqual(['did:key:123']) + expect(event.header.controllers).toEqual([ + 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', + ]) expect(equals(event.header.model, model.bytes)).toBe(true) expect(event.header.unique).toBe(unique) }) @@ -145,7 +157,7 @@ describe('ModelInstanceClient', () => { describe('getEvent() method', () => { test('gets a MID event by commit ID', async () => { const streamID = randomStreamID() - const docEvent = getDeterministicInitEvent(streamID, 'did:key:123') + const docEvent = getDeterministicInitEvent(streamID, authenticatedDID) const getEventType = jest.fn(() => docEvent) const ceramic = { getEventType } as unknown as CeramicClient const client = new ModelInstanceClient({ ceramic, did: authenticatedDID }) @@ -167,7 +179,7 @@ describe('ModelInstanceClient', () => { const client = new ModelInstanceClient({ ceramic, did: authenticatedDID }) const id = await client.createSingleton({ - controller: 'did:key:123', + controller: authenticatedDID, model: randomStreamID(), }) expect(postEventType).toHaveBeenCalled() diff --git a/sdk/packages/model-instance-client/test/utils.test.ts b/sdk/packages/model-instance-client/test/utils.test.ts index 2dd01c07f..4c0bb4f83 100644 --- a/sdk/packages/model-instance-client/test/utils.test.ts +++ b/sdk/packages/model-instance-client/test/utils.test.ts @@ -1,22 +1,28 @@ import { randomStreamID } from '@ceramic-sdk/identifiers' +import { getAuthenticatedDID } from '@didtools/key-did' import { equals } from 'uint8arrays' import { createInitHeader } from '../src/utils.js' +const authenticatedDID = await getAuthenticatedDID(new Uint8Array(32)) + describe('createInitHeader()', () => { test('adds random unique bytes by default or when explcitly set to false', () => { - const controller = 'did:key:123' const model = randomStreamID() - const header1 = createInitHeader({ controller, model }) + const header1 = createInitHeader({ controller: authenticatedDID, model }) expect(header1.unique).toBeInstanceOf(Uint8Array) - const header2 = createInitHeader({ controller, model }) + const header2 = createInitHeader({ controller: authenticatedDID, model }) expect(header2.unique).toBeInstanceOf(Uint8Array) expect( equals(header1.unique as Uint8Array, header2.unique as Uint8Array), ).toBe(false) - const header3 = createInitHeader({ controller, model, unique: false }) + const header3 = createInitHeader({ + controller: authenticatedDID, + model, + unique: false, + }) expect(header3.unique).toBeInstanceOf(Uint8Array) expect( equals(header1.unique as Uint8Array, header3.unique as Uint8Array), @@ -24,13 +30,20 @@ describe('createInitHeader()', () => { }) test('adds the specified unique bytes', () => { - const controller = 'did:key:123' const model = randomStreamID() const unique = new Uint8Array([0, 1, 2]) - const header1 = createInitHeader({ controller, model, unique }) + const header1 = createInitHeader({ + controller: authenticatedDID, + model, + unique, + }) expect(header1.unique).toBeInstanceOf(Uint8Array) - const header2 = createInitHeader({ controller, model, unique }) + const header2 = createInitHeader({ + controller: authenticatedDID, + model, + unique, + }) expect(header2.unique).toBeInstanceOf(Uint8Array) expect( @@ -39,26 +52,27 @@ describe('createInitHeader()', () => { }) test('does not add unique bytes if set to true', () => { - const controller = 'did:key:123' const model = randomStreamID() - const header = createInitHeader({ controller, model, unique: true }) + const header = createInitHeader({ + controller: authenticatedDID, + model, + unique: true, + }) expect(header.unique).toBeUndefined() }) test('does not add context and shouldIndex by default', () => { - const controller = 'did:key:123' const model = randomStreamID() - const header = createInitHeader({ controller, model }) + const header = createInitHeader({ controller: authenticatedDID, model }) expect(header.context).toBeUndefined() expect(header.shouldIndex).toBeUndefined() }) test('adds context and shouldIndex if specified', () => { - const controller = 'did:key:123' const model = randomStreamID() const context = randomStreamID() const header = createInitHeader({ - controller, + controller: authenticatedDID, model, context, shouldIndex: true, diff --git a/sdk/packages/test-vectors/scripts/create-test-vectors.ts b/sdk/packages/test-vectors/scripts/create-test-vectors.ts index 36f1db27a..9b792fb6c 100644 --- a/sdk/packages/test-vectors/scripts/create-test-vectors.ts +++ b/sdk/packages/test-vectors/scripts/create-test-vectors.ts @@ -135,16 +135,13 @@ for (const [controllerType, createController] of Object.entries( const controller = await createController() // Deterministic (init) event - const validDeterministicEvent = getDeterministicInitEvent( - model, - controller.id, - ) + const validDeterministicEvent = getDeterministicInitEvent(model, controller) // Signed init event const validInitPayload = InitEventPayload.encode({ data: { test: true }, header: createInitHeader({ - controller: controller.id, + controller, model, unique: new Uint8Array([0, 1, 2, 3]), }), diff --git a/tests/suite/package.json b/tests/suite/package.json index ded534016..74a5c4f9a 100644 --- a/tests/suite/package.json +++ b/tests/suite/package.json @@ -41,6 +41,7 @@ "@ceramicnetwork/streamid": "^5.6.0", "@composedb/client": "^0.8.0", "@composedb/devtools": "^0.8.0", + "@didtools/cacao": "^3.0.1", "@didtools/codecs": "^3.0.0", "@didtools/key-did": "^1.0.0", "@didtools/pkh-ethereum": "^0.5.0", diff --git a/tests/suite/pnpm-lock.yaml b/tests/suite/pnpm-lock.yaml index 7233f3959..cde4b1a24 100644 --- a/tests/suite/pnpm-lock.yaml +++ b/tests/suite/pnpm-lock.yaml @@ -75,6 +75,9 @@ dependencies: '@composedb/devtools': specifier: ^0.8.0 version: 0.8.0(@polkadot/util@7.9.2)(graphql@16.9.0)(typescript@5.8.2) + '@didtools/cacao': + specifier: ^3.0.1 + version: 3.0.1(typescript@5.8.2) '@didtools/codecs': specifier: ^3.0.0 version: 3.0.0 diff --git a/tests/suite/src/__tests__/correctness/fast/model-mid-did-session.test.ts b/tests/suite/src/__tests__/correctness/fast/model-mid-did-session.test.ts new file mode 100644 index 000000000..70673bfb3 --- /dev/null +++ b/tests/suite/src/__tests__/correctness/fast/model-mid-did-session.test.ts @@ -0,0 +1,157 @@ +import { beforeAll, describe, expect, test } from '@jest/globals' +import { AuthMethod, AuthMethodOpts, Cacao, SiweMessage } from '@didtools/cacao' +import { AccountId } from 'caip' +import { DIDSession } from 'did-session' +import { randomString } from '@stablelib/random' +import { normalizeAccountId } from '@ceramicnetwork/common' +import { Wallet as Signer } from 'ethers' +import { type ClientOptions, createFlightSqlClient, FlightSqlClient } from '@ceramic-sdk/flight-sql-client' +import { CeramicClient } from '@ceramic-sdk/http-client' +import { ModelClient } from '@ceramic-sdk/model-client' +import type { ModelDefinition } from '@ceramic-sdk/model-protocol' +import { StreamID } from '@ceramic-sdk/identifiers' +import { ModelInstanceClient } from '@ceramic-sdk/model-instance-client' +import { DID } from 'dids' + +import { randomDID } from '../../../utils/didHelper' +import { urlsToEndpoint } from '../../../utils/common' +import { waitForEventState } from '../../../utils/rustCeramicHelpers' + +const CeramicUrls = String(process.env.CERAMIC_URLS).split(',') +const CeramicFlightUrls = String(process.env.CERAMIC_FLIGHT_URLS).split(',') +const CeramicFlightEndpoints = urlsToEndpoint(CeramicFlightUrls) + +const FLIGHT_OPTIONS: ClientOptions = { + headers: new Array(), + username: undefined, + password: undefined, + token: undefined, + tls: false, + host: CeramicFlightEndpoints[0].host, + port: CeramicFlightEndpoints[0].port, +} + +const getCacao = ( + accountId: AccountId, + signer: Signer +): AuthMethod => async (opts: AuthMethodOpts) => { + const now = new Date() + const oneWeekLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000) + const normalizedAccount = normalizeAccountId(accountId) + + const siweMessage = new SiweMessage({ + domain: new URL(CeramicUrls[0]).hostname, + address: normalizedAccount.address, + statement: opts.statement ?? 'Give this application access to some of your data on Ceramic', + uri: opts.uri, + version: '1', + nonce: opts.nonce ?? randomString(10), + issuedAt: now.toISOString(), + expirationTime: opts.expirationTime ?? oneWeekLater.toISOString(), + chainId: normalizedAccount.chainId.reference, + resources: opts.resources, + }) + siweMessage.signature = await signer.signMessage(siweMessage.signMessage()) + + return Cacao.fromSiweMessage(siweMessage) +} + +export const authorizedSessionDid = async ( + signer: Signer, + resources: string[], +) => { + const address = await signer.getAddress() + const chainId = 'eip155:11155111' + const caipAccountId = new AccountId({ address, chainId }) + const authMethod: AuthMethod = getCacao(caipAccountId, signer) + const session = await DIDSession.authorize( + authMethod, + { resources } + ) + return session.did +} + +describe('create/update stream using session did', () => { + let flightClient: FlightSqlClient + let client: CeramicClient + let modelInstanceClient: ModelInstanceClient + let modelStream: StreamID + + const testSigner = new Signer('0x4c0883a69102937d6231471b5dbb6204eaa7f9b0c8f2d6f8e1c5f3a2b6c7d8e9') + let authenticatedDID1: DID + let authenticatedDID2: DID + + beforeAll(async () => { + flightClient = await createFlightSqlClient(FLIGHT_OPTIONS) + + client = new CeramicClient({ + url: CeramicUrls[0] + }) + + modelInstanceClient = new ModelInstanceClient({ + ceramic: client, + did: await randomDID() + }) + + const modelClient = new ModelClient({ + ceramic: client, + did: await randomDID() + }) + + const testModel: ModelDefinition = { + version: '2.0', + name: 'TestModel', + description: 'List Test model', + accountRelation: { type: 'list' }, + interface: false, + implements: [], + schema: { + type: 'object', + properties: { + test: { type: 'string', maxLength: 10 }, + }, + additionalProperties: false, + }, + } + + modelStream = await modelClient.createDefinition(testModel) + // Use the flightsql stream behavior to ensure the events states have been process before querying their states. + await waitForEventState(flightClient, modelStream.cid) + + authenticatedDID1 = await authorizedSessionDid( + testSigner, + [`ceramic://*?model=${modelStream.toString()}`], + ) + authenticatedDID2 = await authorizedSessionDid( + testSigner, + [`ceramic://*?model=${modelStream.toString()}`], + ) + }, 10000) + + test('create/update stream', async () => { + const init = await modelInstanceClient.createInstance({ + controller: authenticatedDID1, + model: modelStream, + content: { test: 'hello' }, + shouldIndex: true, + }) + + await waitForEventState(flightClient, init.cid) + + const streamInit = await modelInstanceClient.getDocumentState(init.baseID) + let controller = streamInit.metadata.controller + const signerAddress = (await testSigner.getAddress()).toLowerCase() + + expect(controller).toEqual(authenticatedDID1.parent) + expect(controller!.replace(/did:pkh.*:/, '').toLowerCase()).toEqual(signerAddress) + + const streamUpdate = await modelInstanceClient.updateDocument({ + controller: authenticatedDID2, + streamID: init.baseID.toString(), + newContent: { test: 'world' }, + shouldIndex: true, + }) + + await waitForEventState(flightClient, streamUpdate.commitID.cid) + }) +}) diff --git a/tests/suite/src/__tests__/correctness/fast/model-mid-singleType.test.ts b/tests/suite/src/__tests__/correctness/fast/model-mid-singleType.test.ts index cdf8eaaee..ff221fb2f 100644 --- a/tests/suite/src/__tests__/correctness/fast/model-mid-singleType.test.ts +++ b/tests/suite/src/__tests__/correctness/fast/model-mid-singleType.test.ts @@ -78,7 +78,7 @@ describe('model integration test for single model and MID', () => { }) const documentStream = await modelInstanceClient.createSingleton({ model: modelStream, - controller: authenticatedDID.id, + controller: authenticatedDID, }) // Use the flightsql stream behavior to ensure the events states have been process before querying their states. @@ -97,7 +97,7 @@ describe('model integration test for single model and MID', () => { }) const documentStream = await modelInstanceClient.createSingleton({ model: modelStream, - controller: authenticatedDID.id, + controller: authenticatedDID, }) // Use the flightsql stream behavior to ensure the events states have been process before querying their states. await waitForEventState(flightClient, documentStream.commit) @@ -117,11 +117,11 @@ describe('model integration test for single model and MID', () => { }) const documentStream1 = await modelInstanceClient.createSingleton({ model: modelStream, - controller: authenticatedDID.id, + controller: authenticatedDID, }) const documentStream2 = await modelInstanceClient.createSingleton({ model: modelStream, - controller: authenticatedDID.id, + controller: authenticatedDID, }) expect(documentStream1.baseID).toEqual(documentStream2.baseID) }) @@ -138,11 +138,11 @@ describe('model integration test for single model and MID', () => { }) const documentStream1 = await modelInstanceClient1.createSingleton({ model: modelStream, - controller: authenticatedDID1.id, + controller: authenticatedDID1, }) const documentStream2 = await modelInstanceClient2.createSingleton({ model: modelStream, - controller: authenticatedDID2.id, + controller: authenticatedDID2, }) expect(documentStream1.baseID).not.toEqual(documentStream2.baseID) })