import type { Reference } from "merkle-reference"; import type { JSONSchema, JSONValue } from "@commontools/api"; export type SchemaContext = { schema: JSONSchema; rootSchema: JSONSchema; }; export type SchemaPathSelector = { path: readonly string[]; schemaContext?: Readonly; }; export type { JSONValue, Reference }; export interface Clock { now(): UTCUnixTimestampInSeconds; } export type SubscriberCommand = { watch?: Query; unwatch?: Query; }; /** * Some principal identified via DID identifier. */ export interface Principal { did(): ID; } /** * Principal capable of issuing an {@link Authorization}. */ export interface Authority extends Principal { authorize( access: Iterable | T>, ): AwaitResult, AuthorizationError>; } export interface Verifier extends Principal { verify(authorization: { payload: Uint8Array; signature: Uint8Array; }): AwaitResult; } export interface Signer extends Principal { sign(payload: AsBytes): AwaitResult, Error>; verifier: Verifier; } export interface AsBytes extends Uint8Array { valueOf(): this & AsBytes; } export type AsString = string & { valueOf(): AsString; }; export interface Signature extends Uint8Array { valueOf(): this & Signature; } export type UCAN = { invocation: Command; authorization: Authorization; }; /** * Proof of authorization for a given access. */ export interface Proof { [link: AsString>]: Unit; } /** * Represents a verifiable authorization issued by specific {@link Authority}. * It is slightly more abstract notion than signed payload. */ export type Authorization = { signature: Signature>; access: Proof; }; export interface AuthorizationError extends Error { name: "AuthorizationError"; } export type Call< Ability extends string = The, Of extends DID = DID, Args extends NonNullable = NonNullable, > = { cmd: Ability; sub: Of; args: Command; nonce?: Uint8Array; }; export type Command< Ability extends string = The, Of extends DID = DID, In extends NonNullable = NonNullable, > = { cmd: Ability; sub: Of; args: In; meta?: Meta; nonce?: Uint8Array; }; export type Invocation< Ability extends string = The, Of extends DID = DID, In extends NonNullable = NonNullable, > = { iss: DID; aud?: DID; cmd: Ability; sub: Of; args: In; meta?: Meta; nonce?: Uint8Array; exp?: UTCUnixTimestampInSeconds; iat?: UTCUnixTimestampInSeconds; prf: Delegation[]; cause?: void; }; /** * In the future this will be a delegation chain, but for now we do not support * delegation so it is empty chain implying issuer must be a subject., */ export type Delegation = never; export type UTCUnixTimestampInSeconds = number; export type Seconds = number; export type Protocol = { [Subject in Space]: { memory: { transact(source: { changes: Changes; }): Task< Result< Commit, | AuthorizationError | ConflictError | TransactionError | ConnectionError > >; query: { (query: { select: Selector; since?: number }): Task< Result, AuthorizationError | QueryError>, Selection >; subscribe( source: Subscribe["args"], ): Task< Result, EnhancedCommit >; unsubscribe( source: Unsubscribe["args"], ): Task>; }; graph: { query( schemaQuery: { selectSchema: SchemaSelector; since?: number; subscribe?: boolean; excludeSent?: boolean; }, ): Task< Result, AuthorizationError | QueryError>, Selection >; }; }; }; }; export type Proto = { [Subject: DID]: { [Namespace: string]: NonNullable; }; }; export type InferProtocol = UnionToIntersection< InferProtoMethods >; export type Abilities = keyof InferProtocol; export type InferProtoMethods< Protocol extends Proto, Methods = Protocol[keyof Protocol], Prefix extends string = "", > = { [Name in keyof Methods & string]: Methods[Name] extends ( input: infer In extends NonNullable, ) => Task, infer Effect> ? | { [The in `${Prefix}/${Name}`]: Method< Protocol, `${Prefix}/${Name}`, In, Awaited, Effect >; } | InferProtoMethods : Methods[Name] extends object ? InferProtoMethods : never; }[keyof Methods & string]; export type Method< Protocol, Ability extends The, In extends NonNullable, Out extends NonNullable, Effect, > = { The: Ability; Protocol: Protocol; Of: InferOf; In: In; Out: Out; Effect: Effect; Method: (input: In) => Task; ConsumerCommand: { cmd: Ability; // sub: InferOf & MemorySpace; sub: MemorySpace; args: In; meta?: Meta; nonce?: Uint8Array; }; ConsumerInvocation: Invocation, In>; ProviderCommand: Receipt< Invocation, In>, Out, Effect >; Invocation: InvocationView< Invocation, In>, Out, Effect >; Pending: { return(result: Out): boolean; perform(effect: Effect): void; }; }; export type InferOf = keyof T extends DID ? keyof T : never; /** * Utility type that takes union type `U` and produces intersection type of it's members. */ // Can `U extends any` ever be falsy? // FIXME: typing // deno-lint-ignore no-explicit-any type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; export type Provider> = { perform(command: ProviderCommand): AwaitResult; }; export interface ConsumerSession extends TransformStream< ProviderCommand, UCAN> > { } export interface ProviderChannel extends TransformStream< UCAN>, ProviderCommand > { } export interface ProviderSession extends ProviderChannel { close(): CloseResult; } export type ProviderCommand = ProtocolMethod extends { ProviderCommand: Receipt; } ? Receipt : never; export type ProtocolMethod = InferProtocol< Protocol >[Abilities]; export type ConsumerCommand = ProtocolMethod extends { ConsumerCommand: Command; } ? Command : never; export type ConsumerCommandInvocation< Protocol extends Proto, Method = ProtocolMethod, > = Method extends { ConsumerInvocation: Invocation; } ? Method["ConsumerInvocation"] : never; export type ConsumerCommandFor = & MethodFor< Ability, Protocol >["ConsumerCommand"] & { cmd: Ability }; export type ProviderCommandFor = MethodFor< Ability, Protocol >["ProviderCommand"]; export type ConsumerInvocationFor = & MethodFor< Ability, Protocol >["ConsumerInvocation"] & { cmd: Ability }; export type ConsumerInputFor = MethodFor< Ability, Protocol >["In"]; export type ConsumerEffectFor = MethodFor< Ability, Protocol >["Effect"]; export type ConsumerSpaceFor = MethodFor< Ability, Protocol >["Of"]; export type MethodFor< Ability, Protocol extends Proto, Case = ProtocolMethod, > = Case extends Method ? Method : never; export type ConsumerResultFor = MethodFor< Ability, Protocol >["Out"]; export interface InvocationView< Source extends Invocation, Return extends NonNullable, Effect, > extends Invocation { // Return false to remove listener return(result: Await): boolean; perform(effect: Effect): void; toJSON(): Source; } export type Task = Iterable; export type Job< Command extends NonNullable = NonNullable, Return extends NonNullable | null = NonNullable | null, Effect = unknown, > = { invoke: Command; return: Return; effect: Effect; }; export type WatchTask = Job< { watch: Query; unwatch?: undefined }, QueryResult, Transaction >; export type UnwatchTask = Job< { unwatch: Query; watch?: undefined }, Unit, never >; export type SessionTask = | UnwatchTask | WatchTask; export type Receipt< Command extends NonNullable, Result extends NonNullable | null, Effect, > = | { the: "task/return"; of: InvocationURL>; is: Awaited; } | (Effect extends never ? never : { the: "task/effect"; of: InvocationURL>; is: Effect; }); export type Effect, Command> = { of: Reference; run: Command; is?: undefined; }; export type Return< Of extends NonNullable, Result extends NonNullable | null, > = { of: Reference; is: Result; run?: undefined; }; export type SubscriptionCommand = { transact?: Transaction; brief?: Brief; }; export type Brief = { sub: Space; args: { selector: Selector; selection: Selection; }; meta?: Meta; }; export interface Session { /** * Transacts can be used to assert or retract a document from the repository. * If `version` asserted / retracted does not match version of the document * transaction fails with `ConflictError`. Otherwise document is updated to * the new value. */ transact(transact: Transaction): TransactionResult; /** * Queries space for matching entities based on provided selector. */ query(source: Query): QueryResult; /** * Queries space for matching entities based on provided selector. */ querySchema(source: SchemaQuery): QueryResult; close(): CloseResult; } export interface SpaceSession extends Session { subject: Space; transact( transact: Transaction, ): Result, ConflictError | TransactionError>; query(source: Query): Result, QueryError>; close(): Result; } export interface MemorySession extends Session { subscribe(subscriber: Subscriber): SubscribeResult; unsubscribe(subscriber: Subscriber): SubscribeResult; serviceDid(): DID; } export interface Subscriber { // Notifies a subscriber of a commit that has been applied commit(commit: Commit): AwaitResult; close(): AwaitResult; } export type SubscribeResult = AwaitResult; /** * Represents a subscription controller that can be used to publish commands or * to close subscription. */ export interface SubscriptionController { open: boolean; close(): void; transact(source: Transaction): void; brief(source: Brief): void; } /** * Unique identifier for the memory space. */ export type MemorySpace = `did:${string}:${string}`; /** * Unique identifier for the mutable entity. */ export type Entity = URI; /** * Type of the fact, usually formatted as media type. By default we expect * this to be "application/json", but in the future we may support other * data types. */ export type The = MIME; export type InvocationURL = `job:${string}` & { toString(): InvocationURL; }; export interface FactAddress { the: MIME; of: URI; } /** * Describes not yet claimed memory. It describes a lack of fact about memory. */ export interface Unclaimed { /** * Type of the fact, usually formatted as media type. By default we expect * this to be "application/json", but in the future we may support other * data types. */ the: T; /** * Stable memory identifier that uniquely identifies it. */ of: Of; is?: undefined; cause?: undefined; } /** * `Assertion` is just like a {@link Statement} except the value MUST be inline * {@link JSONValue} as opposed to reference to one. {@link Assertion}s are used * to assert facts, wile {@link Statement}s are used to retract them. This allows * retracting over the wire without having to sending JSON values back and forth. */ export interface Assertion< T extends string = MIME, Of extends string = URI, Is extends JSONValue = JSONValue, > { the: T; of: Of; is: Is; cause: | Reference> | Reference> | Reference>; } /** * Represents retracted {@link Assertion}. It is effectively a tombstone * denoting assertion that no longer hold and is a fact in itself. */ export interface Retraction< T extends string = MIME, Of extends string = URI, Is extends JSONValue = JSONValue, > { the: T; of: Of; is?: undefined; cause: Reference>; } export interface Invariant< T extends string = MIME, Of extends string = URI, Is extends JSONValue = JSONValue, > { the: T; of: Of; fact: Reference>; is?: undefined; cause?: undefined; } /** * Facts represent a memory in the replica. They are either current and * represented as {@link Assertion} or since retracted and therefor represented * by {@link Retraction}. */ export type Fact< T extends string = MIME, Of extends string = URI, Is extends JSONValue = JSONValue, > = Assertion | Retraction; export type Statement< T extends string = MIME, Of extends string = URI, Is extends JSONValue = JSONValue, > = Assertion | Retraction | Invariant; export type State = Fact | Unclaimed; export type Revision = T & { since: number }; export type Assert = { assert: Assertion; retract?: undefined; claim?: undefined; }; export type Retract = { retract: Retraction; assert?: undefined; claim?: undefined; }; export type Claim = { claim: Invariant; assert?: undefined; retract?: undefined; }; // This is essentially an OfTheCause tree with one special record whose value is another OfTheCause tree. export type Commit = { [of in Subject]: { ["application/commit+json"]: { [cause in CauseString]: { is: CommitData; }; }; }; }; export type EnhancedCommit = { revisions: Revision[]; commit: Commit; }; // We include labels here so we can use the commit data to redact the transaction // results before sending them to subscribers with insufficient access. export type CommitData = { since: number; transaction: Transaction; labels?: FactSelection; }; export type CommitFact = Assertion< "application/commit+json", Subject, CommitData >; // This allows a consumer to check that their entities match the current cause // state before making local changes that would be discarded on conflict. export type ClaimFact = true; // ⚠️ Note we use `void` as opposed to `undefined` because the latter makes it // incompatible with JSONValue. export type RetractFact = { is?: void }; export type AssertFact = { is: Is }; // This is the structure of a bunch of our objects export type OfTheCause = { [of in URI]: { [the in MIME]: { [cause in CauseString]: T; }; }; }; export type Changes< T extends string = MIME, Of extends string = URI, Is extends JSONValue = JSONValue, > = { [of in Of]: { [the in T]: { [cause in CauseString]: RetractFact | AssertFact | ClaimFact; }; }; }; export type FactSelection< T extends string = MIME, Of extends string = URI, Is extends JSONValue = JSONValue, > = { [of in Of]: { [the in T]: { [cause in CauseString]: { is?: Is; since: number; }; }; }; }; export type Meta = Record; export type DID = `did:${string}:${string}`; export type DIDKey = `did:key:${string}`; export type ANYONE = "*"; export type ACLUser = DID | ANYONE; /** * Capability levels for space access control. * - READ: Can query and read data from the space * - WRITE: Can read and transact (write) data to the space * - OWNER: Full control including ACL management */ export type Capability = "READ" | "WRITE" | "OWNER"; /** * Access Control List entry mapping DIDs to their capabilities */ export type ACL = { [user in ACLUser]?: Capability; }; // Entity identifier (typically `of:`, but sometimes `did:`). export type URI = `${string}:${string}`; // Mime type or Media Type -- often called 'the' export type MIME = `${string}/${string}`; // This is the base32 digest preceded by "b" as per multibase spec. export type CauseString = `b${string}`; export type Transaction = Invocation< "/memory/transact", Space, { changes: Changes } >; export type TransactionResult = AwaitResult< Commit, ConflictError | TransactionError | ConnectionError | AuthorizationError >; export type QueryArgs = { select: Selector; since?: number }; export type Query = Invocation< "/memory/query", Space, QueryArgs >; export type Subscribe = Invocation< "/memory/query/subscribe", Space, | { select: Selector; since?: number } | { selectSchema: SchemaSelector; since?: number } >; export type Unsubscribe = Invocation< "/memory/query/unsubscribe", Space, { source: InvocationURL>> } >; export type SchemaQueryArgs = { selectSchema: SchemaSelector; since?: number; subscribe?: boolean; // set to true to be notified of changes to any reachable entities excludeSent?: boolean; // set to true to remove entities already sent in this session classification?: string[]; // classifications to claim for access }; export type SchemaQuery = Invocation< "/memory/graph/query", Space, SchemaQueryArgs >; // A normal Selector looks like this (with _ as wildcard cause): // { // "of:ba4jcbvpq3k5sooggkwwosy6sqd3fhr5md7hroyf3bq3vrambqm4xkkus": { // "application/json": { // _: { // is: {} // } // } // } // } // A SchemaSelector looks like this (with _ as wildcard cause): // { // "of:ba4jcbvpq3k5sooggkwwosy6sqd3fhr5md7hroyf3bq3vrambqm4xkkus": { // "application/json": { // _: { // path: [], // schemaContext: { // schema: { "type": "object" }, // rootSchema: { "type": "object" } // } // } // } // } // } export type SchemaSelector = Select< URI, Select> >; export type Operation = | Transaction | Query | SchemaQuery | Subscribe | Unsubscribe; export type QueryResult = AwaitResult< Selection, AuthorizationError | QueryError | ConnectionError >; export type CloseResult = AwaitResult; export type WatchResult = AwaitResult; export type SubscriptionQuery = { iss: DID; sub: MemorySpace; cmd: "/memory/query"; args: { select: Selector; since?: number; }; }; export type SelectAll = "_"; export type Select = & { [key in Key]: Match; } & { _?: Match; }; /** * Selector that replica can be queried by. */ export type Selector = Select< URI, Select> >; export type Selection = { [space in Space]: FactSelection; }; export type Unit = NonNullable; /** * Generic type used to annotate underlying type with a context of the replica. */ export type In = { [For: MemorySpace]: T }; export type AsyncResult = Promise>; export type Await = PromiseLike | T; export type AwaitResult = Await< Result >; export type Result = | Ok | Fail; export interface Ok { ok: T; /** * Discriminant to differentiate between Ok and Fail. */ error?: undefined; } export interface Fail { error: E; /** * Discriminant to differentiate between Ok and Fail. */ ok?: undefined; } export type Conflict = { /** * Identifier of the replica where conflict occurred. */ space: MemorySpace; /** * Type of the fact where a conflict occurred. */ the: The; /** * Identifier of the entity where conflict occurred. */ of: Entity; /** * Expected state in the replica. */ expected: Reference | null; /** * Actual memory state in the replica repository. */ actual: Revision | null; /** * Whether the fact exists in the history of the entity. */ existsInHistory: boolean; /** * Actual history */ history: Revision[]; }; export type ToJSON = T & { toJSON(): T; }; export interface ConflictError extends Error { name: "ConflictError"; transaction: Transaction; conflict: Conflict; } export interface SystemError extends Error { code: number; } export interface ConnectionError extends Error { name: "ConnectionError"; cause: SystemError; address: string; } /** * Error from the underlying storage. */ export interface TransactionError extends Error { name: "TransactionError"; cause: SystemError; /** * Fact being stored when the error occurred. */ transaction: Transaction; } export interface QueryError extends Error { name: "QueryError"; cause: SystemError; space: MemorySpace; selector: Selector | SchemaSelector; } /** * Utility type for defining a [keyed union] type as in IPLD Schema. In practice * this just works around typescript limitation that requires discriminant field * on all variants. * * ```ts * type Result = * | { ok: T } * | { error: X } * * const demo = (result: Result) => { * if (result.ok) { * // ^^^^^^^^^ Property 'ok' does not exist on type '{ error: Error; }` * } * } * ``` * * Using `Variant` type we can define same union type that works as expected: * * ```ts * type Result = Variant<{ * ok: T * error: X * }> * * const demo = (result: Result) => { * if (result.ok) { * result.ok.toUpperCase() * } * } * ``` * * [keyed union]:https://ipld.io/docs/schemas/features/representation-strategies/#union-keyed-representation */ export type Variant> = { [Key in keyof U]: & { [K in Exclude]?: never } & { [K in Key]: U[Key]; }; }[keyof U];