domainkit 0.14.1 → 0.16.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 /** 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\n/**\n * Who a continuation belongs to, without the continuation itself. `payload` has no place here: it\n * carries the PKCE `codeVerifier`, which only the owner-scoped read may reach.\n */\nexport class ContinuationHeader extends Schema.Class<ContinuationHeader>(\n \"@domainkit/Storage/ContinuationHeader\",\n)({\n ownerId: Schema.String,\n actorId: Schema.String,\n provider: 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 /**\n * The flow's owner, actor, provider, and expiry, without a principal and without spending the\n * row. The one read in this interface that is not tenant-scoped, because `domainkit/server`\n * has to learn whose flow a provider callback finishes before it can resolve the principal to\n * scope by; the callback carries a continuation id and a session cookie and nothing else.\n *\n * A missing row fails `NotFound` and a stale one fails `Expired`, exactly as `get` does, so an\n * expired id never reaches a host. `payload` is out of reach here by construction.\n */\n readonly header: (id: string) => Effect.Effect<ContinuationHeader, Errors.DomainKitError>;\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 /** Takes no principal: see `Interface.continuations.header`. Never returns `payload`. */\n readonly header: (id: string) => Promise<ContinuationHeader>;\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 header: (id) =>\n Effect.tryPromise({\n try: () => service.continuations.header(id),\n catch: storageFailed(\"continuations.header\"),\n }),\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.Digest","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 /**\n * What the provider called the account when the credential was issued, so a UI names the\n * connection without a provider call. `null` when the provider names no single account.\n */\n label: Schema.NullOr(Schema.String),\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\n/**\n * Who a continuation belongs to, without the continuation itself. `payload` has no place here: it\n * carries the PKCE `codeVerifier`, which only the owner-scoped read may reach.\n */\nexport class ContinuationHeader extends Schema.Class<ContinuationHeader>(\n \"@domainkit/Storage/ContinuationHeader\",\n)({\n ownerId: Schema.String,\n actorId: Schema.String,\n provider: 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 BatchId = Schema.String.pipe(Schema.brand(\"@domainkit/BatchId\"));\nexport type BatchId = typeof BatchId.Type;\n\n/**\n * Where a batch stands, recomputed from its items' attempts after every transition.\n *\n * `planning` while any item is still without a plan, `planned` once every item carries one,\n * `approved` once the principal bound the batch digest, then the apply states. `partial` and\n * `failed` are resumable; `complete` and `rejected` are terminal and leave the unfinished index.\n */\nexport const BatchStatus = Schema.Literals([\n \"planning\",\n \"planned\",\n \"approved\",\n \"applying\",\n \"complete\",\n \"partial\",\n \"failed\",\n \"rejected\",\n]);\nexport type BatchStatus = typeof BatchStatus.Type;\n\n/** Who bound the batch to its digest, and when. The digest itself is `Batch.digest`. */\nexport const BatchApproval = Schema.Struct({\n actorId: Schema.String,\n at: Schema.DateTimeUtcFromString,\n});\nexport type BatchApproval = typeof BatchApproval.Type;\n\n/**\n * Many domains planned together, approved once, and applied with bounded concurrency.\n *\n * The batch owns the aggregate's own state only: its digest, who approved or declined it, and the\n * status recomputed from its items. Every plan, approval, receipt, lease, and failure stays on the\n * attempt an item points at, so nothing is stored twice.\n */\nexport class Batch extends Schema.Class<Batch>(\"@domainkit/Storage/Batch\")({\n id: BatchId,\n ownerId: Schema.String,\n kind: Plan.Kind,\n status: BatchStatus,\n /** SHA-256 over the items' `attachmentId:planDigest` pairs, bound at approval. */\n digest: Schema.NullOr(Plan.Digest),\n approval: Schema.NullOr(BatchApproval),\n rejection: Schema.NullOr(Rejection),\n /** Unique per owner: a replayed create returns the batch this key already made. */\n idempotencyKey: Schema.String,\n createdBy: Schema.String,\n createdAt: Schema.DateTimeUtcFromString,\n updatedAt: Schema.DateTimeUtcFromString,\n /** When the batch reached `complete` or `rejected`; null while it still owes a move. */\n completedAt: Schema.NullOr(Schema.DateTimeUtcFromString),\n}) {}\n\n/**\n * One domain's place in a batch: a pointer to the attachment and to the attempt carrying its plan.\n *\n * `attemptId` is null until the item is planned, and `planFailure` carries why the last planning\n * pass stopped, cleared the moment a plan lands.\n */\nexport class BatchItem extends Schema.Class<BatchItem>(\"@domainkit/Storage/BatchItem\")({\n batchId: BatchId,\n attachmentId: Schema.String,\n /** The item's place in the batch, as `create` received it. */\n position: Schema.Number,\n attemptId: Schema.NullOr(Plan.PlanId),\n planFailure: Schema.NullOr(Schema.String),\n}) {}\n\n/**\n * The status a batch holds, given whether it was approved or declined and where each item's\n * attempt stands. `null` names an item that has no plan yet.\n *\n * Both `Storage` implementations recompute the stored status through this after every transition,\n * so the owner-scoped unfinished index never disagrees with the attempts it summarizes.\n */\nexport const batchStatusOf = (input: {\n readonly approved: boolean;\n readonly rejected: boolean;\n readonly items: ReadonlyArray<AttemptStatus | null>;\n}): BatchStatus => {\n if (input.rejected) return \"rejected\";\n const items = input.items;\n if (items.length === 0 || items.some((status) => status === null)) return \"planning\";\n const statuses = items as ReadonlyArray<AttemptStatus>;\n const every = (status: AttemptStatus) => statuses.every((item) => item === status);\n if (every(\"planned\")) return \"planned\";\n // An approval the batch never took means an item moved on its own; it is not approvable, and\n // planning is the state a host resumes from.\n if (!input.approved) return \"planning\";\n if (every(\"approved\")) return \"approved\";\n const settled = statuses.every(\n (status) => status !== \"planned\" && status !== \"approved\" && status !== \"applying\",\n );\n if (!settled) return \"applying\";\n if (every(\"complete\")) return \"complete\";\n return statuses.some((status) => status === \"partial\") ? \"partial\" : \"failed\";\n};\n\n/** A batch with its items, in `position` order. Every batch read returns one. */\nexport interface BatchAggregate {\n readonly batch: Batch;\n readonly items: ReadonlyArray<BatchItem>;\n}\n\n/** One item's attempt approval, as `batches.approve` writes it beside the batch's own. */\nexport interface BatchItemApproval {\n readonly attachmentId: string;\n readonly approval: Approval.Model;\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 /**\n * The flow's owner, actor, provider, and expiry, without a principal and without spending the\n * row. The one read in this interface that is not tenant-scoped, because `domainkit/server`\n * has to learn whose flow a provider callback finishes before it can resolve the principal to\n * scope by; the callback carries a continuation id and a session cookie and nothing else.\n *\n * A missing row fails `NotFound` and a stale one fails `Expired`, exactly as `get` does, so an\n * expired id never reaches a host. `payload` is out of reach here by construction.\n */\n readonly header: (id: string) => Effect.Effect<ContinuationHeader, Errors.DomainKitError>;\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 /**\n * The batch aggregate: many attempts planned together and approved once.\n *\n * Every transition runs in one transaction over the locked batch row and leaves `status`\n * recomputed from the items' attempts, so the owner-scoped unfinished index never disagrees\n * with the attempts it summarizes.\n */\n readonly batches: {\n /**\n * Insert the batch and one item per attachment, or return the batch this owner already\n * created under `idempotencyKey`. A replay ignores `attachmentIds` and returns what is\n * stored, which is what makes a retried create safe.\n */\n readonly create: (input: {\n readonly kind: Plan.Kind;\n readonly idempotencyKey: string;\n /** One item per attachment, in order; empty or duplicated fails `InvalidInput`. */\n readonly attachmentIds: ReadonlyArray<string>;\n }) => Fx<BatchAggregate>;\n readonly get: (id: BatchId) => Fx<BatchAggregate>;\n /**\n * The batch this owner created under `idempotencyKey`, if any.\n *\n * What a caller reads before it validates a create's payload, so a retried request answers\n * with the batch the first one made whatever the retry now says.\n */\n readonly byIdempotencyKey: (key: string) => Fx<Option.Option<BatchAggregate>>;\n /** Every batch that has not reached `complete` or `rejected`, most recently touched first. */\n readonly listUnfinished: () => Fx<ReadonlyArray<BatchAggregate>>;\n /**\n * Point an item at the attempt that now carries its plan and clear its plan failure.\n *\n * Fails `Stale` unless the batch is still `planning` or `planned`. That check is the fence a\n * planner still in flight hits when the principal declines the batch underneath it, so plan\n * state never lands on a rejected aggregate.\n */\n readonly recordItemPlan: (\n id: BatchId,\n attachmentId: string,\n attemptId: Plan.PlanId,\n ) => Fx<BatchAggregate>;\n /** Record why an item could not be planned; the batch stays `planning`. Same fence. */\n readonly recordItemPlanFailure: (\n id: BatchId,\n attachmentId: string,\n message: string,\n ) => Fx<BatchAggregate>;\n /**\n * Bind the batch to `digest` and approve every item's attempt in the same transaction, so a\n * batch is never approved without the per-attempt approvals `attempts.apply` needs.\n *\n * Fails `Stale` unless the batch is `planned` and every item still points at the attempt its\n * approval names. Approving an approved batch under the same digest returns it unchanged; a\n * different digest fails `Stale`.\n */\n readonly approve: (\n id: BatchId,\n input: {\n readonly digest: Plan.Digest;\n readonly actorId: string;\n /** One per item, covering every item in the batch. */\n readonly approvals: ReadonlyArray<BatchItemApproval>;\n },\n ) => Fx<BatchAggregate>;\n /**\n * Decline the batch and every planned attempt under it, in one transaction. Terminal.\n *\n * Rejecting again returns the batch unchanged; a batch that reached `approved` or later\n * fails `Stale`.\n */\n readonly reject: (\n id: BatchId,\n input: { readonly actorId: string; readonly reason: string | null },\n ) => Fx<BatchAggregate>;\n /** Recompute the stored status from the items' attempts, after applying moved them. */\n readonly refresh: (id: BatchId) => Fx<BatchAggregate>;\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 /** Takes no principal: see `Interface.continuations.header`. Never returns `payload`. */\n readonly header: (id: string) => Promise<ContinuationHeader>;\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 batches: {\n readonly create: (\n principal: Principal.Interface,\n input: {\n readonly kind: Plan.Kind;\n readonly idempotencyKey: string;\n readonly attachmentIds: ReadonlyArray<string>;\n },\n ) => Promise<BatchAggregate>;\n readonly get: (principal: Principal.Interface, id: BatchId) => Promise<BatchAggregate>;\n readonly byIdempotencyKey: (\n principal: Principal.Interface,\n key: string,\n ) => Promise<BatchAggregate | null>;\n readonly listUnfinished: (\n principal: Principal.Interface,\n ) => Promise<ReadonlyArray<BatchAggregate>>;\n readonly recordItemPlan: (\n principal: Principal.Interface,\n id: BatchId,\n attachmentId: string,\n attemptId: Plan.PlanId,\n ) => Promise<BatchAggregate>;\n readonly recordItemPlanFailure: (\n principal: Principal.Interface,\n id: BatchId,\n attachmentId: string,\n message: string,\n ) => Promise<BatchAggregate>;\n readonly approve: (\n principal: Principal.Interface,\n id: BatchId,\n input: {\n readonly digest: Plan.Digest;\n readonly actorId: string;\n readonly approvals: ReadonlyArray<BatchItemApproval>;\n },\n ) => Promise<BatchAggregate>;\n readonly reject: (\n principal: Principal.Interface,\n id: BatchId,\n input: { readonly actorId: string; readonly reason: string | null },\n ) => Promise<BatchAggregate>;\n readonly refresh: (principal: Principal.Interface, id: BatchId) => Promise<BatchAggregate>;\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 header: (id) =>\n Effect.tryPromise({\n try: () => service.continuations.header(id),\n catch: storageFailed(\"continuations.header\"),\n }),\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 batches: {\n create: (input) => call(\"batches.create\", (p) => service.batches.create(p, input)),\n get: (id) => call(\"batches.get\", (p) => service.batches.get(p, id)),\n byIdempotencyKey: (key) =>\n option(\"batches.byIdempotencyKey\", (p) => service.batches.byIdempotencyKey(p, key)),\n listUnfinished: () =>\n call(\"batches.listUnfinished\", (p) => service.batches.listUnfinished(p)),\n recordItemPlan: (id, attachmentId, attemptId) =>\n call(\"batches.recordItemPlan\", (p) =>\n service.batches.recordItemPlan(p, id, attachmentId, attemptId),\n ),\n recordItemPlanFailure: (id, attachmentId, message) =>\n call(\"batches.recordItemPlanFailure\", (p) =>\n service.batches.recordItemPlanFailure(p, id, attachmentId, message),\n ),\n approve: (id, input) => call(\"batches.approve\", (p) => service.batches.approve(p, id, input)),\n reject: (id, input) => call(\"batches.reject\", (p) => service.batches.reject(p, id, input)),\n refresh: (id) => call(\"batches.refresh\", (p) => service.batches.refresh(p, id)),\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
@@ -74,6 +74,7 @@ const provider = (options = {}) => {
74
74
  return {
75
75
  secret: Redacted.make(secret),
76
76
  context: { account: id },
77
+ label: options.accountLabel ?? null,
77
78
  expiresAt: null
78
79
  };
79
80
  };
@@ -123,6 +124,7 @@ const provider = (options = {}) => {
123
124
  authenticate: ({ token }) => Redacted.value(token).length === 0 ? fail(new Unauthenticated({ message: "fake provider rejected an empty token" })) : Effect.succeed({
124
125
  secret: token,
125
126
  context: { account: id },
127
+ label: options.accountLabel ?? null,
126
128
  expiresAt: null
127
129
  })
128
130
  }),
