domainkit 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"Storage.mjs","names":["Plan.PlanId","Plan.Kind","Plan.Model","Approval.Model","Receipt.Model","Receipt.ReceiptId","Plan.OperationId","DnsRecord.Model","Errors.isDomainKitError","Errors.DomainKitError","Reason.StorageFailed","Principal.Service","Errors.fail","Reason.Busy"],"sources":["../src/Storage.ts"],"sourcesContent":["/**\n * Everything DomainKit persists, behind one service the host installs once. Every method requires\n * `Principal` and is scoped by it. `@domainkit/capsuledb` ships the Postgres implementation;\n * `domainkit/testing` ships the memory one; both pass `Testing.conformance.storage`.\n *\n * Storage never sees plaintext credentials: `Connect` seals secrets through `Custody` before\n * writing a `Credential` row and opens them after reading one.\n *\n * Grouped by noun so the 25-odd methods stay navigable. Implementations must satisfy the\n * invariants documented per group; the conformance suite checks them.\n */\nimport { Context, DateTime, Effect, Layer, Option, Schema } from \"effect\";\n\nimport * as Approval from \"./Approval.ts\";\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport { makeMemory } from \"./internal/storage-memory.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Receipt from \"./Receipt.ts\";\n\nexport type Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\n// ---------------------------------------------------------------------------------------------\n// Rows\n// ---------------------------------------------------------------------------------------------\n\nexport const Capability = Schema.Literals([\"dns:read\", \"dns:write\"]);\nexport type Capability = typeof Capability.Type;\n\nexport const AuthMethod = Schema.Literals([\"token\", \"oauth\", \"integration\"]);\nexport type AuthMethod = typeof AuthMethod.Type;\n\n/** Sealed provider credential plus the metadata needed without unsealing it. */\nexport class Credential extends Schema.Class<Credential>(\"@domainkit/Storage/Credential\")({\n ciphertext: Schema.String,\n expiresAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n rotatedAt: Schema.DateTimeUtcFromString,\n}) {}\n\n/** One provider authorization (an OAuth grant or a token) owned by a principal. */\nexport class Authorization extends Schema.Class<Authorization>(\"@domainkit/Storage/Authorization\")({\n id: Schema.String,\n ownerId: Schema.String,\n provider: Schema.String,\n method: AuthMethod,\n capabilities: Schema.Array(Capability),\n /** Provider-specific account context (account id, team id, ...) decoded by the provider's `context` schema. */\n context: Schema.Unknown,\n revocation: Schema.Literals([\"active\", \"pending\", \"revoked\"]),\n createdBy: Schema.String,\n createdAt: Schema.DateTimeUtcFromString,\n}) {}\n\n/** A connection is the principal-facing handle over an authorization; one authorization, many domains. */\nexport class Connection extends Schema.Class<Connection>(\"@domainkit/Storage/Connection\")({\n id: Schema.String,\n ownerId: Schema.String,\n authorizationId: Schema.String,\n createdAt: Schema.DateTimeUtcFromString,\n}) {}\n\nexport class Attachment extends Schema.Class<Attachment>(\"@domainkit/Storage/Attachment\")({\n id: Schema.String,\n ownerId: Schema.String,\n connectionId: Schema.String,\n domain: Schema.String,\n zone: Schema.String,\n /** Provider zone identity (zone id, account id) decoded by the provider's `context` schema. */\n target: Schema.Unknown,\n createdAt: Schema.DateTimeUtcFromString,\n}) {}\n\n/** Interactive-flow state between `Connect.start` and `Connect.complete`. */\nexport class Continuation extends Schema.Class<Continuation>(\"@domainkit/Storage/Continuation\")({\n id: Schema.String,\n ownerId: Schema.String,\n actorId: Schema.String,\n provider: Schema.String,\n payload: Schema.Unknown,\n returnTo: Schema.NullOr(Schema.String),\n expiresAt: Schema.DateTimeUtcFromString,\n}) {}\n\nexport const AttemptStatus = Schema.Literals([\n \"planned\",\n \"approved\",\n \"applying\",\n \"complete\",\n \"partial\",\n \"failed\",\n \"expired\",\n \"rejected\",\n]);\nexport type AttemptStatus = typeof AttemptStatus.Type;\n\n/** Who declined the plan, why, and when. Terminal: the domain needs a new plan. */\nexport const Rejection = Schema.Struct({\n actorId: Schema.String,\n reason: Schema.NullOr(Schema.String),\n at: Schema.DateTimeUtcFromString,\n});\nexport type Rejection = typeof Rejection.Type;\n\n/** One durable plan -> approval -> receipt lifecycle. */\nexport class Attempt extends Schema.Class<Attempt>(\"@domainkit/Storage/Attempt\")({\n id: Plan.PlanId,\n ownerId: Schema.String,\n attachmentId: Schema.String,\n kind: Plan.Kind,\n status: AttemptStatus,\n plan: Plan.Model,\n approval: Schema.NullOr(Approval.Model),\n receipt: Schema.NullOr(Receipt.Model),\n rejection: Schema.NullOr(Rejection),\n /** Cleanup attempts point at the provisioning receipt they undo. */\n sourceReceiptId: Schema.NullOr(Receipt.ReceiptId),\n leaseExpiresAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n /** Why the last apply stopped before any write; cleared on the next claim. */\n failure: Schema.NullOr(Schema.String),\n updatedAt: Schema.DateTimeUtcFromString,\n}) {}\n\nexport const RequirementStatus = Schema.Literals([\"satisfied\", \"missing\", \"mismatch\", \"unknown\"]);\nexport type RequirementStatus = typeof RequirementStatus.Type;\n\nexport const Overall = Schema.Literals([\"ready\", \"pending\", \"failed\"]);\nexport type Overall = typeof Overall.Type;\n\n/**\n * Latest observed readiness for one domain, written by `Verify.observe`. Keyed by domain so\n * observe-only hosts (no attachment, public DNS alone) get the same row; `attachmentId` links the\n * attachment when one exists.\n */\nexport class Readiness extends Schema.Class<Readiness>(\"@domainkit/Storage/Readiness\")({\n domain: Schema.String,\n attachmentId: Schema.NullOr(Schema.String),\n ownerId: Schema.String,\n overall: Overall,\n requirements: Schema.Array(\n Schema.Struct({\n operationId: Schema.NullOr(Plan.OperationId),\n record: DnsRecord.Model,\n status: RequirementStatus,\n /** Encoded `Verify.Evidence` values. */\n evidence: Schema.Array(Schema.Unknown),\n }),\n ),\n /** Encoded `Verify.HostEvidence` values, keyed by `source` on merge. */\n host: Schema.Array(Schema.Unknown),\n /** When the current pending streak began; drives the backoff ladder. */\n pendingSince: Schema.NullOr(Schema.DateTimeUtcFromString),\n checkedAt: Schema.DateTimeUtcFromString,\n nextCheckAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n}) {}\n\n// ---------------------------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------------------------\n\nexport interface Interface {\n readonly authorizations: {\n /**\n * Insert, or CAS-replace when `expectedId` is set: the row must exist under this owner with an\n * active revocation state, else `NotFound` / `Busy`.\n */\n readonly upsert: (input: {\n readonly authorization: Authorization;\n readonly credential: Credential;\n readonly expectedId?: string;\n }) => Fx<Authorization>;\n readonly get: (id: string) => Fx<Authorization>;\n readonly credential: (id: string) => Fx<Credential>;\n readonly rotate: (id: string, credential: Credential) => Fx<void>;\n readonly promoteCapabilities: (id: string, capabilities: ReadonlyArray<Capability>) => Fx<void>;\n /**\n * Two-phase: mark `pending`, run `revoke` outside any transaction, then delete. A failed\n * `revoke` leaves the row pending; `recoverRevocations` finishes it later.\n */\n readonly revoke: <E, R>(\n id: string,\n revoke: Effect.Effect<void, E, R>,\n ) => Effect.Effect<void, Errors.DomainKitError | E, Principal.Service | R>;\n readonly recoverRevocations: <E, R>(\n revoke: (authorization: Authorization) => Effect.Effect<void, E, R>,\n ) => Effect.Effect<number, Errors.DomainKitError | E, Principal.Service | R>;\n };\n readonly connections: {\n readonly create: (authorizationId: string) => Fx<Connection>;\n readonly get: (id: string) => Fx<Connection>;\n readonly list: (filter?: { readonly provider?: string }) => Fx<ReadonlyArray<Connection>>;\n /** Fails `InvalidInput` while attachments still reference the connection. */\n readonly remove: (id: string) => Fx<void>;\n };\n readonly attachments: {\n /** One attachment per (owner, domain); a duplicate fails `InvalidInput`. */\n readonly create: (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone: string;\n readonly target: unknown;\n }) => Fx<Attachment>;\n readonly get: (id: string) => Fx<Attachment>;\n readonly byDomain: (domain: string) => Fx<Option.Option<Attachment>>;\n readonly list: (connectionId: string) => Fx<ReadonlyArray<Attachment>>;\n readonly remove: (id: string) => Fx<void>;\n };\n readonly continuations: {\n readonly put: (continuation: Continuation) => Fx<void>;\n /** Read without spending; a late read fails `Expired`. */\n readonly get: (id: string) => Fx<Continuation>;\n /** Exactly-once: the second consume of the same id fails `NotFound`; a late one fails `Expired`. */\n readonly consume: (id: string) => Fx<Continuation>;\n };\n readonly attempts: {\n readonly create: (attempt: Attempt) => Fx<Attempt>;\n readonly get: (id: Plan.PlanId) => Fx<Attempt>;\n readonly byApproval: (id: Approval.ApprovalId) => Fx<Attempt>;\n readonly byReceipt: (id: Receipt.ReceiptId) => Fx<Attempt>;\n readonly latest: (attachmentId: string, kind: Plan.Kind) => Fx<Option.Option<Attempt>>;\n /** `planned` -> `approved`; approving again with the same approval id is a no-op, anything else fails `Stale`. */\n readonly approve: (id: Plan.PlanId, approval: Approval.Model) => Fx<Attempt>;\n /**\n * `planned` -> `rejected` (terminal). Rejecting again returns the row unchanged; any other\n * status fails `Stale`, `expired` fails `Expired`, and a digest mismatch fails `Stale`.\n */\n readonly reject: (\n id: Plan.PlanId,\n input: {\n readonly digest: Plan.Digest;\n readonly actorId: string;\n readonly reason: string | null;\n },\n ) => Fx<Attempt>;\n /**\n * Atomic transition to `applying` with a lease from `approved`, `failed`, or an expired\n * `applying`; fails `Busy` while a lease is live and `Stale` from any other status.\n */\n readonly claim: (id: Plan.PlanId, lease: DateTime.Utc) => Fx<Attempt>;\n readonly complete: (id: Plan.PlanId, receipt: Receipt.Model) => Fx<Attempt>;\n readonly fail: (id: Plan.PlanId, message: string) => Fx<Attempt>;\n };\n readonly readiness: {\n /** One row per (owner, domain); `attachmentId`, when set, must exist for the owner. */\n readonly put: (readiness: Readiness) => Fx<void>;\n readonly get: (domain: string) => Fx<Option.Option<Readiness>>;\n };\n /** Single-flight guard keyed by string (credential refresh, apply). Fails `Busy` rather than waiting. */\n readonly withLock: <A, E, R>(\n key: string,\n effect: Effect.Effect<A, E, R>,\n ) => Effect.Effect<A, E | Errors.DomainKitError, R | Principal.Service>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Storage\") {}\n\nexport interface MemoryOptions {\n /** Fault injection: runs before every mutation commits; failing here leaves state untouched. */\n readonly beforeCommit?: (operation: string) => Effect.Effect<void, Errors.DomainKitError>;\n}\n\n/** In-memory implementation for tests and local development. Not for production. */\nexport const layerMemory: Layer.Layer<Service> = Layer.sync(Service)(() => makeMemory());\n\n/** `layerMemory` with fault injection for storage-failure tests. */\nexport const layerMemoryWith = (options: MemoryOptions): Layer.Layer<Service> =>\n Layer.sync(Service)(() => makeMemory(options));\n\nexport { makeMemory };\n\n// ---------------------------------------------------------------------------------------------\n// Async adapter\n// ---------------------------------------------------------------------------------------------\n\n/**\n * A Promise-shaped implementation for hosts that persist without Effect. Every method takes the\n * principal explicitly. Rejections become `StorageFailed`; reject with a `DomainKit.Error` to keep\n * a typed reason. Two-phase revocation and locking are split into plain steps so no callback has\n * to run an Effect.\n */\nexport interface AsyncInterface {\n readonly authorizations: {\n readonly upsert: (\n principal: Principal.Interface,\n input: {\n readonly authorization: Authorization;\n readonly credential: Credential;\n readonly expectedId?: string;\n },\n ) => Promise<Authorization>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Authorization>;\n readonly credential: (principal: Principal.Interface, id: string) => Promise<Credential>;\n readonly rotate: (\n principal: Principal.Interface,\n id: string,\n credential: Credential,\n ) => Promise<void>;\n readonly promoteCapabilities: (\n principal: Principal.Interface,\n id: string,\n capabilities: ReadonlyArray<Capability>,\n ) => Promise<void>;\n /** Mark `pending` durably; must be idempotent. */\n readonly prepareRevocation: (\n principal: Principal.Interface,\n id: string,\n ) => Promise<Authorization>;\n /** Delete the authorization after the provider confirmed revocation. */\n readonly completeRevocation: (principal: Principal.Interface, id: string) => Promise<void>;\n readonly pendingRevocations: (\n principal: Principal.Interface,\n ) => Promise<ReadonlyArray<Authorization>>;\n };\n readonly connections: {\n readonly create: (\n principal: Principal.Interface,\n authorizationId: string,\n ) => Promise<Connection>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Connection>;\n readonly list: (\n principal: Principal.Interface,\n filter?: { readonly provider?: string },\n ) => Promise<ReadonlyArray<Connection>>;\n readonly remove: (principal: Principal.Interface, id: string) => Promise<void>;\n };\n readonly attachments: {\n readonly create: (\n principal: Principal.Interface,\n input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone: string;\n readonly target: unknown;\n },\n ) => Promise<Attachment>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Attachment>;\n readonly byDomain: (\n principal: Principal.Interface,\n domain: string,\n ) => Promise<Attachment | null>;\n readonly list: (\n principal: Principal.Interface,\n connectionId: string,\n ) => Promise<ReadonlyArray<Attachment>>;\n readonly remove: (principal: Principal.Interface, id: string) => Promise<void>;\n };\n readonly continuations: {\n readonly put: (principal: Principal.Interface, continuation: Continuation) => Promise<void>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Continuation>;\n readonly consume: (principal: Principal.Interface, id: string) => Promise<Continuation>;\n };\n readonly attempts: {\n readonly create: (principal: Principal.Interface, attempt: Attempt) => Promise<Attempt>;\n readonly get: (principal: Principal.Interface, id: Plan.PlanId) => Promise<Attempt>;\n readonly byApproval: (\n principal: Principal.Interface,\n id: Approval.ApprovalId,\n ) => Promise<Attempt>;\n readonly byReceipt: (principal: Principal.Interface, id: Receipt.ReceiptId) => Promise<Attempt>;\n readonly latest: (\n principal: Principal.Interface,\n attachmentId: string,\n kind: Plan.Kind,\n ) => Promise<Attempt | null>;\n readonly approve: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n approval: Approval.Model,\n ) => Promise<Attempt>;\n readonly reject: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n input: {\n readonly digest: Plan.Digest;\n readonly actorId: string;\n readonly reason: string | null;\n },\n ) => Promise<Attempt>;\n readonly claim: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n lease: DateTime.Utc,\n ) => Promise<Attempt>;\n readonly complete: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n receipt: Receipt.Model,\n ) => Promise<Attempt>;\n readonly fail: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n message: string,\n ) => Promise<Attempt>;\n };\n readonly readiness: {\n readonly put: (principal: Principal.Interface, readiness: Readiness) => Promise<void>;\n readonly get: (principal: Principal.Interface, domain: string) => Promise<Readiness | null>;\n };\n /** Return `false` when another holder has the key. */\n readonly acquireLock: (principal: Principal.Interface, key: string) => Promise<boolean>;\n readonly releaseLock: (principal: Principal.Interface, key: string) => Promise<void>;\n}\n\nconst storageFailed = (operation: string) => (cause: unknown) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.StorageFailed({\n operation,\n message: cause instanceof Error ? cause.message : String(cause),\n }),\n });\n\n/** Wraps a Promise-shaped implementation. Rejections become `StorageFailed`. */\nexport const fromAsync = (service: AsyncInterface): Interface => {\n const call = <A>(operation: string, run: (principal: Principal.Interface) => Promise<A>): Fx<A> =>\n Effect.flatMap(Principal.Service, (principal) =>\n Effect.tryPromise({ try: () => run(principal), catch: storageFailed(operation) }),\n );\n const option = <A>(\n operation: string,\n run: (principal: Principal.Interface) => Promise<A | null>,\n ) => call(operation, run).pipe(Effect.map(Option.fromNullishOr));\n return {\n authorizations: {\n upsert: (input) =>\n call(\"authorizations.upsert\", (p) => service.authorizations.upsert(p, input)),\n get: (id) => call(\"authorizations.get\", (p) => service.authorizations.get(p, id)),\n credential: (id) =>\n call(\"authorizations.credential\", (p) => service.authorizations.credential(p, id)),\n rotate: (id, credential) =>\n call(\"authorizations.rotate\", (p) => service.authorizations.rotate(p, id, credential)),\n promoteCapabilities: (id, capabilities) =>\n call(\"authorizations.promoteCapabilities\", (p) =>\n service.authorizations.promoteCapabilities(p, id, capabilities),\n ),\n revoke: (id, revoke) =>\n call(\"authorizations.prepareRevocation\", (p) =>\n service.authorizations.prepareRevocation(p, id),\n ).pipe(\n Effect.flatMap(() => revoke),\n Effect.flatMap(() =>\n call(\"authorizations.completeRevocation\", (p) =>\n service.authorizations.completeRevocation(p, id),\n ),\n ),\n ),\n recoverRevocations: (revoke) =>\n call(\"authorizations.pendingRevocations\", (p) =>\n service.authorizations.pendingRevocations(p),\n ).pipe(\n Effect.flatMap((pending) =>\n Effect.forEach(pending, (authorization) =>\n revoke(authorization).pipe(\n Effect.flatMap(() =>\n call(\"authorizations.completeRevocation\", (p) =>\n service.authorizations.completeRevocation(p, authorization.id),\n ),\n ),\n ),\n ),\n ),\n Effect.map((completed) => completed.length),\n ),\n },\n connections: {\n create: (authorizationId) =>\n call(\"connections.create\", (p) => service.connections.create(p, authorizationId)),\n get: (id) => call(\"connections.get\", (p) => service.connections.get(p, id)),\n list: (filter) => call(\"connections.list\", (p) => service.connections.list(p, filter)),\n remove: (id) => call(\"connections.remove\", (p) => service.connections.remove(p, id)),\n },\n attachments: {\n create: (input) => call(\"attachments.create\", (p) => service.attachments.create(p, input)),\n get: (id) => call(\"attachments.get\", (p) => service.attachments.get(p, id)),\n byDomain: (domain) =>\n option(\"attachments.byDomain\", (p) => service.attachments.byDomain(p, domain)),\n list: (connectionId) =>\n call(\"attachments.list\", (p) => service.attachments.list(p, connectionId)),\n remove: (id) => call(\"attachments.remove\", (p) => service.attachments.remove(p, id)),\n },\n continuations: {\n put: (continuation) =>\n call(\"continuations.put\", (p) => service.continuations.put(p, continuation)),\n get: (id) => call(\"continuations.get\", (p) => service.continuations.get(p, id)),\n consume: (id) => call(\"continuations.consume\", (p) => service.continuations.consume(p, id)),\n },\n attempts: {\n create: (attempt) => call(\"attempts.create\", (p) => service.attempts.create(p, attempt)),\n get: (id) => call(\"attempts.get\", (p) => service.attempts.get(p, id)),\n byApproval: (id) => call(\"attempts.byApproval\", (p) => service.attempts.byApproval(p, id)),\n byReceipt: (id) => call(\"attempts.byReceipt\", (p) => service.attempts.byReceipt(p, id)),\n latest: (attachmentId, kind) =>\n option(\"attempts.latest\", (p) => service.attempts.latest(p, attachmentId, kind)),\n approve: (id, approval) =>\n call(\"attempts.approve\", (p) => service.attempts.approve(p, id, approval)),\n reject: (id, input) => call(\"attempts.reject\", (p) => service.attempts.reject(p, id, input)),\n claim: (id, lease) => call(\"attempts.claim\", (p) => service.attempts.claim(p, id, lease)),\n complete: (id, receipt) =>\n call(\"attempts.complete\", (p) => service.attempts.complete(p, id, receipt)),\n fail: (id, message) => call(\"attempts.fail\", (p) => service.attempts.fail(p, id, message)),\n },\n readiness: {\n put: (readiness) => call(\"readiness.put\", (p) => service.readiness.put(p, readiness)),\n get: (domain) => option(\"readiness.get\", (p) => service.readiness.get(p, domain)),\n },\n withLock: (key, effect) =>\n Effect.acquireRelease(\n call(\"acquireLock\", (p) => service.acquireLock(p, key)).pipe(\n Effect.flatMap((acquired) =>\n acquired ? Effect.void : Errors.fail(new Reason.Busy({ key })),\n ),\n ),\n () => call(\"releaseLock\", (p) => service.releaseLock(p, key)).pipe(Effect.ignore),\n ).pipe(\n Effect.flatMap(() => effect),\n Effect.scoped,\n ),\n };\n};\n\nexport const layerFromAsync = (service: AsyncInterface): Layer.Layer<Service> =>\n Layer.succeed(Service)(fromAsync(service));\n"],"mappings":"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"}
1
+ {"version":3,"file":"Storage.mjs","names":["Plan.PlanId","Plan.Kind","Plan.Model","Approval.Model","Receipt.Model","Receipt.ReceiptId","Plan.OperationId","DnsRecord.Model","Errors.isDomainKitError","Errors.DomainKitError","Reason.StorageFailed","Principal.Service","Errors.fail","Reason.Busy"],"sources":["../src/Storage.ts"],"sourcesContent":["/**\n * Everything DomainKit persists, behind one service the host installs once. Every method requires\n * `Principal` and is scoped by it. `@domainkit/capsuledb` ships the Postgres implementation;\n * `domainkit/testing` ships the memory one; both pass `Testing.conformance.storage`.\n *\n * Storage never sees plaintext credentials: `Connect` seals secrets through `Custody` before\n * writing a `Credential` row and opens them after reading one.\n *\n * Grouped by noun so the 25-odd methods stay navigable. Implementations must satisfy the\n * invariants documented per group; the conformance suite checks them.\n */\nimport { Context, DateTime, Effect, Layer, Option, Schema } from \"effect\";\n\nimport * as Approval from \"./Approval.ts\";\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport { makeMemory } from \"./internal/storage-memory.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Receipt from \"./Receipt.ts\";\n\nexport type Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\n// ---------------------------------------------------------------------------------------------\n// Rows\n// ---------------------------------------------------------------------------------------------\n\nexport const Capability = Schema.Literals([\"dns:read\", \"dns:write\"]);\nexport type Capability = typeof Capability.Type;\n\nexport const AuthMethod = Schema.Literals([\"token\", \"oauth\", \"integration\"]);\nexport type AuthMethod = typeof AuthMethod.Type;\n\n/** Sealed provider credential plus the metadata needed without unsealing it. */\nexport class Credential extends Schema.Class<Credential>(\"@domainkit/Storage/Credential\")({\n ciphertext: Schema.String,\n expiresAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n rotatedAt: Schema.DateTimeUtcFromString,\n}) {}\n\n/** One provider authorization (an OAuth grant or a token) owned by a principal. */\nexport class Authorization extends Schema.Class<Authorization>(\"@domainkit/Storage/Authorization\")({\n id: Schema.String,\n ownerId: Schema.String,\n provider: Schema.String,\n method: AuthMethod,\n capabilities: Schema.Array(Capability),\n /** Provider-specific account context (account id, team id, ...) decoded by the provider's `context` schema. */\n context: Schema.Unknown,\n revocation: Schema.Literals([\"active\", \"pending\", \"revoked\"]),\n createdBy: Schema.String,\n createdAt: Schema.DateTimeUtcFromString,\n}) {}\n\n/** A connection is the principal-facing handle over an authorization; one authorization, many domains. */\nexport class Connection extends Schema.Class<Connection>(\"@domainkit/Storage/Connection\")({\n id: Schema.String,\n ownerId: Schema.String,\n authorizationId: Schema.String,\n createdAt: Schema.DateTimeUtcFromString,\n}) {}\n\nexport class Attachment extends Schema.Class<Attachment>(\"@domainkit/Storage/Attachment\")({\n id: Schema.String,\n ownerId: Schema.String,\n connectionId: Schema.String,\n domain: Schema.String,\n zone: Schema.String,\n /** The target's label as the provider gave it at attach time, so a UI names the account without a provider call. */\n label: Schema.String,\n /** Provider zone identity (zone id, account id) decoded by the provider's `context` schema. */\n target: Schema.Unknown,\n createdAt: Schema.DateTimeUtcFromString,\n}) {}\n\n/** Interactive-flow state between `Connect.start` and `Connect.complete`. */\nexport class Continuation extends Schema.Class<Continuation>(\"@domainkit/Storage/Continuation\")({\n id: Schema.String,\n ownerId: Schema.String,\n actorId: Schema.String,\n provider: Schema.String,\n payload: Schema.Unknown,\n returnTo: Schema.NullOr(Schema.String),\n expiresAt: Schema.DateTimeUtcFromString,\n}) {}\n\nexport const AttemptStatus = Schema.Literals([\n \"planned\",\n \"approved\",\n \"applying\",\n \"complete\",\n \"partial\",\n \"failed\",\n \"expired\",\n \"rejected\",\n]);\nexport type AttemptStatus = typeof AttemptStatus.Type;\n\n/** Who declined the plan, why, and when. Terminal: the domain needs a new plan. */\nexport const Rejection = Schema.Struct({\n actorId: Schema.String,\n reason: Schema.NullOr(Schema.String),\n at: Schema.DateTimeUtcFromString,\n});\nexport type Rejection = typeof Rejection.Type;\n\n/** One durable plan -> approval -> receipt lifecycle. */\nexport class Attempt extends Schema.Class<Attempt>(\"@domainkit/Storage/Attempt\")({\n id: Plan.PlanId,\n ownerId: Schema.String,\n attachmentId: Schema.String,\n kind: Plan.Kind,\n status: AttemptStatus,\n plan: Plan.Model,\n approval: Schema.NullOr(Approval.Model),\n receipt: Schema.NullOr(Receipt.Model),\n rejection: Schema.NullOr(Rejection),\n /** Cleanup attempts point at the provisioning receipt they undo. */\n sourceReceiptId: Schema.NullOr(Receipt.ReceiptId),\n leaseExpiresAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n /** Why the last apply stopped before any write; cleared on the next claim. */\n failure: Schema.NullOr(Schema.String),\n updatedAt: Schema.DateTimeUtcFromString,\n}) {}\n\nexport const RequirementStatus = Schema.Literals([\"satisfied\", \"missing\", \"mismatch\", \"unknown\"]);\nexport type RequirementStatus = typeof RequirementStatus.Type;\n\nexport const Overall = Schema.Literals([\"ready\", \"pending\", \"failed\"]);\nexport type Overall = typeof Overall.Type;\n\n/**\n * Latest observed readiness for one domain, written by `Verify.observe`. Keyed by domain so\n * observe-only hosts (no attachment, public DNS alone) get the same row; `attachmentId` links the\n * attachment when one exists.\n */\nexport class Readiness extends Schema.Class<Readiness>(\"@domainkit/Storage/Readiness\")({\n domain: Schema.String,\n attachmentId: Schema.NullOr(Schema.String),\n ownerId: Schema.String,\n overall: Overall,\n requirements: Schema.Array(\n Schema.Struct({\n operationId: Schema.NullOr(Plan.OperationId),\n record: DnsRecord.Model,\n status: RequirementStatus,\n /** Encoded `Verify.Evidence` values. */\n evidence: Schema.Array(Schema.Unknown),\n }),\n ),\n /** Encoded `Verify.HostEvidence` values, keyed by `source` on merge. */\n host: Schema.Array(Schema.Unknown),\n /** When the current pending streak began; drives the backoff ladder. */\n pendingSince: Schema.NullOr(Schema.DateTimeUtcFromString),\n checkedAt: Schema.DateTimeUtcFromString,\n nextCheckAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n}) {}\n\n// ---------------------------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------------------------\n\nexport interface Interface {\n readonly authorizations: {\n /**\n * Insert, or CAS-replace when `expectedId` is set: the row must exist under this owner with an\n * active revocation state, else `NotFound` / `Busy`.\n */\n readonly upsert: (input: {\n readonly authorization: Authorization;\n readonly credential: Credential;\n readonly expectedId?: string;\n }) => Fx<Authorization>;\n readonly get: (id: string) => Fx<Authorization>;\n readonly credential: (id: string) => Fx<Credential>;\n readonly rotate: (id: string, credential: Credential) => Fx<void>;\n readonly promoteCapabilities: (id: string, capabilities: ReadonlyArray<Capability>) => Fx<void>;\n /**\n * Two-phase: mark `pending`, run `revoke` outside any transaction, then delete. A failed\n * `revoke` leaves the row pending; `recoverRevocations` finishes it later.\n */\n readonly revoke: <E, R>(\n id: string,\n revoke: Effect.Effect<void, E, R>,\n ) => Effect.Effect<void, Errors.DomainKitError | E, Principal.Service | R>;\n readonly recoverRevocations: <E, R>(\n revoke: (authorization: Authorization) => Effect.Effect<void, E, R>,\n ) => Effect.Effect<number, Errors.DomainKitError | E, Principal.Service | R>;\n };\n readonly connections: {\n readonly create: (authorizationId: string) => Fx<Connection>;\n readonly get: (id: string) => Fx<Connection>;\n readonly list: (filter?: { readonly provider?: string }) => Fx<ReadonlyArray<Connection>>;\n /** Fails `InvalidInput` while attachments still reference the connection. */\n readonly remove: (id: string) => Fx<void>;\n };\n readonly attachments: {\n /** One attachment per (owner, domain); a duplicate fails `InvalidInput`. */\n readonly create: (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone: string;\n readonly label: string;\n readonly target: unknown;\n }) => Fx<Attachment>;\n readonly get: (id: string) => Fx<Attachment>;\n readonly byDomain: (domain: string) => Fx<Option.Option<Attachment>>;\n readonly list: (connectionId: string) => Fx<ReadonlyArray<Attachment>>;\n readonly remove: (id: string) => Fx<void>;\n };\n readonly continuations: {\n readonly put: (continuation: Continuation) => Fx<void>;\n /** Read without spending; a late read fails `Expired`. */\n readonly get: (id: string) => Fx<Continuation>;\n /** Exactly-once: the second consume of the same id fails `NotFound`; a late one fails `Expired`. */\n readonly consume: (id: string) => Fx<Continuation>;\n };\n readonly attempts: {\n readonly create: (attempt: Attempt) => Fx<Attempt>;\n readonly get: (id: Plan.PlanId) => Fx<Attempt>;\n readonly byApproval: (id: Approval.ApprovalId) => Fx<Attempt>;\n readonly byReceipt: (id: Receipt.ReceiptId) => Fx<Attempt>;\n readonly latest: (attachmentId: string, kind: Plan.Kind) => Fx<Option.Option<Attempt>>;\n /** `planned` -> `approved`; approving again with the same approval id is a no-op, anything else fails `Stale`. */\n readonly approve: (id: Plan.PlanId, approval: Approval.Model) => Fx<Attempt>;\n /**\n * `planned` -> `rejected` (terminal). Rejecting again returns the row unchanged; any other\n * status fails `Stale`, `expired` fails `Expired`, and a digest mismatch fails `Stale`.\n */\n readonly reject: (\n id: Plan.PlanId,\n input: {\n readonly digest: Plan.Digest;\n readonly actorId: string;\n readonly reason: string | null;\n },\n ) => Fx<Attempt>;\n /**\n * Atomic transition to `applying` with a lease from `approved`, `failed`, or an expired\n * `applying`; fails `Busy` while a lease is live and `Stale` from any other status.\n */\n readonly claim: (id: Plan.PlanId, lease: DateTime.Utc) => Fx<Attempt>;\n readonly complete: (id: Plan.PlanId, receipt: Receipt.Model) => Fx<Attempt>;\n readonly fail: (id: Plan.PlanId, message: string) => Fx<Attempt>;\n };\n readonly readiness: {\n /** One row per (owner, domain); `attachmentId`, when set, must exist for the owner. */\n readonly put: (readiness: Readiness) => Fx<void>;\n readonly get: (domain: string) => Fx<Option.Option<Readiness>>;\n };\n /** Single-flight guard keyed by string (credential refresh, apply). Fails `Busy` rather than waiting. */\n readonly withLock: <A, E, R>(\n key: string,\n effect: Effect.Effect<A, E, R>,\n ) => Effect.Effect<A, E | Errors.DomainKitError, R | Principal.Service>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@domainkit/Storage\") {}\n\nexport interface MemoryOptions {\n /** Fault injection: runs before every mutation commits; failing here leaves state untouched. */\n readonly beforeCommit?: (operation: string) => Effect.Effect<void, Errors.DomainKitError>;\n}\n\n/** In-memory implementation for tests and local development. Not for production. */\nexport const layerMemory: Layer.Layer<Service> = Layer.sync(Service)(() => makeMemory());\n\n/** `layerMemory` with fault injection for storage-failure tests. */\nexport const layerMemoryWith = (options: MemoryOptions): Layer.Layer<Service> =>\n Layer.sync(Service)(() => makeMemory(options));\n\nexport { makeMemory };\n\n// ---------------------------------------------------------------------------------------------\n// Async adapter\n// ---------------------------------------------------------------------------------------------\n\n/**\n * A Promise-shaped implementation for hosts that persist without Effect. Every method takes the\n * principal explicitly. Rejections become `StorageFailed`; reject with a `DomainKit.Error` to keep\n * a typed reason. Two-phase revocation and locking are split into plain steps so no callback has\n * to run an Effect.\n */\nexport interface AsyncInterface {\n readonly authorizations: {\n readonly upsert: (\n principal: Principal.Interface,\n input: {\n readonly authorization: Authorization;\n readonly credential: Credential;\n readonly expectedId?: string;\n },\n ) => Promise<Authorization>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Authorization>;\n readonly credential: (principal: Principal.Interface, id: string) => Promise<Credential>;\n readonly rotate: (\n principal: Principal.Interface,\n id: string,\n credential: Credential,\n ) => Promise<void>;\n readonly promoteCapabilities: (\n principal: Principal.Interface,\n id: string,\n capabilities: ReadonlyArray<Capability>,\n ) => Promise<void>;\n /** Mark `pending` durably; must be idempotent. */\n readonly prepareRevocation: (\n principal: Principal.Interface,\n id: string,\n ) => Promise<Authorization>;\n /** Delete the authorization after the provider confirmed revocation. */\n readonly completeRevocation: (principal: Principal.Interface, id: string) => Promise<void>;\n readonly pendingRevocations: (\n principal: Principal.Interface,\n ) => Promise<ReadonlyArray<Authorization>>;\n };\n readonly connections: {\n readonly create: (\n principal: Principal.Interface,\n authorizationId: string,\n ) => Promise<Connection>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Connection>;\n readonly list: (\n principal: Principal.Interface,\n filter?: { readonly provider?: string },\n ) => Promise<ReadonlyArray<Connection>>;\n readonly remove: (principal: Principal.Interface, id: string) => Promise<void>;\n };\n readonly attachments: {\n readonly create: (\n principal: Principal.Interface,\n input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone: string;\n readonly label: string;\n readonly target: unknown;\n },\n ) => Promise<Attachment>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Attachment>;\n readonly byDomain: (\n principal: Principal.Interface,\n domain: string,\n ) => Promise<Attachment | null>;\n readonly list: (\n principal: Principal.Interface,\n connectionId: string,\n ) => Promise<ReadonlyArray<Attachment>>;\n readonly remove: (principal: Principal.Interface, id: string) => Promise<void>;\n };\n readonly continuations: {\n readonly put: (principal: Principal.Interface, continuation: Continuation) => Promise<void>;\n readonly get: (principal: Principal.Interface, id: string) => Promise<Continuation>;\n readonly consume: (principal: Principal.Interface, id: string) => Promise<Continuation>;\n };\n readonly attempts: {\n readonly create: (principal: Principal.Interface, attempt: Attempt) => Promise<Attempt>;\n readonly get: (principal: Principal.Interface, id: Plan.PlanId) => Promise<Attempt>;\n readonly byApproval: (\n principal: Principal.Interface,\n id: Approval.ApprovalId,\n ) => Promise<Attempt>;\n readonly byReceipt: (principal: Principal.Interface, id: Receipt.ReceiptId) => Promise<Attempt>;\n readonly latest: (\n principal: Principal.Interface,\n attachmentId: string,\n kind: Plan.Kind,\n ) => Promise<Attempt | null>;\n readonly approve: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n approval: Approval.Model,\n ) => Promise<Attempt>;\n readonly reject: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n input: {\n readonly digest: Plan.Digest;\n readonly actorId: string;\n readonly reason: string | null;\n },\n ) => Promise<Attempt>;\n readonly claim: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n lease: DateTime.Utc,\n ) => Promise<Attempt>;\n readonly complete: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n receipt: Receipt.Model,\n ) => Promise<Attempt>;\n readonly fail: (\n principal: Principal.Interface,\n id: Plan.PlanId,\n message: string,\n ) => Promise<Attempt>;\n };\n readonly readiness: {\n readonly put: (principal: Principal.Interface, readiness: Readiness) => Promise<void>;\n readonly get: (principal: Principal.Interface, domain: string) => Promise<Readiness | null>;\n };\n /** Return `false` when another holder has the key. */\n readonly acquireLock: (principal: Principal.Interface, key: string) => Promise<boolean>;\n readonly releaseLock: (principal: Principal.Interface, key: string) => Promise<void>;\n}\n\nconst storageFailed = (operation: string) => (cause: unknown) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.StorageFailed({\n operation,\n message: cause instanceof Error ? cause.message : String(cause),\n }),\n });\n\n/** Wraps a Promise-shaped implementation. Rejections become `StorageFailed`. */\nexport const fromAsync = (service: AsyncInterface): Interface => {\n const call = <A>(operation: string, run: (principal: Principal.Interface) => Promise<A>): Fx<A> =>\n Effect.flatMap(Principal.Service, (principal) =>\n Effect.tryPromise({ try: () => run(principal), catch: storageFailed(operation) }),\n );\n const option = <A>(\n operation: string,\n run: (principal: Principal.Interface) => Promise<A | null>,\n ) => call(operation, run).pipe(Effect.map(Option.fromNullishOr));\n return {\n authorizations: {\n upsert: (input) =>\n call(\"authorizations.upsert\", (p) => service.authorizations.upsert(p, input)),\n get: (id) => call(\"authorizations.get\", (p) => service.authorizations.get(p, id)),\n credential: (id) =>\n call(\"authorizations.credential\", (p) => service.authorizations.credential(p, id)),\n rotate: (id, credential) =>\n call(\"authorizations.rotate\", (p) => service.authorizations.rotate(p, id, credential)),\n promoteCapabilities: (id, capabilities) =>\n call(\"authorizations.promoteCapabilities\", (p) =>\n service.authorizations.promoteCapabilities(p, id, capabilities),\n ),\n revoke: (id, revoke) =>\n call(\"authorizations.prepareRevocation\", (p) =>\n service.authorizations.prepareRevocation(p, id),\n ).pipe(\n Effect.flatMap(() => revoke),\n Effect.flatMap(() =>\n call(\"authorizations.completeRevocation\", (p) =>\n service.authorizations.completeRevocation(p, id),\n ),\n ),\n ),\n recoverRevocations: (revoke) =>\n call(\"authorizations.pendingRevocations\", (p) =>\n service.authorizations.pendingRevocations(p),\n ).pipe(\n Effect.flatMap((pending) =>\n Effect.forEach(pending, (authorization) =>\n revoke(authorization).pipe(\n Effect.flatMap(() =>\n call(\"authorizations.completeRevocation\", (p) =>\n service.authorizations.completeRevocation(p, authorization.id),\n ),\n ),\n ),\n ),\n ),\n Effect.map((completed) => completed.length),\n ),\n },\n connections: {\n create: (authorizationId) =>\n call(\"connections.create\", (p) => service.connections.create(p, authorizationId)),\n get: (id) => call(\"connections.get\", (p) => service.connections.get(p, id)),\n list: (filter) => call(\"connections.list\", (p) => service.connections.list(p, filter)),\n remove: (id) => call(\"connections.remove\", (p) => service.connections.remove(p, id)),\n },\n attachments: {\n create: (input) => call(\"attachments.create\", (p) => service.attachments.create(p, input)),\n get: (id) => call(\"attachments.get\", (p) => service.attachments.get(p, id)),\n byDomain: (domain) =>\n option(\"attachments.byDomain\", (p) => service.attachments.byDomain(p, domain)),\n list: (connectionId) =>\n call(\"attachments.list\", (p) => service.attachments.list(p, connectionId)),\n remove: (id) => call(\"attachments.remove\", (p) => service.attachments.remove(p, id)),\n },\n continuations: {\n put: (continuation) =>\n call(\"continuations.put\", (p) => service.continuations.put(p, continuation)),\n get: (id) => call(\"continuations.get\", (p) => service.continuations.get(p, id)),\n consume: (id) => call(\"continuations.consume\", (p) => service.continuations.consume(p, id)),\n },\n attempts: {\n create: (attempt) => call(\"attempts.create\", (p) => service.attempts.create(p, attempt)),\n get: (id) => call(\"attempts.get\", (p) => service.attempts.get(p, id)),\n byApproval: (id) => call(\"attempts.byApproval\", (p) => service.attempts.byApproval(p, id)),\n byReceipt: (id) => call(\"attempts.byReceipt\", (p) => service.attempts.byReceipt(p, id)),\n latest: (attachmentId, kind) =>\n option(\"attempts.latest\", (p) => service.attempts.latest(p, attachmentId, kind)),\n approve: (id, approval) =>\n call(\"attempts.approve\", (p) => service.attempts.approve(p, id, approval)),\n reject: (id, input) => call(\"attempts.reject\", (p) => service.attempts.reject(p, id, input)),\n claim: (id, lease) => call(\"attempts.claim\", (p) => service.attempts.claim(p, id, lease)),\n complete: (id, receipt) =>\n call(\"attempts.complete\", (p) => service.attempts.complete(p, id, receipt)),\n fail: (id, message) => call(\"attempts.fail\", (p) => service.attempts.fail(p, id, message)),\n },\n readiness: {\n put: (readiness) => call(\"readiness.put\", (p) => service.readiness.put(p, readiness)),\n get: (domain) => option(\"readiness.get\", (p) => service.readiness.get(p, domain)),\n },\n withLock: (key, effect) =>\n Effect.acquireRelease(\n call(\"acquireLock\", (p) => service.acquireLock(p, key)).pipe(\n Effect.flatMap((acquired) =>\n acquired ? Effect.void : Errors.fail(new Reason.Busy({ key })),\n ),\n ),\n () => call(\"releaseLock\", (p) => service.releaseLock(p, key)).pipe(Effect.ignore),\n ).pipe(\n Effect.flatMap(() => effect),\n Effect.scoped,\n ),\n };\n};\n\nexport const layerFromAsync = (service: AsyncInterface): Layer.Layer<Service> =>\n Layer.succeed(Service)(fromAsync(service));\n"],"mappings":"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"}
package/dist/Testing.mjs CHANGED
@@ -53,6 +53,7 @@ const provider = (options = {}) => {
53
53
  const normalized = fromStringUnsafe(name);
54
54
  const zone = {
55
55
  name: normalized,
56
+ label: options.labels?.[name] ?? options.labels?.[normalized] ?? normalized,
56
57
  nameservers: options.nameservers?.[name] ?? options.nameservers?.[normalized] ?? [`ns1.${normalized}`, `ns2.${normalized}`],
57
58
  rows: []
58
59
  };
@@ -87,7 +88,7 @@ const provider = (options = {}) => {
87
88
  const targets = () => [...zones.values()].map((zone) => ({
88
89
  zone: zone.name,
89
90
  context: { zone: zone.name },
90
- label: zone.name,
91
+ label: zone.label,
91
92
  nameservers: zone.nameservers
92
93
  }));
93
94
  const oauth = {
@@ -110,7 +111,7 @@ const provider = (options = {}) => {
110
111
  return {
111
112
  ...make$1({
112
113
  id,
113
- name: `Fake ${id}`,
114
+ name: options.name ?? `Fake ${id}`,
114
115
  ...options.nameserverSuffixes === void 0 ? {} : { nameservers: options.nameserverSuffixes },
115
116
  context: Context$1,
116
117
  contextVersion: "fake.v1",
@@ -176,15 +177,15 @@ const fakeNameservers = (name) => [...registry].filter((zone) => zone.name === n
176
177
  /** Records every fake provider currently holds for `name`, across all fake zones. */
177
178
  const fakeRecords = (name) => [...registry].flatMap((zone) => zone.rows.map(({ record }) => record)).filter((record) => record.name === name);
178
179
  /**
179
- * A resolver answering from the fake providers' zones, or from an explicit table. Every answer
180
- * is attributed to the resolver named `fake`.
180
+ * A resolver answering from the fake providers' zones, or from an explicit table. Every answer is
181
+ * attributed to `options.id`, which a fixture that ships screenshots names after a real resolver.
181
182
  */
182
- const resolver = (answers) => Layer.succeed(Service)({ resolve: (name, type) => Effect.sync(() => {
183
+ const resolver = (answers, options = {}) => Layer.succeed(Service)({ resolve: (name, type) => Effect.sync(() => {
183
184
  const normalized = fromString(name);
184
185
  const lookup = normalized._tag === "Some" ? normalized.value : name;
185
186
  const records = (answers === void 0 ? [...fakeRecords(lookup), ...fakeNameservers(lookup)] : answers.filter((entry) => entry.name === lookup).flatMap((entry) => entry.records)).filter((record) => (record._tag === "Opaque" ? record.type : record._tag) === type);
186
187
  return [Outcome.Answered({ answer: {
187
- resolver: "fake",
188
+ resolver: options.id ?? "fake",
188
189
  records,
189
190
  negative: records.length === 0,
190
191
  ttl: 60
@@ -198,7 +199,7 @@ const transport = (options = {}) => {
198
199
  const fake = provider(options.provider);
199
200
  const { handler } = toWebHandler(layerMemory$1({
200
201
  providers: [fake],
201
- resolver: resolver()
202
+ resolver: resolver(void 0, options.resolver)
202
203
  }).pipe(Layer.merge(Layer.succeed(Identity)({ principal: () => Effect.succeed(principal) }))));
203
204
  const live = fromFetch("http://domainkit.test", {
204
205
  fetch: (input, init) => handler(new Request(input, init)),
@@ -1 +1 @@
1
- {"version":3,"file":"Testing.mjs","names":["Storage.layerMemory","Principal.make","Context","DomainName.fromStringUnsafe","Errors.fail","Reason.NotFound","Reason.Unauthenticated","Provider.make","Provider.tokenAuth","DomainName.decode","Provider.resolveAmong","Reason.ProviderUnavailable","DomainName.isWithin","Reason.ProviderRejected","DnsRecord.ns","Resolver.Service","DomainName.fromString","Server.toWebHandler","DomainKit.layerMemory","Server.Identity","Transport.fromFetch","Transport.capabilities","storageCases","providerCases"],"sources":["../src/Testing.ts"],"sourcesContent":["/**\n * domainkit/testing — fakes and conformance runners so hosts test against the seam instead of\n * stubbing global fetch.\n */\nimport { DateTime, Effect, Layer, Redacted, Schema } from \"effect\";\n\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as DomainKit from \"./DomainKit.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport { provider as providerCases } from \"./internal/conformance/provider.ts\";\nimport { storage as storageCases } from \"./internal/conformance/storage.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Provider from \"./Provider.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Server from \"./Server.ts\";\nimport * as Storage from \"./Storage.ts\";\nimport * as Transport from \"./Transport.ts\";\n\n/** In-memory Storage. Same as `Storage.layerMemory`; re-exported for discoverability. */\nexport const storage: Layer.Layer<Storage.Service> = Storage.layerMemory;\n\nexport const principal: Principal.Interface = Principal.make({\n ownerId: \"org_test\",\n actorId: \"user_test\",\n});\n\nexport interface FakeProviderOptions {\n readonly id?: string;\n /** Zones the fake credential can reach. Default: `example.com`. */\n readonly zones?: ReadonlyArray<string>;\n /** Pre-existing records, to produce `Noop` and `Conflict` operations. */\n readonly records?: ReadonlyArray<{ readonly zone: string; readonly record: DnsRecord.Observed }>;\n /** Offer OAuth in addition to tokens; the fake redirects to `callbackUrl` immediately. */\n readonly oauth?: boolean;\n /** Fail the write at this zero-based index, to exercise partial receipts. */\n readonly failWrite?: (index: number) => boolean;\n /** Nameservers per zone; default `ns1.<zone>`, `ns2.<zone>`. `resolver()` answers NS queries from them. */\n readonly nameservers?: Readonly<Record<string, ReadonlyArray<string>>>;\n /** Suffixes the fake declares as `Definition.nameservers`, so `Connect.discover` can name it as a host. */\n readonly nameserverSuffixes?: ReadonlyArray<string>;\n}\n\nexport interface FakeProvider extends Provider.Definition<FakeContext> {\n readonly records: (zone: string) => ReadonlyArray<DnsRecord.Observed>;\n /** Every credential the fake issued, oldest first. */\n readonly issued: () => ReadonlyArray<string>;\n /** How many credentials were revoked at the fake. */\n readonly revoked: () => number;\n}\n\ninterface Zone {\n readonly name: string;\n readonly nameservers: ReadonlyArray<string>;\n readonly rows: Array<{ readonly id: string; readonly record: DnsRecord.Observed }>;\n}\n\n/** Every fake zone created in this process, so `resolver()` can answer from them. */\nconst registry = new Set<Zone>();\n\n/** Account context carries `account`; target context carries `zone`. One schema covers both. */\nconst Context = Schema.Struct({\n account: Schema.optionalKey(Schema.String),\n zone: Schema.optionalKey(Schema.String),\n});\ntype FakeContext = typeof Context.Type;\nconst TargetContext = Schema.Struct({ zone: Schema.String });\n\n/** A provider definition with a token method (and optional OAuth) over in-memory zones. */\nexport const provider = (options: FakeProviderOptions = {}): FakeProvider => {\n const id = options.id ?? \"fake\";\n const zones = new Map<string, Zone>();\n let nextId = 1;\n let writes = 0;\n let revokedCount = 0;\n const issued: Array<string> = [];\n for (const name of options.zones ?? [\"example.com\"]) {\n const normalized = DomainName.fromStringUnsafe(name);\n const zone: Zone = {\n name: normalized,\n nameservers: options.nameservers?.[name] ??\n options.nameservers?.[normalized] ?? [`ns1.${normalized}`, `ns2.${normalized}`],\n rows: [],\n };\n zones.set(zone.name, zone);\n registry.add(zone);\n }\n for (const { zone: name, record } of options.records ?? []) {\n const zone = zones.get(DomainName.fromStringUnsafe(name));\n if (zone === undefined) throw new Error(`Fake provider ${id} has no zone ${name}`);\n zone.rows.push({ id: `${id}-${nextId++}`, record });\n }\n\n const issue = (prefix: string): Provider.IssuedCredential => {\n const secret = `${prefix}-${issued.length + 1}`;\n issued.push(secret);\n return { secret: Redacted.make(secret), context: { account: id }, expiresAt: null };\n };\n\n const zoneOf = (target: Provider.Target) =>\n Effect.suspend(() => {\n const decoded = Schema.decodeUnknownOption(TargetContext)(target.context);\n const zone = decoded._tag === \"Some\" ? zones.get(decoded.value.zone) : undefined;\n return zone === undefined\n ? Errors.fail(new Reason.NotFound({ entity: \"zone\", id: target.zone }))\n : Effect.succeed(zone);\n });\n\n const targets = () =>\n [...zones.values()].map((zone): Provider.Target => ({\n zone: zone.name,\n context: { zone: zone.name },\n label: zone.name,\n nameservers: zone.nameservers,\n }));\n\n const oauth: Provider.OAuthAuth = {\n label: \"Sign in (fake)\",\n scopes: [\"dns\"],\n start: (input) =>\n Effect.succeed({\n authorizationUrl: `${input.callbackUrl}?${new URLSearchParams({\n state: input.state,\n code: \"fake-code\",\n })}`,\n }),\n complete: (input) =>\n input.code === \"fake-code\"\n ? Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n }))\n : Errors.fail(new Reason.Unauthenticated({ message: \"fake provider rejected the code\" })),\n refresh: (credential) =>\n Redacted.value(credential.secret).startsWith(\"revoked\")\n ? Errors.fail(new Reason.Unauthenticated({ message: \"fake refresh token was revoked\" }))\n : Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n })),\n revoke: () => Effect.sync(() => void (revokedCount += 1)),\n };\n\n const definition = Provider.make<FakeContext>({\n id,\n name: `Fake ${id}`,\n ...(options.nameserverSuffixes === undefined\n ? {}\n : { nameservers: options.nameserverSuffixes }),\n context: Context,\n contextVersion: \"fake.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"Token (fake)\",\n requiredCapabilities: [\"dns:read\", \"dns:write\"],\n fields: Schema.Struct({ token: Schema.RedactedFromValue(Schema.String) }),\n authenticate: ({ token }) =>\n Redacted.value(token).length === 0\n ? Errors.fail(\n new Reason.Unauthenticated({\n message: \"fake provider rejected an empty token\",\n }),\n )\n : Effect.succeed({ secret: token, context: { account: id }, expiresAt: null }),\n }),\n ...(options.oauth === true ? { oauth } : {}),\n },\n session: () => ({\n capabilities: () => Effect.succeed([\"dns:read\", \"dns:write\"]),\n listTargets: () => Effect.succeed(targets()),\n resolveTarget: (domain) =>\n Effect.map(DomainName.decode(domain), (name) => Provider.resolveAmong(name, targets())),\n dns: (target) => ({\n list: () =>\n Effect.map(zoneOf(target), (zone) =>\n zone.rows.map(({ id: providerRecordId, record }) => ({ record, providerRecordId })),\n ),\n create: (_zone, record) =>\n Effect.gen(function* () {\n const zone = yield* zoneOf(target);\n const index = writes;\n writes += 1;\n if (options.failWrite?.(index) === true) {\n return yield* Errors.fail(\n new Reason.ProviderUnavailable({\n provider: id,\n message: `fake provider failed write ${index}`,\n }),\n );\n }\n if (!DomainName.isWithin(record.name, zone.name)) {\n return yield* Errors.fail(\n new Reason.ProviderRejected({\n provider: id,\n message: `${record.name} is outside ${zone.name}`,\n }),\n );\n }\n const providerRecordId = `${id}-${nextId++}`;\n zone.rows.push({ id: providerRecordId, record });\n return { providerRecordId };\n }),\n get: (_zone, providerRecordId) =>\n Effect.map(\n zoneOf(target),\n (zone) => zone.rows.find((row) => row.id === providerRecordId)?.record ?? null,\n ),\n delete: (_zone, providerRecordId) =>\n Effect.map(zoneOf(target), (zone) => {\n const index = zone.rows.findIndex((row) => row.id === providerRecordId);\n if (index >= 0) zone.rows.splice(index, 1);\n }),\n }),\n }),\n });\n\n return {\n ...definition,\n records: (zone) =>\n zones.get(DomainName.fromStringUnsafe(zone))?.rows.map(({ record }) => record) ?? [],\n issued: () => issued,\n revoked: () => revokedCount,\n };\n};\n\n/** NS records for every fake zone named `name`, so `resolver()` can answer authority queries. */\nconst fakeNameservers = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .filter((zone) => zone.name === name)\n .flatMap((zone) =>\n zone.nameservers.map((nameserver) => DnsRecord.ns({ name: zone.name, nameserver })),\n );\n\n/** Records every fake provider currently holds for `name`, across all fake zones. */\nexport const fakeRecords = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .flatMap((zone) => zone.rows.map(({ record }) => record))\n .filter((record) => record.name === name);\n\n/**\n * A resolver answering from the fake providers' zones, or from an explicit table. Every answer\n * is attributed to the resolver named `fake`.\n */\nexport const resolver = (\n answers?: ReadonlyArray<{\n readonly name: string;\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n }>,\n): Layer.Layer<Resolver.Service> =>\n Layer.succeed(Resolver.Service)({\n resolve: (name, type) =>\n Effect.sync(() => {\n const normalized = DomainName.fromString(name);\n const lookup = normalized._tag === \"Some\" ? normalized.value : name;\n const table =\n answers === undefined\n ? [...fakeRecords(lookup), ...fakeNameservers(lookup)]\n : answers.filter((entry) => entry.name === lookup).flatMap((entry) => entry.records);\n const records = table.filter(\n (record) => (record._tag === \"Opaque\" ? record.type : record._tag) === type,\n );\n return [\n Resolver.Outcome.Answered({\n answer: { resolver: \"fake\", records, negative: records.length === 0, ttl: 60 },\n }),\n ];\n }),\n });\n\nexport interface TransportOptions {\n /** Which groups the fake server exposes, so a UI can be tested against a partial host. */\n readonly capabilities?: ReadonlyArray<Transport.Capability>;\n readonly provider?: FakeProviderOptions;\n}\n\nexport interface RecordedCall {\n /** `connection.inspect`, `provisioning.approve`, ... */\n readonly method: string;\n /** The call's only argument, or the argument list when the method takes more than one. */\n readonly input: unknown;\n}\n\ntype TransportMethod = (\n ...args: ReadonlyArray<never>\n) => Effect.Effect<unknown, Errors.DomainKitError>;\n\nexport interface RecordingTransport extends Transport.Interface {\n readonly calls: ReadonlyArray<RecordedCall>;\n}\n\n/**\n * A transport over an in-memory `domainkit/server` and memory Storage, recording every call. What\n * `@domainkit/react` tests render against, instead of stubbing global `fetch`.\n */\nexport const transport = (options: TransportOptions = {}): RecordingTransport => {\n const fake = provider(options.provider);\n // The handler's layer holds memory Storage and a throwaway custody key, so there is nothing to\n // release; a test that wants the server disposed builds `Server.toWebHandler` itself.\n const { handler } = Server.toWebHandler(\n DomainKit.layerMemory({ providers: [fake], resolver: resolver() }).pipe(\n Layer.merge(Layer.succeed(Server.Identity)({ principal: () => Effect.succeed(principal) })),\n ),\n );\n const live = Transport.fromFetch(\"http://domainkit.test\", {\n fetch: (input, init) => handler(new Request(input, init)),\n ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),\n });\n const calls: Array<RecordedCall> = [];\n const recorded = Object.fromEntries(\n Transport.capabilities(live).map((capability) => [\n capability,\n Object.fromEntries(\n Object.entries(live[capability] as unknown as Record<string, TransportMethod>).map(\n ([name, method]) => [\n name,\n (...args: ReadonlyArray<never>) =>\n Effect.suspend(() => {\n calls.push({\n method: `${capability}.${name}`,\n input: args.length > 1 ? args : args[0],\n });\n return method(...args);\n }),\n ],\n ),\n ),\n ]),\n ) as Transport.Interface;\n return { ...recorded, calls };\n};\n\nexport type { Case as ProviderCase } from \"./internal/conformance/provider.ts\";\nexport type { Case as StorageCase } from \"./internal/conformance/storage.ts\";\n\nexport const conformance = {\n /** Runs every Storage invariant (tenant isolation, leases, exactly-once continuations, revocation recovery). */\n storage: storageCases,\n /** Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and rejected-token against a real provider definition. */\n provider: providerCases,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"Testing.mjs","names":["Storage.layerMemory","Principal.make","Context","DomainName.fromStringUnsafe","Errors.fail","Reason.NotFound","Reason.Unauthenticated","Provider.make","Provider.tokenAuth","DomainName.decode","Provider.resolveAmong","Reason.ProviderUnavailable","DomainName.isWithin","Reason.ProviderRejected","DnsRecord.ns","Resolver.Service","DomainName.fromString","Server.toWebHandler","DomainKit.layerMemory","Server.Identity","Transport.fromFetch","Transport.capabilities","storageCases","providerCases"],"sources":["../src/Testing.ts"],"sourcesContent":["/**\n * domainkit/testing — fakes and conformance runners so hosts test against the seam instead of\n * stubbing global fetch.\n */\nimport { DateTime, Effect, Layer, Redacted, Schema } from \"effect\";\n\nimport * as DnsRecord from \"./DnsRecord.ts\";\nimport * as DomainKit from \"./DomainKit.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as DomainName from \"./DomainName.ts\";\nimport { provider as providerCases } from \"./internal/conformance/provider.ts\";\nimport { storage as storageCases } from \"./internal/conformance/storage.ts\";\nimport * as Principal from \"./Principal.ts\";\nimport * as Provider from \"./Provider.ts\";\nimport * as Resolver from \"./Resolver.ts\";\nimport * as Server from \"./Server.ts\";\nimport * as Storage from \"./Storage.ts\";\nimport * as Transport from \"./Transport.ts\";\n\n/** In-memory Storage. Same as `Storage.layerMemory`; re-exported for discoverability. */\nexport const storage: Layer.Layer<Storage.Service> = Storage.layerMemory;\n\nexport const principal: Principal.Interface = Principal.make({\n ownerId: \"org_test\",\n actorId: \"user_test\",\n});\n\nexport interface FakeProviderOptions {\n readonly id?: string;\n /** The name a customer reads. Default `Fake <id>`; a fixture that ships screenshots sets it. */\n readonly name?: string;\n /** Zones the fake credential can reach. Default: `example.com`. */\n readonly zones?: ReadonlyArray<string>;\n /** The label each zone carries, keyed by zone. Default: the zone name. */\n readonly labels?: Readonly<Record<string, string>>;\n /** Pre-existing records, to produce `Noop` and `Conflict` operations. */\n readonly records?: ReadonlyArray<{ readonly zone: string; readonly record: DnsRecord.Observed }>;\n /** Offer OAuth in addition to tokens; the fake redirects to `callbackUrl` immediately. */\n readonly oauth?: boolean;\n /** Fail the write at this zero-based index, to exercise partial receipts. */\n readonly failWrite?: (index: number) => boolean;\n /** Nameservers per zone; default `ns1.<zone>`, `ns2.<zone>`. `resolver()` answers NS queries from them. */\n readonly nameservers?: Readonly<Record<string, ReadonlyArray<string>>>;\n /** Suffixes the fake declares as `Definition.nameservers`, so `Connect.discover` can name it as a host. */\n readonly nameserverSuffixes?: ReadonlyArray<string>;\n}\n\nexport interface FakeProvider extends Provider.Definition<FakeContext> {\n readonly records: (zone: string) => ReadonlyArray<DnsRecord.Observed>;\n /** Every credential the fake issued, oldest first. */\n readonly issued: () => ReadonlyArray<string>;\n /** How many credentials were revoked at the fake. */\n readonly revoked: () => number;\n}\n\ninterface Zone {\n readonly name: string;\n /** What a customer reads for this zone: the account it sits in, where the fake names one. */\n readonly label: string;\n readonly nameservers: ReadonlyArray<string>;\n readonly rows: Array<{ readonly id: string; readonly record: DnsRecord.Observed }>;\n}\n\n/** Every fake zone created in this process, so `resolver()` can answer from them. */\nconst registry = new Set<Zone>();\n\n/** Account context carries `account`; target context carries `zone`. One schema covers both. */\nconst Context = Schema.Struct({\n account: Schema.optionalKey(Schema.String),\n zone: Schema.optionalKey(Schema.String),\n});\ntype FakeContext = typeof Context.Type;\nconst TargetContext = Schema.Struct({ zone: Schema.String });\n\n/** A provider definition with a token method (and optional OAuth) over in-memory zones. */\nexport const provider = (options: FakeProviderOptions = {}): FakeProvider => {\n const id = options.id ?? \"fake\";\n const zones = new Map<string, Zone>();\n let nextId = 1;\n let writes = 0;\n let revokedCount = 0;\n const issued: Array<string> = [];\n for (const name of options.zones ?? [\"example.com\"]) {\n const normalized = DomainName.fromStringUnsafe(name);\n const zone: Zone = {\n name: normalized,\n label: options.labels?.[name] ?? options.labels?.[normalized] ?? normalized,\n nameservers: options.nameservers?.[name] ??\n options.nameservers?.[normalized] ?? [`ns1.${normalized}`, `ns2.${normalized}`],\n rows: [],\n };\n zones.set(zone.name, zone);\n registry.add(zone);\n }\n for (const { zone: name, record } of options.records ?? []) {\n const zone = zones.get(DomainName.fromStringUnsafe(name));\n if (zone === undefined) throw new Error(`Fake provider ${id} has no zone ${name}`);\n zone.rows.push({ id: `${id}-${nextId++}`, record });\n }\n\n const issue = (prefix: string): Provider.IssuedCredential => {\n const secret = `${prefix}-${issued.length + 1}`;\n issued.push(secret);\n return { secret: Redacted.make(secret), context: { account: id }, expiresAt: null };\n };\n\n const zoneOf = (target: Provider.Target) =>\n Effect.suspend(() => {\n const decoded = Schema.decodeUnknownOption(TargetContext)(target.context);\n const zone = decoded._tag === \"Some\" ? zones.get(decoded.value.zone) : undefined;\n return zone === undefined\n ? Errors.fail(new Reason.NotFound({ entity: \"zone\", id: target.zone }))\n : Effect.succeed(zone);\n });\n\n const targets = () =>\n [...zones.values()].map((zone): Provider.Target => ({\n zone: zone.name,\n context: { zone: zone.name },\n label: zone.label,\n nameservers: zone.nameservers,\n }));\n\n const oauth: Provider.OAuthAuth = {\n label: \"Sign in (fake)\",\n scopes: [\"dns\"],\n start: (input) =>\n Effect.succeed({\n authorizationUrl: `${input.callbackUrl}?${new URLSearchParams({\n state: input.state,\n code: \"fake-code\",\n })}`,\n }),\n complete: (input) =>\n input.code === \"fake-code\"\n ? Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n }))\n : Errors.fail(new Reason.Unauthenticated({ message: \"fake provider rejected the code\" })),\n refresh: (credential) =>\n Redacted.value(credential.secret).startsWith(\"revoked\")\n ? Errors.fail(new Reason.Unauthenticated({ message: \"fake refresh token was revoked\" }))\n : Effect.map(DateTime.now, (now) => ({\n ...issue(\"oauth\"),\n expiresAt: DateTime.add(now, { hours: 1 }),\n })),\n revoke: () => Effect.sync(() => void (revokedCount += 1)),\n };\n\n const definition = Provider.make<FakeContext>({\n id,\n name: options.name ?? `Fake ${id}`,\n ...(options.nameserverSuffixes === undefined\n ? {}\n : { nameservers: options.nameserverSuffixes }),\n context: Context,\n contextVersion: \"fake.v1\",\n auth: {\n token: Provider.tokenAuth({\n label: \"Token (fake)\",\n requiredCapabilities: [\"dns:read\", \"dns:write\"],\n fields: Schema.Struct({ token: Schema.RedactedFromValue(Schema.String) }),\n authenticate: ({ token }) =>\n Redacted.value(token).length === 0\n ? Errors.fail(\n new Reason.Unauthenticated({\n message: \"fake provider rejected an empty token\",\n }),\n )\n : Effect.succeed({ secret: token, context: { account: id }, expiresAt: null }),\n }),\n ...(options.oauth === true ? { oauth } : {}),\n },\n session: () => ({\n capabilities: () => Effect.succeed([\"dns:read\", \"dns:write\"]),\n listTargets: () => Effect.succeed(targets()),\n resolveTarget: (domain) =>\n Effect.map(DomainName.decode(domain), (name) => Provider.resolveAmong(name, targets())),\n dns: (target) => ({\n list: () =>\n Effect.map(zoneOf(target), (zone) =>\n zone.rows.map(({ id: providerRecordId, record }) => ({ record, providerRecordId })),\n ),\n create: (_zone, record) =>\n Effect.gen(function* () {\n const zone = yield* zoneOf(target);\n const index = writes;\n writes += 1;\n if (options.failWrite?.(index) === true) {\n return yield* Errors.fail(\n new Reason.ProviderUnavailable({\n provider: id,\n message: `fake provider failed write ${index}`,\n }),\n );\n }\n if (!DomainName.isWithin(record.name, zone.name)) {\n return yield* Errors.fail(\n new Reason.ProviderRejected({\n provider: id,\n message: `${record.name} is outside ${zone.name}`,\n }),\n );\n }\n const providerRecordId = `${id}-${nextId++}`;\n zone.rows.push({ id: providerRecordId, record });\n return { providerRecordId };\n }),\n get: (_zone, providerRecordId) =>\n Effect.map(\n zoneOf(target),\n (zone) => zone.rows.find((row) => row.id === providerRecordId)?.record ?? null,\n ),\n delete: (_zone, providerRecordId) =>\n Effect.map(zoneOf(target), (zone) => {\n const index = zone.rows.findIndex((row) => row.id === providerRecordId);\n if (index >= 0) zone.rows.splice(index, 1);\n }),\n }),\n }),\n });\n\n return {\n ...definition,\n records: (zone) =>\n zones.get(DomainName.fromStringUnsafe(zone))?.rows.map(({ record }) => record) ?? [],\n issued: () => issued,\n revoked: () => revokedCount,\n };\n};\n\n/** NS records for every fake zone named `name`, so `resolver()` can answer authority queries. */\nconst fakeNameservers = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .filter((zone) => zone.name === name)\n .flatMap((zone) =>\n zone.nameservers.map((nameserver) => DnsRecord.ns({ name: zone.name, nameserver })),\n );\n\n/** Records every fake provider currently holds for `name`, across all fake zones. */\nexport const fakeRecords = (name: string): ReadonlyArray<DnsRecord.Observed> =>\n [...registry]\n .flatMap((zone) => zone.rows.map(({ record }) => record))\n .filter((record) => record.name === name);\n\nexport interface FakeResolverOptions {\n /** The name every answer carries. A UI reads it as the observer. Default `fake`. */\n readonly id?: string;\n}\n\n/**\n * A resolver answering from the fake providers' zones, or from an explicit table. Every answer is\n * attributed to `options.id`, which a fixture that ships screenshots names after a real resolver.\n */\nexport const resolver = (\n answers?: ReadonlyArray<{\n readonly name: string;\n readonly records: ReadonlyArray<DnsRecord.Observed>;\n }>,\n options: FakeResolverOptions = {},\n): Layer.Layer<Resolver.Service> =>\n Layer.succeed(Resolver.Service)({\n resolve: (name, type) =>\n Effect.sync(() => {\n const normalized = DomainName.fromString(name);\n const lookup = normalized._tag === \"Some\" ? normalized.value : name;\n const table =\n answers === undefined\n ? [...fakeRecords(lookup), ...fakeNameservers(lookup)]\n : answers.filter((entry) => entry.name === lookup).flatMap((entry) => entry.records);\n const records = table.filter(\n (record) => (record._tag === \"Opaque\" ? record.type : record._tag) === type,\n );\n return [\n Resolver.Outcome.Answered({\n answer: {\n resolver: options.id ?? \"fake\",\n records,\n negative: records.length === 0,\n ttl: 60,\n },\n }),\n ];\n }),\n });\n\nexport interface TransportOptions {\n /** Which groups the fake server exposes, so a UI can be tested against a partial host. */\n readonly capabilities?: ReadonlyArray<Transport.Capability>;\n readonly provider?: FakeProviderOptions;\n readonly resolver?: FakeResolverOptions;\n}\n\nexport interface RecordedCall {\n /** `connection.inspect`, `provisioning.approve`, ... */\n readonly method: string;\n /** The call's only argument, or the argument list when the method takes more than one. */\n readonly input: unknown;\n}\n\ntype TransportMethod = (\n ...args: ReadonlyArray<never>\n) => Effect.Effect<unknown, Errors.DomainKitError>;\n\nexport interface RecordingTransport extends Transport.Interface {\n readonly calls: ReadonlyArray<RecordedCall>;\n}\n\n/**\n * A transport over an in-memory `domainkit/server` and memory Storage, recording every call. What\n * `@domainkit/react` tests render against, instead of stubbing global `fetch`.\n */\nexport const transport = (options: TransportOptions = {}): RecordingTransport => {\n const fake = provider(options.provider);\n // The handler's layer holds memory Storage and a throwaway custody key, so there is nothing to\n // release; a test that wants the server disposed builds `Server.toWebHandler` itself.\n const { handler } = Server.toWebHandler(\n DomainKit.layerMemory({\n providers: [fake],\n resolver: resolver(undefined, options.resolver),\n }).pipe(\n Layer.merge(Layer.succeed(Server.Identity)({ principal: () => Effect.succeed(principal) })),\n ),\n );\n const live = Transport.fromFetch(\"http://domainkit.test\", {\n fetch: (input, init) => handler(new Request(input, init)),\n ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),\n });\n const calls: Array<RecordedCall> = [];\n const recorded = Object.fromEntries(\n Transport.capabilities(live).map((capability) => [\n capability,\n Object.fromEntries(\n Object.entries(live[capability] as unknown as Record<string, TransportMethod>).map(\n ([name, method]) => [\n name,\n (...args: ReadonlyArray<never>) =>\n Effect.suspend(() => {\n calls.push({\n method: `${capability}.${name}`,\n input: args.length > 1 ? args : args[0],\n });\n return method(...args);\n }),\n ],\n ),\n ),\n ]),\n ) as Transport.Interface;\n return { ...recorded, calls };\n};\n\nexport type { Case as ProviderCase } from \"./internal/conformance/provider.ts\";\nexport type { Case as StorageCase } from \"./internal/conformance/storage.ts\";\n\nexport const conformance = {\n /** Runs every Storage invariant (tenant isolation, leases, exactly-once continuations, revocation recovery). */\n storage: storageCases,\n /** Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and rejected-token against a real provider definition. */\n provider: providerCases,\n};\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { DomainKitError, decode, isDomainKitError } from "./internal/error.mjs";
5
5
  import { Model as Model$1 } from "./Approval.mjs";
6
6
  import { Model as Model$2 } from "./Receipt.mjs";
7
7
  import { classify, headersFrom, requestJson } from "./internal/http.mjs";
8
- import { ApprovePayload, AttachPayload, Attempt, Discovery, Integration, OAuth, ObservePayload, PlanPayload, Readiness, RejectPayload, Snapshot, StartPayload, Started, Token } from "./Server.mjs";
8
+ import { ApprovePayload, AttachPayload, Attempt, Discovery, Integration, OAuth, ObservePayload, PlanPayload, Readiness, ReconnectPayload, RejectPayload, Snapshot, StartPayload, Started, Token, Zones } from "./Server.mjs";
9
9
  import { Effect, Schema } from "effect";
10
10
  //#region src/Transport.ts
11
11
  /**
@@ -97,10 +97,28 @@ const fromFetch = (baseUrl, options = {}) => {
97
97
  path: `/domains/${encodeURIComponent(domain)}/discovery`,
98
98
  success: Discovery
99
99
  }),
100
+ zones: (filter) => request({
101
+ method: "GET",
102
+ path: filter?.provider === void 0 ? "/zones" : `/zones?provider=${encodeURIComponent(filter.provider)}`,
103
+ success: Zones
104
+ }),
100
105
  start: (input) => request({
101
106
  method: "POST",
102
107
  path: "/connections",
103
- body: Schema.encodeSync(StartPayload)(input),
108
+ body: Schema.encodeSync(StartPayload)(input.domain === void 0 ? {
109
+ provider: input.provider,
110
+ method: input.method
111
+ } : {
112
+ domain: input.domain,
113
+ provider: input.provider,
114
+ method: input.method
115
+ }),
116
+ success: Started
117
+ }),
118
+ reconnect: (input) => request({
119
+ method: "POST",
120
+ path: `/connections/${encodeURIComponent(input.connectionId)}/reconnections`,
121
+ body: Schema.encodeSync(ReconnectPayload)({ method: input.method }),
104
122
  success: Started
105
123
  }),
106
124
  attach: (input) => request({
@@ -137,6 +155,11 @@ const fromFetch = (baseUrl, options = {}) => {
137
155
  method: "GET",
138
156
  path: `/plans/${encodeURIComponent(planId)}`,
139
157
  success: Attempt
158
+ }),
159
+ receipt: (receiptId) => request({
160
+ method: "GET",
161
+ path: `/receipts/${encodeURIComponent(receiptId)}`,
162
+ success: Model$2
140
163
  })
141
164
  },
142
165
  verification: { observe: (domain, input) => request({
@@ -1 +1 @@
1
- {"version":3,"file":"Transport.mjs","names":["Server.Token","Server.OAuth","Server.Integration","Http.headersFrom","Http.requestJson","Errors.decode","Errors.DomainKitError","Http.classify","Server.ApprovePayload","Approval.Model","Server.RejectPayload","Server.Attempt","Receipt.Model","Server.Snapshot","Server.Discovery","Server.StartPayload","Server.Started","Server.AttachPayload","Server.PlanPayload","Plan.Model","Server.ObservePayload","Server.Readiness","Errors.isDomainKitError","Reason.ProviderUnavailable"],"sources":["../src/Transport.ts"],"sourcesContent":["/**\n * domainkit/client — the browser-side transport `@domainkit/react` consumes. Capability groups are\n * optional: a host that only exposes connection routes declares just `connection`, and the parts\n * of the UI that plan or clean up do not render.\n *\n * const transport = Transport.fromFetch(\"/api/domainkit\")\n *\n * Every method decodes the same wire schemas `domainkit/server` encodes, so a failure arrives as\n * the `DomainKit.Error` the lifecycle raised, reason intact.\n */\nimport { Effect, Schema } from \"effect\";\n\nimport * as Approval from \"./Approval.ts\";\nimport type * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Http from \"./internal/http.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as Receipt from \"./Receipt.ts\";\nimport * as Server from \"./Server.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nexport type Snapshot = Server.Snapshot;\nexport type Started = Server.Started;\nexport type Readiness = Server.Readiness;\nexport type Attempt = Server.Attempt;\nexport type Candidate = Server.Candidate;\nexport type Discovery = Server.Discovery;\nexport type Field = Server.Field;\nexport type MethodDescriptor = Server.MethodDescriptor;\nexport type Method = Server.Method;\n\n/** How a client asks to connect. Mirrors `Connect.Method`, over the wire. */\nexport const Method = {\n /** A lone string for the common one-field provider, or the values its descriptor names. */\n token: (values: string | Readonly<Record<string, string>>): Method =>\n new Server.Token({ values: typeof values === \"string\" ? { token: values } : values }),\n oauth: (options: { readonly returnTo?: string } = {}): Method => new Server.OAuth(options),\n integration: (options: { readonly returnTo?: string } = {}): Method =>\n new Server.Integration(options),\n} as const;\n\nexport interface ConnectionGroup {\n readonly inspect: (domain: string) => Fx<Snapshot>;\n /** Which of this owner's existing connections already reaches the domain. */\n readonly discover: (domain: string) => Fx<Discovery>;\n readonly start: (input: {\n readonly domain: string;\n readonly provider: string;\n readonly method: Method;\n }) => Fx<Started>;\n readonly attach: (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone?: string;\n }) => Fx<Started>;\n readonly detach: (attachmentId: string) => Fx<void>;\n readonly disconnect: (connectionId: string) => Fx<void>;\n}\n\nexport interface ProvisioningGroup {\n readonly plan: (input: {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Plan.Model>;\n readonly approve: (input: {\n readonly planId: Plan.PlanId;\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n }) => Fx<Approval.Model>;\n /** Decline the plan; terminal, and approving it afterwards fails `Stale`. */\n readonly reject: (input: {\n readonly planId: Plan.PlanId;\n readonly reason?: string;\n }) => Fx<Attempt>;\n readonly apply: (approvalId: Approval.ApprovalId) => Fx<Receipt.Model>;\n /** The stored plan with its status, approval, receipt, and rejection. */\n readonly attempt: (planId: Plan.PlanId) => Fx<Attempt>;\n}\n\nexport interface VerificationGroup {\n /**\n * Observe the domain's provisioning receipt, or `options.requirements` when supplied (required\n * for a domain with no attachment).\n */\n readonly observe: (\n domain: string,\n options?: { readonly requirements?: ReadonlyArray<DnsRecord.Model> },\n ) => Fx<Readiness>;\n}\n\nexport interface CleanupGroup {\n readonly plan: (receiptId: Receipt.ReceiptId) => Fx<Plan.Model>;\n readonly approve: (input: {\n readonly planId: Plan.PlanId;\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n }) => Fx<Approval.Model>;\n readonly reject: (input: {\n readonly planId: Plan.PlanId;\n readonly reason?: string;\n }) => Fx<Attempt>;\n readonly apply: (approvalId: Approval.ApprovalId) => Fx<Receipt.Model>;\n}\n\nexport interface Interface {\n readonly connection?: ConnectionGroup;\n readonly provisioning?: ProvisioningGroup;\n readonly verification?: VerificationGroup;\n readonly cleanup?: CleanupGroup;\n}\n\nexport type Capability = keyof Interface;\n\nexport const allCapabilities: ReadonlyArray<Capability> = [\n \"connection\",\n \"provisioning\",\n \"verification\",\n \"cleanup\",\n];\n\n/** Which groups this transport actually carries, in declaration order. */\nexport const capabilities = (transport: Interface): ReadonlyArray<Capability> =>\n allCapabilities.filter((capability) => transport[capability] !== undefined);\n\nexport interface FetchOptions {\n readonly fetch?: Http.Fetch;\n readonly headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n /** Which groups the server exposes. Default: all. */\n readonly capabilities?: ReadonlyArray<Capability>;\n}\n\n// ---------------------------------------------------------------------------------------------\n// fetch\n// ---------------------------------------------------------------------------------------------\n\n/** A transport over `domainkit/server` routes mounted at `baseUrl`. */\nexport const fromFetch = (baseUrl: string, options: FetchOptions = {}): Interface => {\n const base = baseUrl.replace(/\\/+$/, \"\");\n const source = safeOrigin(base);\n // Resolved at call time and invoked as a free function: a browser `fetch` throws \"Illegal\n // invocation\" when called as a method of anything but `window`, and a host may polyfill later.\n const call: Http.Fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));\n const declared = options.capabilities ?? allCapabilities;\n\n const request = <A>(input: {\n readonly method: \"GET\" | \"POST\" | \"DELETE\";\n readonly path: string;\n readonly body?: unknown;\n readonly success: Schema.Codec<A, unknown> | null;\n }): Fx<A> =>\n Effect.gen(function* () {\n const headers = yield* Http.headersFrom(options.headers);\n if (input.body !== undefined) headers.set(\"content-type\", \"application/json\");\n const reply = yield* Http.requestJson({\n fetch: call,\n provider: source,\n url: `${base}${input.path}`,\n init: {\n method: input.method,\n headers,\n ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),\n },\n });\n if (!reply.ok) {\n // The server answers failures with the `DomainKit.Error` value itself. Anything else came\n // from in front of it: a proxy, a login page, a maintenance window.\n const decoded = yield* Effect.result(\n Errors.decode(Errors.DomainKitError, reply.body, \"response\"),\n );\n return yield* decoded._tag === \"Success\"\n ? Effect.fail(decoded.success)\n : Effect.fail(\n new Errors.DomainKitError({\n reason: Http.classify(\n source,\n reply.status,\n reply.headers,\n `${input.method} ${input.path} answered ${reply.status}`,\n ),\n }),\n );\n }\n if (input.success === null) return undefined as A;\n return yield* Errors.decode(input.success, reply.body, \"response\");\n });\n\n /** Provisioning and cleanup share these routes; the attempt knows its kind. */\n const approve = (input: {\n readonly planId: Plan.PlanId;\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n }) =>\n request({\n method: \"POST\",\n path: `/plans/${encodeURIComponent(input.planId)}/approvals`,\n body: Schema.encodeSync(Server.ApprovePayload)(\n input.operationIds === undefined ? {} : { operationIds: input.operationIds },\n ),\n success: Approval.Model,\n });\n\n const reject = (input: { readonly planId: Plan.PlanId; readonly reason?: string }) =>\n request({\n method: \"POST\",\n path: `/plans/${encodeURIComponent(input.planId)}/rejections`,\n body: Schema.encodeSync(Server.RejectPayload)(\n input.reason === undefined ? {} : { reason: input.reason },\n ),\n success: Server.Attempt,\n });\n\n const apply = (approvalId: Approval.ApprovalId) =>\n request({\n method: \"POST\",\n path: `/approvals/${encodeURIComponent(approvalId)}/apply`,\n success: Receipt.Model,\n });\n\n const connection: ConnectionGroup = {\n inspect: (domain) =>\n request({\n method: \"GET\",\n path: `/domains/${encodeURIComponent(domain)}`,\n success: Server.Snapshot,\n }),\n discover: (domain) =>\n request({\n method: \"GET\",\n path: `/domains/${encodeURIComponent(domain)}/discovery`,\n success: Server.Discovery,\n }),\n start: (input) =>\n request({\n method: \"POST\",\n path: \"/connections\",\n body: Schema.encodeSync(Server.StartPayload)(input),\n success: Server.Started,\n }),\n attach: (input) =>\n request({\n method: \"POST\",\n path: `/connections/${encodeURIComponent(input.connectionId)}/attachments`,\n body: Schema.encodeSync(Server.AttachPayload)(\n input.zone === undefined\n ? { domain: input.domain }\n : { domain: input.domain, zone: input.zone },\n ),\n success: Server.Started,\n }),\n detach: (attachmentId) =>\n request({\n method: \"DELETE\",\n path: `/attachments/${encodeURIComponent(attachmentId)}`,\n success: null,\n }),\n disconnect: (connectionId) =>\n request({\n method: \"DELETE\",\n path: `/connections/${encodeURIComponent(connectionId)}`,\n success: null,\n }),\n };\n\n const provisioning: ProvisioningGroup = {\n plan: (input) =>\n request({\n method: \"POST\",\n path: `/domains/${encodeURIComponent(input.domain)}/plans`,\n body: Schema.encodeSync(Server.PlanPayload)({ requirements: input.requirements }),\n success: Plan.Model,\n }),\n approve,\n reject,\n apply,\n attempt: (planId) =>\n request({\n method: \"GET\",\n path: `/plans/${encodeURIComponent(planId)}`,\n success: Server.Attempt,\n }),\n };\n\n const verification: VerificationGroup = {\n observe: (domain, input) =>\n request({\n method: \"POST\",\n path: `/domains/${encodeURIComponent(domain)}/observations`,\n body: Schema.encodeSync(Server.ObservePayload)(\n input?.requirements === undefined ? {} : { requirements: input.requirements },\n ),\n success: Server.Readiness,\n }),\n };\n\n const cleanup: CleanupGroup = {\n plan: (receiptId) =>\n request({\n method: \"POST\",\n path: `/receipts/${encodeURIComponent(receiptId)}/cleanup-plans`,\n success: Plan.Model,\n }),\n approve,\n reject,\n apply,\n };\n\n const groups = { connection, provisioning, verification, cleanup };\n return Object.fromEntries(\n allCapabilities\n .filter((capability) => declared.includes(capability))\n .map((capability) => [capability, groups[capability]]),\n );\n};\n\n// ---------------------------------------------------------------------------------------------\n// Promise adapters\n// ---------------------------------------------------------------------------------------------\n\n/** The same transport in Promises, for hosts and tests that do not run Effect. */\nexport type AsyncTransport = {\n readonly [K in keyof Interface]?: {\n readonly [M in keyof NonNullable<Interface[K]>]: NonNullable<Interface[K]>[M] extends (\n ...args: infer A\n ) => Effect.Effect<infer R, Errors.DomainKitError>\n ? (...args: A) => Promise<R>\n : never;\n };\n};\n\n/** Adapt a Promise-shaped transport; a rejection that is not a `DomainKit.Error` becomes one. */\nexport const fromAsync = (transport: AsyncTransport): Interface =>\n mapGroups(\n transport,\n (method) =>\n (...args: ReadonlyArray<never>) =>\n Effect.tryPromise({\n try: () => method(...args) as Promise<unknown>,\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider: \"domainkit\",\n message: `The transport rejected: ${String(cause)}`,\n }),\n }),\n }),\n ) as Interface;\n\n/** Adapt to Promises; every rejection is the `DomainKit.Error` the lifecycle raised. */\nexport const toAsync = (transport: Interface): AsyncTransport =>\n mapGroups(\n transport,\n (method) =>\n (...args: ReadonlyArray<never>) =>\n Effect.runPromise(method(...args) as Effect.Effect<unknown, Errors.DomainKitError>),\n ) as AsyncTransport;\n\ntype AnyMethod = (...args: ReadonlyArray<never>) => unknown;\ntype AnyGroups = { readonly [key: string]: Record<string, AnyMethod> | undefined };\n\n/** Rebuild every declared group with each method wrapped; the two adapters differ only in `wrap`. */\nconst mapGroups = (source: object, wrap: (method: AnyMethod) => AnyMethod): unknown =>\n Object.fromEntries(\n allCapabilities.flatMap((capability) => {\n const group = (source as AnyGroups)[capability];\n if (group === undefined) return [];\n return [\n [\n capability,\n Object.fromEntries(Object.entries(group).map(([name, method]) => [name, wrap(method)])),\n ],\n ];\n }),\n );\n\n// ---------------------------------------------------------------------------------------------\n// Internals\n// ---------------------------------------------------------------------------------------------\n\nconst safeOrigin = (base: string): string => {\n try {\n return new URL(base).origin;\n } catch {\n return base === \"\" ? \"domainkit\" : base;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,SAAS;;CAEpB,QAAQ,WACN,IAAIA,MAAa,EAAE,QAAQ,OAAO,WAAW,WAAW,EAAE,OAAO,OAAO,IAAI,OAAO,CAAC;CACtF,QAAQ,UAA0C,CAAC,MAAc,IAAIC,MAAa,OAAO;CACzF,cAAc,UAA0C,CAAC,MACvD,IAAIC,YAAmB,OAAO;AAClC;AAwEA,MAAa,kBAA6C;CACxD;CACA;CACA;CACA;AACF;;AAGA,MAAa,gBAAgB,cAC3B,gBAAgB,QAAQ,eAAe,UAAU,gBAAgB,KAAA,CAAS;;AAc5E,MAAa,aAAa,SAAiB,UAAwB,CAAC,MAAiB;CACnF,MAAM,OAAO,QAAQ,QAAQ,QAAQ,EAAE;CACvC,MAAM,SAAS,WAAW,IAAI;CAG9B,MAAM,OAAmB,QAAQ,WAAW,OAAO,SAAS,WAAW,MAAM,OAAO,IAAI;CACxF,MAAM,WAAW,QAAQ,gBAAgB;CAEzC,MAAM,WAAc,UAMlB,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAOC,YAAiB,QAAQ,OAAO;EACvD,IAAI,MAAM,SAAS,KAAA,GAAW,QAAQ,IAAI,gBAAgB,kBAAkB;EAC5E,MAAM,QAAQ,OAAOC,YAAiB;GACpC,OAAO;GACP,UAAU;GACV,KAAK,GAAG,OAAO,MAAM;GACrB,MAAM;IACJ,QAAQ,MAAM;IACd;IACA,GAAI,MAAM,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,MAAM,IAAI,EAAE;GACzE;EACF,CAAC;EACD,IAAI,CAAC,MAAM,IAAI;GAGb,MAAM,UAAU,OAAO,OAAO,OAC5BC,OAAcC,gBAAuB,MAAM,MAAM,UAAU,CAC7D;GACA,OAAO,OAAO,QAAQ,SAAS,YAC3B,OAAO,KAAK,QAAQ,OAAO,IAC3B,OAAO,KACL,IAAIA,eAAsB,EACxB,QAAQC,SACN,QACA,MAAM,QACN,MAAM,SACN,GAAG,MAAM,OAAO,GAAG,MAAM,KAAK,YAAY,MAAM,QAClD,EACF,CAAC,CACH;EACN;EACA,IAAI,MAAM,YAAY,MAAM,OAAO,KAAA;EACnC,OAAO,OAAOF,OAAc,MAAM,SAAS,MAAM,MAAM,UAAU;CACnE,CAAC;;CAGH,MAAM,WAAW,UAIf,QAAQ;EACN,QAAQ;EACR,MAAM,UAAU,mBAAmB,MAAM,MAAM,EAAE;EACjD,MAAM,OAAO,WAAWG,cAAqB,CAAC,CAC5C,MAAM,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,MAAM,aAAa,CAC7E;EACA,SAASC;CACX,CAAC;CAEH,MAAM,UAAU,UACd,QAAQ;EACN,QAAQ;EACR,MAAM,UAAU,mBAAmB,MAAM,MAAM,EAAE;EACjD,MAAM,OAAO,WAAWC,aAAoB,CAAC,CAC3C,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO,CAC3D;EACA,SAASC;CACX,CAAC;CAEH,MAAM,SAAS,eACb,QAAQ;EACN,QAAQ;EACR,MAAM,cAAc,mBAAmB,UAAU,EAAE;EACnD,SAASC;CACX,CAAC;CA0FH,MAAM,SAAS;EAAE,YAAA;GAvFf,UAAU,WACR,QAAQ;IACN,QAAQ;IACR,MAAM,YAAY,mBAAmB,MAAM;IAC3C,SAASC;GACX,CAAC;GACH,WAAW,WACT,QAAQ;IACN,QAAQ;IACR,MAAM,YAAY,mBAAmB,MAAM,EAAE;IAC7C,SAASC;GACX,CAAC;GACH,QAAQ,UACN,QAAQ;IACN,QAAQ;IACR,MAAM;IACN,MAAM,OAAO,WAAWC,YAAmB,CAAC,CAAC,KAAK;IAClD,SAASC;GACX,CAAC;GACH,SAAS,UACP,QAAQ;IACN,QAAQ;IACR,MAAM,gBAAgB,mBAAmB,MAAM,YAAY,EAAE;IAC7D,MAAM,OAAO,WAAWC,aAAoB,CAAC,CAC3C,MAAM,SAAS,KAAA,IACX,EAAE,QAAQ,MAAM,OAAO,IACvB;KAAE,QAAQ,MAAM;KAAQ,MAAM,MAAM;IAAK,CAC/C;IACA,SAASD;GACX,CAAC;GACH,SAAS,iBACP,QAAQ;IACN,QAAQ;IACR,MAAM,gBAAgB,mBAAmB,YAAY;IACrD,SAAS;GACX,CAAC;GACH,aAAa,iBACX,QAAQ;IACN,QAAQ;IACR,MAAM,gBAAgB,mBAAmB,YAAY;IACrD,SAAS;GACX,CAAC;EA8CqB;EAAG,cAAA;GA1C3B,OAAO,UACL,QAAQ;IACN,QAAQ;IACR,MAAM,YAAY,mBAAmB,MAAM,MAAM,EAAE;IACnD,MAAM,OAAO,WAAWE,WAAkB,CAAC,CAAC,EAAE,cAAc,MAAM,aAAa,CAAC;IAChF,SAASC;GACX,CAAC;GACH;GACA;GACA;GACA,UAAU,WACR,QAAQ;IACN,QAAQ;IACR,MAAM,UAAU,mBAAmB,MAAM;IACzC,SAASR;GACX,CAAC;EA2BmC;EAAG,cAAA,EAvBzC,UAAU,QAAQ,UAChB,QAAQ;GACN,QAAQ;GACR,MAAM,YAAY,mBAAmB,MAAM,EAAE;GAC7C,MAAM,OAAO,WAAWS,cAAqB,CAAC,CAC5C,OAAO,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,MAAM,aAAa,CAC9E;GACA,SAASC;EACX,CAAC,EAeiD;EAAG,SAAA;GAXvD,OAAO,cACL,QAAQ;IACN,QAAQ;IACR,MAAM,aAAa,mBAAmB,SAAS,EAAE;IACjD,SAASF;GACX,CAAC;GACH;GACA;GACA;EAG6D;CAAE;CACjE,OAAO,OAAO,YACZ,gBACG,QAAQ,eAAe,SAAS,SAAS,UAAU,CAAC,CAAC,CACrD,KAAK,eAAe,CAAC,YAAY,OAAO,WAAW,CAAC,CACzD;AACF;;AAkBA,MAAa,aAAa,cACxB,UACE,YACC,YACE,GAAG,SACF,OAAO,WAAW;CAChB,WAAW,OAAO,GAAG,IAAI;CACzB,QAAQ,UACNG,iBAAwB,KAAK,IACzB,QACA,IAAIhB,eAAsB,EACxB,QAAQ,IAAIiB,oBAA2B;EACrC,UAAU;EACV,SAAS,2BAA2B,OAAO,KAAK;CAClD,CAAC,EACH,CAAC;AACT,CAAC,CACP;;AAGF,MAAa,WAAW,cACtB,UACE,YACC,YACE,GAAG,SACF,OAAO,WAAW,OAAO,GAAG,IAAI,CAAkD,CACxF;;AAMF,MAAM,aAAa,QAAgB,SACjC,OAAO,YACL,gBAAgB,SAAS,eAAe;CACtC,MAAM,QAAS,OAAqB;CACpC,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;CACjC,OAAO,CACL,CACE,YACA,OAAO,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,CACxF,CACF;AACF,CAAC,CACH;AAMF,MAAM,cAAc,SAAyB;CAC3C,IAAI;EACF,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC;CACvB,QAAQ;EACN,OAAO,SAAS,KAAK,cAAc;CACrC;AACF"}
1
+ {"version":3,"file":"Transport.mjs","names":["Server.Token","Server.OAuth","Server.Integration","Http.headersFrom","Http.requestJson","Errors.decode","Errors.DomainKitError","Http.classify","Server.ApprovePayload","Approval.Model","Server.RejectPayload","Server.Attempt","Receipt.Model","Server.Snapshot","Server.Discovery","Server.Zones","Server.StartPayload","Server.Started","Server.ReconnectPayload","Server.AttachPayload","Server.PlanPayload","Plan.Model","Server.ObservePayload","Server.Readiness","Errors.isDomainKitError","Reason.ProviderUnavailable"],"sources":["../src/Transport.ts"],"sourcesContent":["/**\n * domainkit/client — the browser-side transport `@domainkit/react` consumes. Capability groups are\n * optional: a host that only exposes connection routes declares just `connection`, and the parts\n * of the UI that plan or clean up do not render.\n *\n * const transport = Transport.fromFetch(\"/api/domainkit\")\n *\n * Every method decodes the same wire schemas `domainkit/server` encodes, so a failure arrives as\n * the `DomainKit.Error` the lifecycle raised, reason intact.\n */\nimport { Effect, Schema } from \"effect\";\n\nimport * as Approval from \"./Approval.ts\";\nimport type * as DnsRecord from \"./DnsRecord.ts\";\nimport * as Errors from \"./internal/error.ts\";\nimport * as Http from \"./internal/http.ts\";\nimport * as Plan from \"./Plan.ts\";\nimport * as Reason from \"./Reason.ts\";\nimport * as Receipt from \"./Receipt.ts\";\nimport * as Server from \"./Server.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError>;\n\nexport type Snapshot = Server.Snapshot;\nexport type Started = Server.Started;\nexport type Readiness = Server.Readiness;\nexport type Attempt = Server.Attempt;\nexport type Candidate = Server.Candidate;\nexport type Discovery = Server.Discovery;\nexport type Zone = Server.Zone;\nexport type Zones = Server.Zones;\nexport type Attachment = Server.Attachment;\nexport type Field = Server.Field;\nexport type MethodDescriptor = Server.MethodDescriptor;\nexport type Method = Server.Method;\n\n/** How a client asks to connect. Mirrors `Connect.Method`, over the wire. */\nexport const Method = {\n /** A lone string for the common one-field provider, or the values its descriptor names. */\n token: (values: string | Readonly<Record<string, string>>): Method =>\n new Server.Token({ values: typeof values === \"string\" ? { token: values } : values }),\n oauth: (options: { readonly returnTo?: string } = {}): Method => new Server.OAuth(options),\n integration: (options: { readonly returnTo?: string } = {}): Method =>\n new Server.Integration(options),\n} as const;\n\nexport interface ConnectionGroup {\n readonly inspect: (domain: string) => Fx<Snapshot>;\n /** Which of this owner's existing connections already reaches the domain. */\n readonly discover: (domain: string) => Fx<Discovery>;\n /** Every zone this owner's connections reach, and where each connection stands. */\n readonly zones: (options?: { readonly provider?: string }) => Fx<Zones>;\n /** Connect a provider. Without a domain the account connects alone and attaches nothing. */\n readonly start: (input: {\n readonly domain?: string;\n readonly provider: string;\n readonly method: Method;\n }) => Fx<Started>;\n /** Prove an account again for a connection this owner holds, keeping its domains. */\n readonly reconnect: (input: {\n readonly connectionId: string;\n readonly method: Method;\n }) => Fx<Started>;\n readonly attach: (input: {\n readonly connectionId: string;\n readonly domain: string;\n readonly zone?: string;\n }) => Fx<Started>;\n readonly detach: (attachmentId: string) => Fx<void>;\n readonly disconnect: (connectionId: string) => Fx<void>;\n}\n\nexport interface ProvisioningGroup {\n readonly plan: (input: {\n readonly domain: string;\n readonly requirements: ReadonlyArray<DnsRecord.Model>;\n }) => Fx<Plan.Model>;\n readonly approve: (input: {\n readonly planId: Plan.PlanId;\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n }) => Fx<Approval.Model>;\n /** Decline the plan; terminal, and approving it afterwards fails `Stale`. */\n readonly reject: (input: {\n readonly planId: Plan.PlanId;\n readonly reason?: string;\n }) => Fx<Attempt>;\n readonly apply: (approvalId: Approval.ApprovalId) => Fx<Receipt.Model>;\n /** The stored plan with its status, approval, receipt, and rejection. */\n readonly attempt: (planId: Plan.PlanId) => Fx<Attempt>;\n /** What one apply landed, by receipt id, for a surface that holds the id and not the attempt. */\n readonly receipt: (receiptId: Receipt.ReceiptId) => Fx<Receipt.Model>;\n}\n\nexport interface VerificationGroup {\n /**\n * Observe the domain's provisioning receipt, or `options.requirements` when supplied (required\n * for a domain with no attachment).\n */\n readonly observe: (\n domain: string,\n options?: { readonly requirements?: ReadonlyArray<DnsRecord.Model> },\n ) => Fx<Readiness>;\n}\n\nexport interface CleanupGroup {\n readonly plan: (receiptId: Receipt.ReceiptId) => Fx<Plan.Model>;\n readonly approve: (input: {\n readonly planId: Plan.PlanId;\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n }) => Fx<Approval.Model>;\n readonly reject: (input: {\n readonly planId: Plan.PlanId;\n readonly reason?: string;\n }) => Fx<Attempt>;\n readonly apply: (approvalId: Approval.ApprovalId) => Fx<Receipt.Model>;\n}\n\nexport interface Interface {\n readonly connection?: ConnectionGroup;\n readonly provisioning?: ProvisioningGroup;\n readonly verification?: VerificationGroup;\n readonly cleanup?: CleanupGroup;\n}\n\nexport type Capability = keyof Interface;\n\nexport const allCapabilities: ReadonlyArray<Capability> = [\n \"connection\",\n \"provisioning\",\n \"verification\",\n \"cleanup\",\n];\n\n/** Which groups this transport actually carries, in declaration order. */\nexport const capabilities = (transport: Interface): ReadonlyArray<Capability> =>\n allCapabilities.filter((capability) => transport[capability] !== undefined);\n\nexport interface FetchOptions {\n readonly fetch?: Http.Fetch;\n readonly headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n /** Which groups the server exposes. Default: all. */\n readonly capabilities?: ReadonlyArray<Capability>;\n}\n\n// ---------------------------------------------------------------------------------------------\n// fetch\n// ---------------------------------------------------------------------------------------------\n\n/** A transport over `domainkit/server` routes mounted at `baseUrl`. */\nexport const fromFetch = (baseUrl: string, options: FetchOptions = {}): Interface => {\n const base = baseUrl.replace(/\\/+$/, \"\");\n const source = safeOrigin(base);\n // Resolved at call time and invoked as a free function: a browser `fetch` throws \"Illegal\n // invocation\" when called as a method of anything but `window`, and a host may polyfill later.\n const call: Http.Fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));\n const declared = options.capabilities ?? allCapabilities;\n\n const request = <A>(input: {\n readonly method: \"GET\" | \"POST\" | \"DELETE\";\n readonly path: string;\n readonly body?: unknown;\n readonly success: Schema.Codec<A, unknown> | null;\n }): Fx<A> =>\n Effect.gen(function* () {\n const headers = yield* Http.headersFrom(options.headers);\n if (input.body !== undefined) headers.set(\"content-type\", \"application/json\");\n const reply = yield* Http.requestJson({\n fetch: call,\n provider: source,\n url: `${base}${input.path}`,\n init: {\n method: input.method,\n headers,\n ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),\n },\n });\n if (!reply.ok) {\n // The server answers failures with the `DomainKit.Error` value itself. Anything else came\n // from in front of it: a proxy, a login page, a maintenance window.\n const decoded = yield* Effect.result(\n Errors.decode(Errors.DomainKitError, reply.body, \"response\"),\n );\n return yield* decoded._tag === \"Success\"\n ? Effect.fail(decoded.success)\n : Effect.fail(\n new Errors.DomainKitError({\n reason: Http.classify(\n source,\n reply.status,\n reply.headers,\n `${input.method} ${input.path} answered ${reply.status}`,\n ),\n }),\n );\n }\n if (input.success === null) return undefined as A;\n return yield* Errors.decode(input.success, reply.body, \"response\");\n });\n\n /** Provisioning and cleanup share these routes; the attempt knows its kind. */\n const approve = (input: {\n readonly planId: Plan.PlanId;\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n }) =>\n request({\n method: \"POST\",\n path: `/plans/${encodeURIComponent(input.planId)}/approvals`,\n body: Schema.encodeSync(Server.ApprovePayload)(\n input.operationIds === undefined ? {} : { operationIds: input.operationIds },\n ),\n success: Approval.Model,\n });\n\n const reject = (input: { readonly planId: Plan.PlanId; readonly reason?: string }) =>\n request({\n method: \"POST\",\n path: `/plans/${encodeURIComponent(input.planId)}/rejections`,\n body: Schema.encodeSync(Server.RejectPayload)(\n input.reason === undefined ? {} : { reason: input.reason },\n ),\n success: Server.Attempt,\n });\n\n const apply = (approvalId: Approval.ApprovalId) =>\n request({\n method: \"POST\",\n path: `/approvals/${encodeURIComponent(approvalId)}/apply`,\n success: Receipt.Model,\n });\n\n const connection: ConnectionGroup = {\n inspect: (domain) =>\n request({\n method: \"GET\",\n path: `/domains/${encodeURIComponent(domain)}`,\n success: Server.Snapshot,\n }),\n discover: (domain) =>\n request({\n method: \"GET\",\n path: `/domains/${encodeURIComponent(domain)}/discovery`,\n success: Server.Discovery,\n }),\n zones: (filter) =>\n request({\n method: \"GET\",\n path:\n filter?.provider === undefined\n ? \"/zones\"\n : `/zones?provider=${encodeURIComponent(filter.provider)}`,\n success: Server.Zones,\n }),\n start: (input) =>\n request({\n method: \"POST\",\n path: \"/connections\",\n body: Schema.encodeSync(Server.StartPayload)(\n input.domain === undefined\n ? { provider: input.provider, method: input.method }\n : { domain: input.domain, provider: input.provider, method: input.method },\n ),\n success: Server.Started,\n }),\n reconnect: (input) =>\n request({\n method: \"POST\",\n path: `/connections/${encodeURIComponent(input.connectionId)}/reconnections`,\n body: Schema.encodeSync(Server.ReconnectPayload)({ method: input.method }),\n success: Server.Started,\n }),\n attach: (input) =>\n request({\n method: \"POST\",\n path: `/connections/${encodeURIComponent(input.connectionId)}/attachments`,\n body: Schema.encodeSync(Server.AttachPayload)(\n input.zone === undefined\n ? { domain: input.domain }\n : { domain: input.domain, zone: input.zone },\n ),\n success: Server.Started,\n }),\n detach: (attachmentId) =>\n request({\n method: \"DELETE\",\n path: `/attachments/${encodeURIComponent(attachmentId)}`,\n success: null,\n }),\n disconnect: (connectionId) =>\n request({\n method: \"DELETE\",\n path: `/connections/${encodeURIComponent(connectionId)}`,\n success: null,\n }),\n };\n\n const provisioning: ProvisioningGroup = {\n plan: (input) =>\n request({\n method: \"POST\",\n path: `/domains/${encodeURIComponent(input.domain)}/plans`,\n body: Schema.encodeSync(Server.PlanPayload)({ requirements: input.requirements }),\n success: Plan.Model,\n }),\n approve,\n reject,\n apply,\n attempt: (planId) =>\n request({\n method: \"GET\",\n path: `/plans/${encodeURIComponent(planId)}`,\n success: Server.Attempt,\n }),\n receipt: (receiptId) =>\n request({\n method: \"GET\",\n path: `/receipts/${encodeURIComponent(receiptId)}`,\n success: Receipt.Model,\n }),\n };\n\n const verification: VerificationGroup = {\n observe: (domain, input) =>\n request({\n method: \"POST\",\n path: `/domains/${encodeURIComponent(domain)}/observations`,\n body: Schema.encodeSync(Server.ObservePayload)(\n input?.requirements === undefined ? {} : { requirements: input.requirements },\n ),\n success: Server.Readiness,\n }),\n };\n\n const cleanup: CleanupGroup = {\n plan: (receiptId) =>\n request({\n method: \"POST\",\n path: `/receipts/${encodeURIComponent(receiptId)}/cleanup-plans`,\n success: Plan.Model,\n }),\n approve,\n reject,\n apply,\n };\n\n const groups = { connection, provisioning, verification, cleanup };\n return Object.fromEntries(\n allCapabilities\n .filter((capability) => declared.includes(capability))\n .map((capability) => [capability, groups[capability]]),\n );\n};\n\n// ---------------------------------------------------------------------------------------------\n// Promise adapters\n// ---------------------------------------------------------------------------------------------\n\n/** The same transport in Promises, for hosts and tests that do not run Effect. */\nexport type AsyncTransport = {\n readonly [K in keyof Interface]?: {\n readonly [M in keyof NonNullable<Interface[K]>]: NonNullable<Interface[K]>[M] extends (\n ...args: infer A\n ) => Effect.Effect<infer R, Errors.DomainKitError>\n ? (...args: A) => Promise<R>\n : never;\n };\n};\n\n/** Adapt a Promise-shaped transport; a rejection that is not a `DomainKit.Error` becomes one. */\nexport const fromAsync = (transport: AsyncTransport): Interface =>\n mapGroups(\n transport,\n (method) =>\n (...args: ReadonlyArray<never>) =>\n Effect.tryPromise({\n try: () => method(...args) as Promise<unknown>,\n catch: (cause) =>\n Errors.isDomainKitError(cause)\n ? cause\n : new Errors.DomainKitError({\n reason: new Reason.ProviderUnavailable({\n provider: \"domainkit\",\n message: `The transport rejected: ${String(cause)}`,\n }),\n }),\n }),\n ) as Interface;\n\n/** Adapt to Promises; every rejection is the `DomainKit.Error` the lifecycle raised. */\nexport const toAsync = (transport: Interface): AsyncTransport =>\n mapGroups(\n transport,\n (method) =>\n (...args: ReadonlyArray<never>) =>\n Effect.runPromise(method(...args) as Effect.Effect<unknown, Errors.DomainKitError>),\n ) as AsyncTransport;\n\ntype AnyMethod = (...args: ReadonlyArray<never>) => unknown;\ntype AnyGroups = { readonly [key: string]: Record<string, AnyMethod> | undefined };\n\n/** Rebuild every declared group with each method wrapped; the two adapters differ only in `wrap`. */\nconst mapGroups = (source: object, wrap: (method: AnyMethod) => AnyMethod): unknown =>\n Object.fromEntries(\n allCapabilities.flatMap((capability) => {\n const group = (source as AnyGroups)[capability];\n if (group === undefined) return [];\n return [\n [\n capability,\n Object.fromEntries(Object.entries(group).map(([name, method]) => [name, wrap(method)])),\n ],\n ];\n }),\n );\n\n// ---------------------------------------------------------------------------------------------\n// Internals\n// ---------------------------------------------------------------------------------------------\n\nconst safeOrigin = (base: string): string => {\n try {\n return new URL(base).origin;\n } catch {\n return base === \"\" ? \"domainkit\" : base;\n }\n};\n"],"mappings":"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"}
@@ -54,6 +54,7 @@ const connect = (principal, id) => Effect.gen(function* () {
54
54
  connectionId: connection.id,
55
55
  domain: `app.${id}.example.com`,
56
56
  zone: `${id}.example.com`,
57
+ label: `${id}.example.com (${id})`,
57
58
  target: { zoneId: `zone-${id}` }
58
59
  })
59
60
  };
@@ -337,6 +338,20 @@ const cases = (layer) => {
337
338
  yield* expect(again === 0, "recovery must be idempotent");
338
339
  }).pipe(Effect.provideService(Service, owner)))
339
340
  },
341
+ {
342
+ name: "returns an attachment's label from every read",
343
+ run: run(Effect.gen(function* () {
344
+ const { storage, connection, attachment } = yield* connect(owner, "auth-label");
345
+ const label = `${attachment.zone} (auth-label)`;
346
+ yield* expect(attachment.label === label, "create must return the label it was given");
347
+ const read = yield* storage.attachments.get(attachment.id);
348
+ yield* expect(read.label === label, "get must return the stored label");
349
+ const byDomain = yield* storage.attachments.byDomain(attachment.domain);
350
+ yield* expect(Option.isSome(byDomain) && byDomain.value.label === label, "byDomain must return the stored label");
351
+ const listed = yield* storage.attachments.list(connection.id);
352
+ yield* expect(listed.every((row) => row.label === label), "list must return the stored label");
353
+ }).pipe(Effect.provideService(Service, owner)))
354
+ },
340
355
  {
341
356
  name: "blocks connection removal while attachments exist",
342
357
  run: run(Effect.gen(function* () {
@@ -346,6 +361,7 @@ const cases = (layer) => {
346
361
  connectionId: connection.id,
347
362
  domain: attachment.domain,
348
363
  zone: attachment.zone,
364
+ label: attachment.label,
349
365
  target: {}
350
366
  }), "InvalidInput", "duplicate domain attachment");
351
367
  yield* storage.attachments.remove(attachment.id);