@@ -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 /** 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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"}
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 /** What the fake calls the account it issues credentials for. Default: none, so the label is `null`. */\n readonly accountLabel?: 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 {\n secret: Redacted.make(secret),\n context: { account: id },\n label: options.accountLabel ?? null,\n expiresAt: null,\n };\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({\n secret: token,\n context: { account: id },\n label: options.accountLabel ?? null,\n expiresAt: null,\n }),\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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"}
@@ -81,9 +81,13 @@ const make = (storage, connect, kind) => {
81
81
  if (typeof plan !== "string" && plan.digest !== attempt.plan.digest) return yield* stale(plan);
82
82
  return attempt;
83
83
  });
84
- const approve = (plan, options = {}) => Effect.gen(function* () {
85
- const attempt = yield* attemptOf(plan);
86
- if (attempt.approval !== null) return attempt.approval;
84
+ /**
85
+ * The approval a planned attempt would record, built and checked but not written.
86
+ *
87
+ * Split out so an aggregate can collect one approval per attempt and write them all in a single
88
+ * transaction beside its own.
89
+ */
90
+ const draftApproval = (attempt, options = {}) => Effect.gen(function* () {
87
91
  const principal = yield* Service;
88
92
  const now = yield* DateTime.now;
89
93
  if (past(attempt.plan.expiresAt, now)) return yield* expired("plan", attempt.plan.id);
@@ -106,7 +110,7 @@ const make = (storage, connect, kind) => {
106
110
  field: "operationIds"
107
111
  }));
108
112
  }
109
- const approval = new Model$1({
113
+ return new Model$1({
110
114
  id: ApprovalId.make(yield* fresh("apr")),
111
115
  version: "domainkit.approval.v2",
112
116
  kind,
@@ -117,6 +121,11 @@ const make = (storage, connect, kind) => {
117
121
  approvedAt: now,
118
122
  expiresAt: attempt.plan.expiresAt
119
123
  });
124
+ });
125
+ const approve = (plan, options = {}) => Effect.gen(function* () {
126
+ const attempt = yield* attemptOf(plan);
127
+ if (attempt.approval !== null) return attempt.approval;
128
+ const approval = yield* draftApproval(attempt, options);
120
129
  return (yield* storage.attempts.approve(attempt.id, approval)).approval ?? approval;
121
130
  });
122
131
  /** Decline a planned attempt on behalf of the principal's actor. */
@@ -253,6 +262,7 @@ const make = (storage, connect, kind) => {
253
262
  return {
254
263
  record,
255
264
  approve,
265
+ draftApproval,
256
266
  reject,
257
267
  apply,
258
268
  attemptOf
@@ -1 +1 @@
1
- {"version":3,"file":"attempts.mjs","names":["Errors.fail","Reason.Stale","Reason.Expired","DomainName.isWithin","Reason.InvalidInput","Principal.Service","Plan.Model","Planner.digest","Storage.Attempt","Reason.NotFound","writes","Plan.writes","conflicts","Plan.conflicts","Reason.Conflict","Approval.Model","Receipt.Model","Receipt.Skipped","Receipt.Applied","Receipt.Failed","Planner.reconcile","Planner.requirements","Plan.Conflict","DnsRecord.equals","Plan.Delete"],"sources":["../../src/internal/attempts.ts"],"sourcesContent":["import { DateTime, Duration, Effect } from \"effect\";\n\nimport * as Approval from \"../Approval.ts\";\nimport * as Connect from \"../Connect.ts\";\nimport * as DnsRecord from \"../DnsRecord.ts\";\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport * as DomainName from \"../DomainName.ts\";\nimport * as Plan from \"../Plan.ts\";\nimport * as Principal from \"../Principal.ts\";\nimport type * as Provider from \"../Provider.ts\";\nimport * as Receipt from \"../Receipt.ts\";\nimport * as Storage from \"../Storage.ts\";\nimport { fresh } from \"./ids.ts\";\nimport * as Planner from \"./planner.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface ApproveOptions {\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n readonly allowPartial?: boolean;\n}\n\nexport interface PolicyShape {\n readonly planTtlMs: number;\n readonly applyLeaseMs: number;\n}\n\nconst stale = (plan: Plan.Model) =>\n Errors.fail(new Reason.Stale({ planId: plan.id, digest: plan.digest }));\n\nconst expired = (entity: \"plan\" | \"approval\", id: string) =>\n Errors.fail(new Reason.Expired({ entity, id }));\n\nconst past = (at: DateTime.Utc, now: DateTime.Utc) =>\n DateTime.toEpochMillis(at) <= DateTime.toEpochMillis(now);\n\n/** Every requirement must sit at or below the attached domain; other tenants share the zone. */\nexport const assertWithin = (\n attachment: Storage.Attachment,\n records: ReadonlyArray<DnsRecord.Model>,\n): Effect.Effect<void, Errors.DomainKitError> => {\n const outside = records.filter((record) => !DomainName.isWithin(record.name, attachment.domain));\n return outside.length === 0\n ? Effect.void\n : Errors.fail(\n new Reason.InvalidInput({\n message: `${outside.map(({ name }) => name).join(\", \")} is outside the attached domain ${attachment.domain}`,\n field: \"requirements\",\n }),\n );\n};\n\n/** Shared plan -> approve -> apply machinery for provisioning and cleanup attempts. */\nexport const make = (storage: Storage.Interface, connect: Connect.Interface, kind: Plan.Kind) => {\n const providerOf = (attachment: Storage.Attachment) =>\n Effect.gen(function* () {\n const connection = yield* storage.connections.get(attachment.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n return authorization.provider;\n });\n\n /** Persist a freshly built plan as a `planned` attempt. */\n const record = (input: {\n readonly attachment: Storage.Attachment;\n readonly target: Provider.Target;\n readonly operations: ReadonlyArray<Plan.Operation>;\n readonly ttlMs: number;\n readonly sourceReceiptId: Receipt.ReceiptId | null;\n }): Fx<Plan.Model> =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const provider = yield* providerOf(input.attachment);\n const now = yield* DateTime.now;\n const unsigned: Planner.Unsigned = {\n version: \"domainkit.plan.v2\",\n kind,\n domain: input.attachment.domain,\n zone: input.target.zone,\n provider,\n attachmentId: input.attachment.id,\n operations: input.operations,\n };\n const plan = new Plan.Model({\n ...unsigned,\n id: Plan.PlanId.make(yield* fresh(\"plan\")),\n digest: yield* Planner.digest(unsigned),\n createdAt: now,\n expiresAt: DateTime.addDuration(now, Duration.millis(input.ttlMs)),\n });\n yield* storage.attempts.create(\n new Storage.Attempt({\n id: plan.id,\n ownerId: principal.ownerId,\n attachmentId: input.attachment.id,\n kind,\n status: \"planned\",\n plan,\n approval: null,\n receipt: null,\n rejection: null,\n sourceReceiptId: input.sourceReceiptId,\n leaseExpiresAt: null,\n failure: null,\n updatedAt: now,\n }),\n );\n return plan;\n });\n\n const attemptOf = (plan: Plan.Model | Plan.PlanId): Fx<Storage.Attempt> =>\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.get(typeof plan === \"string\" ? plan : plan.id);\n if (attempt.kind !== kind) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"plan\", id: attempt.id }));\n }\n if (typeof plan !== \"string\" && plan.digest !== attempt.plan.digest)\n return yield* stale(plan);\n return attempt;\n });\n\n const approve = (\n plan: Plan.Model | Plan.PlanId,\n options: ApproveOptions = {},\n ): Fx<Approval.Model> =>\n Effect.gen(function* () {\n const attempt = yield* attemptOf(plan);\n if (attempt.approval !== null) return attempt.approval;\n const principal = yield* Principal.Service;\n const now = yield* DateTime.now;\n if (past(attempt.plan.expiresAt, now)) return yield* expired(\"plan\", attempt.plan.id);\n const writes = Plan.writes(attempt.plan);\n const writeIds = new Set(writes.map(({ id }) => id));\n const selected = [...new Set(options.operationIds ?? writes.map(({ id }) => id))].sort();\n const unknown = selected.filter((id) => !writeIds.has(id));\n if (unknown.length > 0) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: `Operations ${unknown.join(\", \")} are not writable in plan ${attempt.plan.id}`,\n field: \"operationIds\",\n }),\n );\n }\n const conflicts = Plan.conflicts(attempt.plan);\n if (options.allowPartial !== true) {\n if (conflicts.length > 0) {\n return yield* Errors.fail(\n new Reason.Conflict({ planId: attempt.plan.id, operations: conflicts }),\n );\n }\n if (selected.length !== writes.length) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: \"Approving a subset of the plan requires allowPartial\",\n field: \"operationIds\",\n }),\n );\n }\n }\n const approval = new Approval.Model({\n id: Approval.ApprovalId.make(yield* fresh(\"apr\")),\n version: \"domainkit.approval.v2\",\n kind,\n planId: attempt.plan.id,\n digest: attempt.plan.digest,\n operationIds: selected,\n approvedBy: principal.actorId,\n approvedAt: now,\n expiresAt: attempt.plan.expiresAt,\n });\n const stored = yield* storage.attempts.approve(attempt.id, approval);\n return stored.approval ?? approval;\n });\n\n /** Decline a planned attempt on behalf of the principal's actor. */\n const reject = (\n plan: Plan.Model | Plan.PlanId,\n options: { readonly reason?: string } = {},\n ): Fx<Storage.Attempt> =>\n Effect.gen(function* () {\n const attempt = yield* attemptOf(plan);\n const principal = yield* Principal.Service;\n return yield* storage.attempts.reject(attempt.id, {\n digest: attempt.plan.digest,\n actorId: principal.actorId,\n reason: options.reason ?? null,\n });\n });\n\n const apply = (\n approval: Approval.Model | Approval.ApprovalId,\n policy: PolicyShape,\n ): Fx<Receipt.Model> =>\n Effect.gen(function* () {\n const attempt =\n typeof approval === \"string\"\n ? yield* storage.attempts.byApproval(approval)\n : yield* attemptOf(approval.planId);\n if (attempt.kind !== kind) {\n return yield* Errors.fail(\n new Reason.NotFound({\n entity: \"approval\",\n id: typeof approval === \"string\" ? approval : approval.id,\n }),\n );\n }\n const stored = attempt.approval;\n if (stored === null || (typeof approval !== \"string\" && stored.id !== approval.id)) {\n return yield* Errors.fail(\n new Reason.NotFound({\n entity: \"approval\",\n id: typeof approval === \"string\" ? approval : approval.id,\n }),\n );\n }\n if (attempt.receipt !== null) return attempt.receipt;\n const now = yield* DateTime.now;\n if (past(stored.expiresAt, now)) return yield* expired(\"approval\", stored.id);\n const claimed = yield* storage.attempts.claim(\n attempt.id,\n DateTime.addDuration(now, Duration.millis(policy.applyLeaseMs)),\n );\n return yield* run(claimed, stored).pipe(\n Effect.tapError((error) =>\n storage.attempts.fail(claimed.id, error.message).pipe(Effect.ignore),\n ),\n );\n });\n\n /** Writes under a claimed lease; a failure before any write releases the attempt as `failed`. */\n const run = (attempt: Storage.Attempt, approval: Approval.Model): Fx<Receipt.Model> =>\n Effect.gen(function* () {\n const plan = attempt.plan;\n const attachment = yield* storage.attachments.get(attempt.attachmentId);\n const { session, target } = yield* connect.session(attachment.id);\n const dns = session.dns(target);\n yield* revalidate(plan, dns, target.zone);\n const approved = new Set(approval.operationIds);\n const outcomes: Array<Receipt.Outcome> = [];\n const applied: Array<Receipt.Applied> = [];\n let stopped = false;\n const finish = (status: \"complete\" | \"partial\") =>\n Effect.gen(function* () {\n const receipt = new Receipt.Model({\n id: Receipt.ReceiptId.make(yield* fresh(\"rcpt\")),\n version: \"domainkit.receipt.v2\",\n kind,\n planId: plan.id,\n approvalId: approval.id,\n digest: plan.digest,\n provider: plan.provider,\n zone: plan.zone,\n status,\n outcomes: [...outcomes],\n appliedAt: yield* DateTime.now,\n });\n yield* storage.attempts.complete(attempt.id, receipt);\n return receipt;\n });\n const writes = Effect.gen(function* () {\n for (const operation of plan.operations) {\n if (operation._tag === \"Noop\") {\n outcomes.push(new Receipt.Skipped({ operationId: operation.id, reason: \"noop\" }));\n continue;\n }\n if (operation._tag === \"Conflict\" || !approved.has(operation.id)) {\n outcomes.push(\n new Receipt.Skipped({ operationId: operation.id, reason: \"not-approved\" }),\n );\n continue;\n }\n if (stopped) {\n outcomes.push(\n new Receipt.Skipped({ operationId: operation.id, reason: \"not-attempted\" }),\n );\n continue;\n }\n const result = yield* write(plan, operation, dns, target.zone).pipe(Effect.result);\n if (result._tag === \"Success\") {\n const outcome = new Receipt.Applied({\n operationId: operation.id,\n providerRecordId: result.success,\n });\n outcomes.push(outcome);\n applied.push(outcome);\n continue;\n }\n if (applied.length === 0) return yield* Effect.fail(result.failure);\n stopped = true;\n outcomes.push(\n new Receipt.Failed({ operationId: operation.id, message: result.failure.message }),\n );\n }\n return yield* finish(stopped ? \"partial\" : \"complete\");\n });\n return yield* writes.pipe(\n Effect.onInterrupt(() =>\n applied.length === 0\n ? Effect.void\n : Effect.gen(function* () {\n for (const operation of plan.operations) {\n if (outcomes.some((outcome) => outcome.operationId === operation.id)) continue;\n outcomes.push(\n new Receipt.Skipped({ operationId: operation.id, reason: \"not-attempted\" }),\n );\n }\n yield* finish(\"partial\");\n }).pipe(Effect.ignore),\n ),\n );\n });\n\n /** Re-plan the zone and fail `Stale` when the digest moved since the plan was built. */\n const revalidate = (plan: Plan.Model, dns: Provider.Dns, zone: string) =>\n Effect.gen(function* () {\n const current = yield* currentOperations(plan, dns, zone);\n const digest = yield* Planner.digest({\n version: plan.version,\n kind: plan.kind,\n domain: plan.domain,\n zone: plan.zone,\n provider: plan.provider,\n attachmentId: plan.attachmentId,\n operations: current,\n });\n if (digest !== plan.digest) return yield* stale(plan);\n });\n\n const currentOperations = (\n plan: Plan.Model,\n dns: Provider.Dns,\n zone: string,\n ): Effect.Effect<ReadonlyArray<Plan.Operation>, Errors.DomainKitError> =>\n kind === \"provisioning\"\n ? Effect.flatMap(dns.list(zone), (observed) =>\n Planner.reconcile(\n Planner.requirements(plan),\n observed.map(({ record: observedRecord }) => observedRecord),\n ),\n )\n : Effect.forEach(\n plan.operations,\n (operation): Effect.Effect<Plan.Operation, Errors.DomainKitError> =>\n operation._tag === \"Delete\"\n ? cleanupOperation(\n dns,\n zone,\n operation.id,\n operation.record,\n operation.providerRecordId,\n )\n : Effect.succeed(operation),\n );\n\n const write = (\n plan: Plan.Model,\n operation: Plan.Create | Plan.Delete,\n dns: Provider.Dns,\n zone: string,\n ): Effect.Effect<string, Errors.DomainKitError> =>\n operation._tag === \"Create\"\n ? Effect.gen(function* () {\n const observed = yield* dns.list(zone);\n const [check] = yield* Planner.reconcile(\n [operation.record],\n observed.map(({ record: observedRecord }) => observedRecord),\n );\n if (check?._tag !== \"Create\") return yield* stale(plan);\n const created = yield* dns.create(zone, operation.record);\n return created.providerRecordId;\n })\n : Effect.gen(function* () {\n const check = yield* cleanupOperation(\n dns,\n zone,\n operation.id,\n operation.record,\n operation.providerRecordId,\n );\n if (check._tag !== \"Delete\") return yield* stale(plan);\n yield* dns.delete(zone, operation.providerRecordId);\n return operation.providerRecordId;\n });\n\n return { record, approve, reject, apply, attemptOf };\n};\n\n/** Read one receipt-proven record back; anything but an exact match is a conflict. */\nexport const cleanupOperation = (\n dns: Provider.Dns,\n zone: string,\n id: Plan.OperationId,\n record: DnsRecord.Model,\n providerRecordId: string,\n): Effect.Effect<Plan.Delete | Plan.Conflict, Errors.DomainKitError> =>\n Effect.map(dns.get(zone, providerRecordId), (observed) => {\n if (observed === null)\n return new Plan.Conflict({ id, record, existing: [], reason: \"missing\" });\n if (!DnsRecord.equals(observed, record)) {\n return new Plan.Conflict({ id, record, existing: [observed], reason: \"value-mismatch\" });\n }\n return new Plan.Delete({ id, record, providerRecordId });\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"attempts.mjs","names":["Errors.fail","Reason.Stale","Reason.Expired","DomainName.isWithin","Reason.InvalidInput","Principal.Service","Plan.Model","Planner.digest","Storage.Attempt","Reason.NotFound","writes","Plan.writes","conflicts","Plan.conflicts","Reason.Conflict","Approval.Model","Receipt.Model","Receipt.Skipped","Receipt.Applied","Receipt.Failed","Planner.reconcile","Planner.requirements","Plan.Conflict","DnsRecord.equals","Plan.Delete"],"sources":["../../src/internal/attempts.ts"],"sourcesContent":["import { DateTime, Duration, Effect } from \"effect\";\n\nimport * as Approval from \"../Approval.ts\";\nimport * as Connect from \"../Connect.ts\";\nimport * as DnsRecord from \"../DnsRecord.ts\";\nimport * as Errors from \"./error.ts\";\nimport * as Reason from \"../Reason.ts\";\nimport * as DomainName from \"../DomainName.ts\";\nimport * as Plan from \"../Plan.ts\";\nimport * as Principal from \"../Principal.ts\";\nimport type * as Provider from \"../Provider.ts\";\nimport * as Receipt from \"../Receipt.ts\";\nimport * as Storage from \"../Storage.ts\";\nimport { fresh } from \"./ids.ts\";\nimport * as Planner from \"./planner.ts\";\n\ntype Fx<A> = Effect.Effect<A, Errors.DomainKitError, Principal.Service>;\n\nexport interface ApproveOptions {\n readonly operationIds?: ReadonlyArray<Plan.OperationId>;\n readonly allowPartial?: boolean;\n}\n\nexport interface PolicyShape {\n readonly planTtlMs: number;\n readonly applyLeaseMs: number;\n}\n\nconst stale = (plan: Plan.Model) =>\n Errors.fail(new Reason.Stale({ planId: plan.id, digest: plan.digest }));\n\nconst expired = (entity: \"plan\" | \"approval\", id: string) =>\n Errors.fail(new Reason.Expired({ entity, id }));\n\nconst past = (at: DateTime.Utc, now: DateTime.Utc) =>\n DateTime.toEpochMillis(at) <= DateTime.toEpochMillis(now);\n\n/** Every requirement must sit at or below the attached domain; other tenants share the zone. */\nexport const assertWithin = (\n attachment: Storage.Attachment,\n records: ReadonlyArray<DnsRecord.Model>,\n): Effect.Effect<void, Errors.DomainKitError> => {\n const outside = records.filter((record) => !DomainName.isWithin(record.name, attachment.domain));\n return outside.length === 0\n ? Effect.void\n : Errors.fail(\n new Reason.InvalidInput({\n message: `${outside.map(({ name }) => name).join(\", \")} is outside the attached domain ${attachment.domain}`,\n field: \"requirements\",\n }),\n );\n};\n\n/** Shared plan -> approve -> apply machinery for provisioning and cleanup attempts. */\nexport const make = (storage: Storage.Interface, connect: Connect.Interface, kind: Plan.Kind) => {\n const providerOf = (attachment: Storage.Attachment) =>\n Effect.gen(function* () {\n const connection = yield* storage.connections.get(attachment.connectionId);\n const authorization = yield* storage.authorizations.get(connection.authorizationId);\n return authorization.provider;\n });\n\n /** Persist a freshly built plan as a `planned` attempt. */\n const record = (input: {\n readonly attachment: Storage.Attachment;\n readonly target: Provider.Target;\n readonly operations: ReadonlyArray<Plan.Operation>;\n readonly ttlMs: number;\n readonly sourceReceiptId: Receipt.ReceiptId | null;\n }): Fx<Plan.Model> =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const provider = yield* providerOf(input.attachment);\n const now = yield* DateTime.now;\n const unsigned: Planner.Unsigned = {\n version: \"domainkit.plan.v2\",\n kind,\n domain: input.attachment.domain,\n zone: input.target.zone,\n provider,\n attachmentId: input.attachment.id,\n operations: input.operations,\n };\n const plan = new Plan.Model({\n ...unsigned,\n id: Plan.PlanId.make(yield* fresh(\"plan\")),\n digest: yield* Planner.digest(unsigned),\n createdAt: now,\n expiresAt: DateTime.addDuration(now, Duration.millis(input.ttlMs)),\n });\n yield* storage.attempts.create(\n new Storage.Attempt({\n id: plan.id,\n ownerId: principal.ownerId,\n attachmentId: input.attachment.id,\n kind,\n status: \"planned\",\n plan,\n approval: null,\n receipt: null,\n rejection: null,\n sourceReceiptId: input.sourceReceiptId,\n leaseExpiresAt: null,\n failure: null,\n updatedAt: now,\n }),\n );\n return plan;\n });\n\n const attemptOf = (plan: Plan.Model | Plan.PlanId): Fx<Storage.Attempt> =>\n Effect.gen(function* () {\n const attempt = yield* storage.attempts.get(typeof plan === \"string\" ? plan : plan.id);\n if (attempt.kind !== kind) {\n return yield* Errors.fail(new Reason.NotFound({ entity: \"plan\", id: attempt.id }));\n }\n if (typeof plan !== \"string\" && plan.digest !== attempt.plan.digest)\n return yield* stale(plan);\n return attempt;\n });\n\n /**\n * The approval a planned attempt would record, built and checked but not written.\n *\n * Split out so an aggregate can collect one approval per attempt and write them all in a single\n * transaction beside its own.\n */\n const draftApproval = (\n attempt: Storage.Attempt,\n options: ApproveOptions = {},\n ): Fx<Approval.Model> =>\n Effect.gen(function* () {\n const principal = yield* Principal.Service;\n const now = yield* DateTime.now;\n if (past(attempt.plan.expiresAt, now)) return yield* expired(\"plan\", attempt.plan.id);\n const writes = Plan.writes(attempt.plan);\n const writeIds = new Set(writes.map(({ id }) => id));\n const selected = [...new Set(options.operationIds ?? writes.map(({ id }) => id))].sort();\n const unknown = selected.filter((id) => !writeIds.has(id));\n if (unknown.length > 0) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: `Operations ${unknown.join(\", \")} are not writable in plan ${attempt.plan.id}`,\n field: \"operationIds\",\n }),\n );\n }\n const conflicts = Plan.conflicts(attempt.plan);\n if (options.allowPartial !== true) {\n if (conflicts.length > 0) {\n return yield* Errors.fail(\n new Reason.Conflict({ planId: attempt.plan.id, operations: conflicts }),\n );\n }\n if (selected.length !== writes.length) {\n return yield* Errors.fail(\n new Reason.InvalidInput({\n message: \"Approving a subset of the plan requires allowPartial\",\n field: \"operationIds\",\n }),\n );\n }\n }\n return new Approval.Model({\n id: Approval.ApprovalId.make(yield* fresh(\"apr\")),\n version: \"domainkit.approval.v2\",\n kind,\n planId: attempt.plan.id,\n digest: attempt.plan.digest,\n operationIds: selected,\n approvedBy: principal.actorId,\n approvedAt: now,\n expiresAt: attempt.plan.expiresAt,\n });\n });\n\n const approve = (\n plan: Plan.Model | Plan.PlanId,\n options: ApproveOptions = {},\n ): Fx<Approval.Model> =>\n Effect.gen(function* () {\n const attempt = yield* attemptOf(plan);\n if (attempt.approval !== null) return attempt.approval;\n const approval = yield* draftApproval(attempt, options);\n const stored = yield* storage.attempts.approve(attempt.id, approval);\n return stored.approval ?? approval;\n });\n\n /** Decline a planned attempt on behalf of the principal's actor. */\n const reject = (\n plan: Plan.Model | Plan.PlanId,\n options: { readonly reason?: string } = {},\n ): Fx<Storage.Attempt> =>\n Effect.gen(function* () {\n const attempt = yield* attemptOf(plan);\n const principal = yield* Principal.Service;\n return yield* storage.attempts.reject(attempt.id, {\n digest: attempt.plan.digest,\n actorId: principal.actorId,\n reason: options.reason ?? null,\n });\n });\n\n const apply = (\n approval: Approval.Model | Approval.ApprovalId,\n policy: PolicyShape,\n ): Fx<Receipt.Model> =>\n Effect.gen(function* () {\n const attempt =\n typeof approval === \"string\"\n ? yield* storage.attempts.byApproval(approval)\n : yield* attemptOf(approval.planId);\n if (attempt.kind !== kind) {\n return yield* Errors.fail(\n new Reason.NotFound({\n entity: \"approval\",\n id: typeof approval === \"string\" ? approval : approval.id,\n }),\n );\n }\n const stored = attempt.approval;\n if (stored === null || (typeof approval !== \"string\" && stored.id !== approval.id)) {\n return yield* Errors.fail(\n new Reason.NotFound({\n entity: \"approval\",\n id: typeof approval === \"string\" ? approval : approval.id,\n }),\n );\n }\n if (attempt.receipt !== null) return attempt.receipt;\n const now = yield* DateTime.now;\n if (past(stored.expiresAt, now)) return yield* expired(\"approval\", stored.id);\n const claimed = yield* storage.attempts.claim(\n attempt.id,\n DateTime.addDuration(now, Duration.millis(policy.applyLeaseMs)),\n );\n return yield* run(claimed, stored).pipe(\n Effect.tapError((error) =>\n storage.attempts.fail(claimed.id, error.message).pipe(Effect.ignore),\n ),\n );\n });\n\n /** Writes under a claimed lease; a failure before any write releases the attempt as `failed`. */\n const run = (attempt: Storage.Attempt, approval: Approval.Model): Fx<Receipt.Model> =>\n Effect.gen(function* () {\n const plan = attempt.plan;\n const attachment = yield* storage.attachments.get(attempt.attachmentId);\n const { session, target } = yield* connect.session(attachment.id);\n const dns = session.dns(target);\n yield* revalidate(plan, dns, target.zone);\n const approved = new Set(approval.operationIds);\n const outcomes: Array<Receipt.Outcome> = [];\n const applied: Array<Receipt.Applied> = [];\n let stopped = false;\n const finish = (status: \"complete\" | \"partial\") =>\n Effect.gen(function* () {\n const receipt = new Receipt.Model({\n id: Receipt.ReceiptId.make(yield* fresh(\"rcpt\")),\n version: \"domainkit.receipt.v2\",\n kind,\n planId: plan.id,\n approvalId: approval.id,\n digest: plan.digest,\n provider: plan.provider,\n zone: plan.zone,\n status,\n outcomes: [...outcomes],\n appliedAt: yield* DateTime.now,\n });\n yield* storage.attempts.complete(attempt.id, receipt);\n return receipt;\n });\n const writes = Effect.gen(function* () {\n for (const operation of plan.operations) {\n if (operation._tag === \"Noop\") {\n outcomes.push(new Receipt.Skipped({ operationId: operation.id, reason: \"noop\" }));\n continue;\n }\n if (operation._tag === \"Conflict\" || !approved.has(operation.id)) {\n outcomes.push(\n new Receipt.Skipped({ operationId: operation.id, reason: \"not-approved\" }),\n );\n continue;\n }\n if (stopped) {\n outcomes.push(\n new Receipt.Skipped({ operationId: operation.id, reason: \"not-attempted\" }),\n );\n continue;\n }\n const result = yield* write(plan, operation, dns, target.zone).pipe(Effect.result);\n if (result._tag === \"Success\") {\n const outcome = new Receipt.Applied({\n operationId: operation.id,\n providerRecordId: result.success,\n });\n outcomes.push(outcome);\n applied.push(outcome);\n continue;\n }\n if (applied.length === 0) return yield* Effect.fail(result.failure);\n stopped = true;\n outcomes.push(\n new Receipt.Failed({ operationId: operation.id, message: result.failure.message }),\n );\n }\n return yield* finish(stopped ? \"partial\" : \"complete\");\n });\n return yield* writes.pipe(\n Effect.onInterrupt(() =>\n applied.length === 0\n ? Effect.void\n : Effect.gen(function* () {\n for (const operation of plan.operations) {\n if (outcomes.some((outcome) => outcome.operationId === operation.id)) continue;\n outcomes.push(\n new Receipt.Skipped({ operationId: operation.id, reason: \"not-attempted\" }),\n );\n }\n yield* finish(\"partial\");\n }).pipe(Effect.ignore),\n ),\n );\n });\n\n /** Re-plan the zone and fail `Stale` when the digest moved since the plan was built. */\n const revalidate = (plan: Plan.Model, dns: Provider.Dns, zone: string) =>\n Effect.gen(function* () {\n const current = yield* currentOperations(plan, dns, zone);\n const digest = yield* Planner.digest({\n version: plan.version,\n kind: plan.kind,\n domain: plan.domain,\n zone: plan.zone,\n provider: plan.provider,\n attachmentId: plan.attachmentId,\n operations: current,\n });\n if (digest !== plan.digest) return yield* stale(plan);\n });\n\n const currentOperations = (\n plan: Plan.Model,\n dns: Provider.Dns,\n zone: string,\n ): Effect.Effect<ReadonlyArray<Plan.Operation>, Errors.DomainKitError> =>\n kind === \"provisioning\"\n ? Effect.flatMap(dns.list(zone), (observed) =>\n Planner.reconcile(\n Planner.requirements(plan),\n observed.map(({ record: observedRecord }) => observedRecord),\n ),\n )\n : Effect.forEach(\n plan.operations,\n (operation): Effect.Effect<Plan.Operation, Errors.DomainKitError> =>\n operation._tag === \"Delete\"\n ? cleanupOperation(\n dns,\n zone,\n operation.id,\n operation.record,\n operation.providerRecordId,\n )\n : Effect.succeed(operation),\n );\n\n const write = (\n plan: Plan.Model,\n operation: Plan.Create | Plan.Delete,\n dns: Provider.Dns,\n zone: string,\n ): Effect.Effect<string, Errors.DomainKitError> =>\n operation._tag === \"Create\"\n ? Effect.gen(function* () {\n const observed = yield* dns.list(zone);\n const [check] = yield* Planner.reconcile(\n [operation.record],\n observed.map(({ record: observedRecord }) => observedRecord),\n );\n if (check?._tag !== \"Create\") return yield* stale(plan);\n const created = yield* dns.create(zone, operation.record);\n return created.providerRecordId;\n })\n : Effect.gen(function* () {\n const check = yield* cleanupOperation(\n dns,\n zone,\n operation.id,\n operation.record,\n operation.providerRecordId,\n );\n if (check._tag !== \"Delete\") return yield* stale(plan);\n yield* dns.delete(zone, operation.providerRecordId);\n return operation.providerRecordId;\n });\n\n return { record, approve, draftApproval, reject, apply, attemptOf };\n};\n\n/** Read one receipt-proven record back; anything but an exact match is a conflict. */\nexport const cleanupOperation = (\n dns: Provider.Dns,\n zone: string,\n id: Plan.OperationId,\n record: DnsRecord.Model,\n providerRecordId: string,\n): Effect.Effect<Plan.Delete | Plan.Conflict, Errors.DomainKitError> =>\n Effect.map(dns.get(zone, providerRecordId), (observed) => {\n if (observed === null)\n return new Plan.Conflict({ id, record, existing: [], reason: \"missing\" });\n if (!DnsRecord.equals(observed, record)) {\n return new Plan.Conflict({ id, record, existing: [observed], reason: \"value-mismatch\" });\n }\n return new Plan.Delete({ id, record, providerRecordId });\n });\n"],"mappings":"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"}
@@ -35,6 +35,7 @@ const attachZone = (definition, credential, zone) => Effect.gen(function* () {
35
35
  method: "token",
36
36
  capabilities: ["dns:read", "dns:write"],
37
37
  context: yield* encodeContext(definition, credential.context),
38
+ label: null,
38
39
  revocation: "active",
39
40
  createdBy: principal.actorId,
40
41
  createdAt: now
@@ -1 +1 @@
1
- {"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable","Provider.tokenFields"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer, Redacted } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n \"rejected-token\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and\n * rejected-token against a real provider definition, through the same Provision and Cleanup\n * services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: {\n readonly prefix?: string;\n /**\n * Token values the provider rejects, for the `rejected-token` case. Default: every required\n * field set to an empty secret, which needs a token method whose required fields are all\n * secrets.\n */\n readonly rejectedToken?: Provider.TokenValues;\n } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n /** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */\n const rejectedToken = Effect.gen(function* () {\n const name: Case = \"rejected-token\";\n const auth = definition.auth.token;\n if (auth === undefined) return;\n const required = Provider.tokenFields(auth).filter((field) => field.required);\n if (options.rejectedToken === undefined && required.some((field) => !field.secret)) {\n return yield* Effect.fail(\n failure(\n name,\n `the token method requires ${required\n .filter((field) => !field.secret)\n .map((field) => field.name)\n .join(\", \")}; pass options.rejectedToken with values the provider rejects`,\n ),\n );\n }\n const values =\n options.rejectedToken ??\n Object.fromEntries(required.map((field) => [field.name, Redacted.make(\"\")]));\n const outcome = yield* auth.authenticate(values).pipe(Effect.result);\n yield* expect(\n name,\n outcome._tag === \"Failure\" && outcome.failure.reason._tag === \"Unauthenticated\",\n \"authenticating a rejected token must fail Unauthenticated\",\n );\n });\n\n return Effect.all(\n [createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup, rejectedToken],\n { discard: true },\n );\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"provider.mjs","names":["Principal.make","Errors.DomainKitError","Reason.InvalidInput","Storage.Service","Custody.Service","Storage.Authorization","Provider.encodeContext","Storage.Credential","Connect.attach","Storage.Attachment","DnsRecord.cname","DnsRecord.txt","Receipt.applied","Connect.session","session","Principal.Service","DomainKit.layerMemory","Resolver.Service","plan","Provision.plan","Provision.apply","Provision.approve","DnsRecord.equals","Cleanup.plan","Cleanup.apply","Cleanup.approve","Errors.fail","Reason.ProviderUnavailable","Provider.tokenFields"],"sources":["../../../src/internal/conformance/provider.ts"],"sourcesContent":["import { DateTime, Effect, Layer, Redacted } from \"effect\";\n\nimport * as Cleanup from \"../../Cleanup.ts\";\nimport * as Connect from \"../../Connect.ts\";\nimport * as Custody from \"../../Custody.ts\";\nimport * as DnsRecord from \"../../DnsRecord.ts\";\nimport * as Errors from \"../error.ts\";\nimport * as Reason from \"../../Reason.ts\";\nimport * as DomainKit from \"../../DomainKit.ts\";\nimport * as Principal from \"../../Principal.ts\";\nimport * as Provider from \"../../Provider.ts\";\nimport * as Provision from \"../../Provision.ts\";\nimport * as Receipt from \"../../Receipt.ts\";\nimport * as Resolver from \"../../Resolver.ts\";\nimport * as Storage from \"../../Storage.ts\";\nimport { fresh } from \"../ids.ts\";\n\nexport const cases = [\n \"create-readback-cleanup\",\n \"exact-noop\",\n \"conflict\",\n \"stale-plan\",\n \"partial-apply-cleanup\",\n \"rejected-token\",\n] as const;\nexport type Case = (typeof cases)[number];\n\nconst principal = Principal.make({ ownerId: \"conformance\", actorId: \"conformance\" });\n\nconst failure = (name: Case, message: string) =>\n new Errors.DomainKitError({\n reason: new Reason.InvalidInput({\n message: `provider conformance ${name}: ${message}`,\n field: \"conformance\",\n }),\n });\n\nconst expect = (name: Case, condition: boolean, message: string) =>\n condition ? Effect.void : Effect.fail(failure(name, message));\n\ntype Env = Storage.Service | Connect.Service | Provision.Service | Cleanup.Service;\n\n/** Attach `zone` to a connection built straight from `credential`, bypassing `authenticate`. */\nconst attachZone = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n) =>\n Effect.gen(function* () {\n const storage = yield* Storage.Service;\n const custody = yield* Custody.Service;\n const now = yield* DateTime.now;\n const authorization = new Storage.Authorization({\n id: yield* fresh(\"auth\"),\n ownerId: principal.ownerId,\n provider: definition.id,\n method: \"token\",\n capabilities: [\"dns:read\", \"dns:write\"],\n context: yield* Provider.encodeContext(definition, credential.context),\n label: null,\n revocation: \"active\",\n createdBy: principal.actorId,\n createdAt: now,\n });\n yield* storage.authorizations.upsert({\n authorization,\n credential: new Storage.Credential({\n ciphertext: yield* custody.seal(credential.secret),\n expiresAt: null,\n rotatedAt: now,\n }),\n });\n const connection = yield* storage.connections.create(authorization.id);\n const attached = yield* Connect.attach({ connectionId: connection.id, domain: zone });\n if (!(attached instanceof Storage.Attachment)) {\n return yield* Effect.fail(\n failure(\"create-readback-cleanup\", `zone ${zone} needs a selection; pass an exact zone`),\n );\n }\n return attached;\n });\n\nconst cname = (zone: string, label: string) =>\n DnsRecord.cname({ name: `${label}.${zone}`, target: \"target.example.net\", ttl: 300 });\nconst txt = (zone: string, label: string, value: string) =>\n DnsRecord.txt({ name: `${label}.${zone}`, value, ttl: 300 });\n\nconst recordIds = (receipt: Receipt.Model) =>\n Receipt.applied(receipt).map(({ providerRecordId }) => providerRecordId);\n\n/**\n * Runs create/readback/cleanup, exact-noop, conflict, stale-plan, partial-apply, and\n * rejected-token against a real provider definition, through the same Provision and Cleanup\n * services hosts use.\n */\nexport const provider = (\n definition: Provider.Definition,\n credential: Provider.Credential,\n zone: string,\n options: {\n readonly prefix?: string;\n /**\n * Token values the provider rejects, for the `rejected-token` case. Default: every required\n * field set to an empty secret, which needs a token method whose required fields are all\n * secrets.\n */\n readonly rejectedToken?: Provider.TokenValues;\n } = {},\n): Effect.Effect<void, Errors.DomainKitError> => {\n const prefix = options.prefix ?? \"domainkit-conformance\";\n const run = <A>(\n target: Provider.Definition,\n body: (\n dns: Provider.Dns,\n attachment: Storage.Attachment,\n ) => Effect.Effect<A, Errors.DomainKitError, Env | Principal.Service>,\n ) =>\n Effect.gen(function* () {\n const attachment = yield* attachZone(target, credential, zone);\n const { session, target: resolved } = yield* Connect.session(attachment.id);\n return yield* body(session.dns(resolved), attachment);\n }).pipe(\n Effect.provideService(Principal.Service, principal),\n Effect.provide(\n DomainKit.layerMemory({\n providers: [target],\n resolver: Layer.succeed(Resolver.Service)(silentResolver),\n }),\n ),\n );\n\n const cleanup = (dns: Provider.Dns, ids: () => ReadonlyArray<string>) =>\n Effect.suspend(() =>\n Effect.forEach(ids(), (id) => dns.delete(zone, id).pipe(Effect.ignore), { discard: true }),\n );\n\n const createReadbackCleanup = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"create-readback-cleanup\";\n return Effect.gen(function* () {\n const requirements = [\n cname(zone, `${prefix}-create`),\n txt(zone, `${prefix}-page`, \"domainkit\"),\n ];\n const plan = yield* Provision.plan({ domain: attachment.domain, requirements });\n yield* expect(\n name,\n plan.operations.every(({ _tag }) => _tag === \"Create\"),\n \"expected two Create operations\",\n );\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"complete\" && created.length === 2,\n \"apply must return a complete receipt with provider record ids\",\n );\n const observed = yield* dns.list(zone);\n yield* expect(\n name,\n requirements.every((requirement) =>\n observed.some(({ record }) => DnsRecord.equals(record, requirement)),\n ),\n \"list must return every created record, including across provider pages\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* expect(\n name,\n cleanupPlan.operations.every(({ _tag }) => _tag === \"Delete\"),\n \"cleanup plan must delete both records\",\n );\n const cleaned = yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n cleaned.status === \"complete\" && remaining.every((record) => record === null),\n \"receipt-bound cleanup must remove every created record\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const exactNoop = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"exact-noop\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-exact`);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n yield* expect(name, plan.operations[0]?._tag === \"Noop\", \"an exact record must plan as Noop\");\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n yield* expect(\n name,\n Receipt.applied(receipt).length === 0,\n \"an exact plan must not create another record\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const conflict = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"conflict\";\n return Effect.gen(function* () {\n const { providerRecordId } = yield* dns.create(\n zone,\n txt(zone, `${prefix}-conflict`, \"occupied\"),\n );\n created = [providerRecordId];\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [cname(zone, `${prefix}-conflict`)],\n });\n yield* expect(\n name,\n plan.operations[0]?._tag === \"Conflict\",\n \"a CNAME over an occupied name must plan as Conflict\",\n );\n const rejected = yield* Provision.approve(plan).pipe(Effect.result);\n yield* expect(\n name,\n rejected._tag === \"Failure\" && rejected.failure.reason._tag === \"Conflict\",\n \"approve must fail closed on conflicts\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const stalePlan = run(definition, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"stale-plan\";\n return Effect.gen(function* () {\n const requirement = cname(zone, `${prefix}-stale`);\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [requirement],\n });\n const approval = yield* Provision.approve(plan);\n const { providerRecordId } = yield* dns.create(zone, requirement);\n created = [providerRecordId];\n const stale = yield* Provision.apply(approval).pipe(Effect.result);\n yield* expect(\n name,\n stale._tag === \"Failure\" && stale.failure.reason._tag === \"Stale\",\n \"apply must reject provider state that changed after approval\",\n );\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n const flaky: Provider.Definition = {\n ...definition,\n session: (input) => {\n const session = definition.session(input);\n return {\n ...session,\n dns: (target) => {\n const dns = session.dns(target);\n let writes = 0;\n return {\n ...dns,\n create: (targetZone, record) => {\n writes += 1;\n return writes === 2\n ? Errors.fail(\n new Reason.ProviderUnavailable({\n provider: definition.id,\n message: \"injected conformance failure\",\n }),\n )\n : dns.create(targetZone, record);\n },\n };\n },\n };\n },\n };\n\n const partialApplyCleanup = run(flaky, (dns, attachment) => {\n let created: ReadonlyArray<string> = [];\n const name: Case = \"partial-apply-cleanup\";\n return Effect.gen(function* () {\n const plan = yield* Provision.plan({\n domain: attachment.domain,\n requirements: [\n cname(zone, `${prefix}-partial-one`),\n txt(zone, `${prefix}-partial-two`, \"domainkit\"),\n ],\n });\n const receipt = yield* Provision.apply(yield* Provision.approve(plan));\n created = recordIds(receipt);\n yield* expect(\n name,\n receipt.status === \"partial\" && created.length === 1,\n \"a later write failure must preserve the first confirmed write in a partial receipt\",\n );\n const cleanupPlan = yield* Cleanup.plan({ receiptId: receipt.id });\n yield* Cleanup.apply(yield* Cleanup.approve(cleanupPlan));\n const remaining = yield* Effect.forEach(created, (id) => dns.get(zone, id));\n yield* expect(\n name,\n remaining.every((record) => record === null),\n \"partial receipt cleanup must remove the confirmed write\",\n );\n created = [];\n }).pipe(Effect.ensuring(cleanup(dns, () => created)));\n });\n\n /** A token the provider rejects must read as `Unauthenticated`, so a host tells the customer the provider refused it. */\n const rejectedToken = Effect.gen(function* () {\n const name: Case = \"rejected-token\";\n const auth = definition.auth.token;\n if (auth === undefined) return;\n const required = Provider.tokenFields(auth).filter((field) => field.required);\n if (options.rejectedToken === undefined && required.some((field) => !field.secret)) {\n return yield* Effect.fail(\n failure(\n name,\n `the token method requires ${required\n .filter((field) => !field.secret)\n .map((field) => field.name)\n .join(\", \")}; pass options.rejectedToken with values the provider rejects`,\n ),\n );\n }\n const values =\n options.rejectedToken ??\n Object.fromEntries(required.map((field) => [field.name, Redacted.make(\"\")]));\n const outcome = yield* auth.authenticate(values).pipe(Effect.result);\n yield* expect(\n name,\n outcome._tag === \"Failure\" && outcome.failure.reason._tag === \"Unauthenticated\",\n \"authenticating a rejected token must fail Unauthenticated\",\n );\n });\n\n return Effect.all(\n [createReadbackCleanup, exactNoop, conflict, stalePlan, partialApplyCleanup, rejectedToken],\n { discard: true },\n );\n};\n\n/** Verification is out of scope for the provider contract; keep the pool quiet. */\nconst silentResolver: Resolver.Interface = {\n resolve: () => Effect.succeed([]),\n};\n"],"mappings":"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"}