@domainkit/capsuledb 0.1.2 → 0.9.1

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.
package/dist/index.mjs CHANGED
@@ -1,493 +1,802 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { Context, Effect, Layer, Schema } from "effect";
3
- import { makeCapsule, makeMigration, sqlMigrationBody } from "capsuledb";
4
- import { Connection, ManagedDnsConnections } from "domainkit";
2
+ import { Capsule, Migration, Pg, Registry, Schema } from "capsuledb";
3
+ import { DomainKit, Principal, Reason, Storage } from "domainkit";
4
+ import { DateTime, Effect, Layer, Option, Schema as Schema$1 } from "effect";
5
5
  import * as SqlClient from "effect/unstable/sql/SqlClient";
6
- import * as SqlError from "effect/unstable/sql/SqlError";
7
6
  //#region package.json
8
- var version = "0.1.2";
7
+ var version = "0.9.1";
9
8
  //#endregion
10
- //#region src/custody.ts
11
- var custody_exports = /* @__PURE__ */ __exportAll({
12
- Error: () => Error$2,
13
- Service: () => Service$1
14
- });
15
- var Error$2 = class extends Schema.TaggedError()("CredentialCustodyError", {
16
- message: Schema.String,
17
- operation: Schema.String,
18
- retry: Schema.Literals([
19
- "never",
20
- "after-user-action",
21
- "safe",
22
- "unknown"
23
- ])
24
- }) {};
25
- /** Host-owned encryption boundary. DomainKit never receives keys or stores plaintext. */
26
- var Service$1 = class extends Context.Service()("@domainkit/capsuledb/CredentialCustody") {};
27
- //#endregion
28
- //#region src/host-bindings.ts
29
- var host_bindings_exports = /* @__PURE__ */ __exportAll({
30
- Error: () => Error$1,
31
- Service: () => Service
32
- });
33
- var Error$1 = class extends Schema.TaggedError()("HostBindingError", {
34
- message: Schema.String,
35
- operation: Schema.String,
36
- retry: Schema.Literals([
37
- "never",
38
- "after-user-action",
39
- "safe",
40
- "unknown"
41
- ])
42
- }) {};
43
- /** Stateless host mapping for tenant/domain foreign-key references. */
44
- var Service = class extends Context.Service()("@domainkit/capsuledb/HostBindings") {};
45
- //#endregion
46
- //#region src/persistence.ts
47
- const AttachmentContext = Schema.Struct({
48
- version: Schema.Literal("domainkit.attachment.v1"),
49
- value: Schema.Struct({
50
- accountKind: Schema.NullOr(Schema.Literals([
51
- "account",
52
- "personal",
53
- "team"
54
- ])),
55
- domain: Schema.String,
56
- evidence: Schema.optionalKey(Connection.ProviderTargetEvidence)
57
- })
58
- });
59
- const storageError = (operation, message, retry = "unknown") => new ManagedDnsConnections.Error({
60
- category: "storage",
9
+ //#region src/internal/storage.ts
10
+ /**
11
+ * `Storage.Interface` on PostgreSQL.
12
+ *
13
+ * Every method reads `Principal` and filters by `owner_id`, so a row belonging to another tenant
14
+ * is indistinguishable from a row that does not exist. Aggregate transitions run inside one
15
+ * transaction over a `FOR UPDATE` row, single-flight guards use a session-level advisory lock on a
16
+ * reserved connection, and revocation is two-phase: mark `pending`, call the provider outside any
17
+ * transaction, then delete.
18
+ *
19
+ * Storage never sees plaintext. `Connect` seals a credential through `Custody` before it reaches
20
+ * `upsert` or `rotate`, so this module only ever moves a ciphertext string.
21
+ */
22
+ const fail = (reason) => Effect.fail(new DomainKit.Error({ reason }));
23
+ const notFound = (entity, id) => fail(new Reason.NotFound({
24
+ entity,
25
+ id
26
+ }));
27
+ const invalid = (message, field) => fail(new Reason.InvalidInput({
61
28
  message,
29
+ ...field === void 0 ? {} : { field }
30
+ }));
31
+ const busy = (key) => fail(new Reason.Busy({ key }));
32
+ const storageFailed = (operation, message) => new DomainKit.Error({ reason: new Reason.StorageFailed({
62
33
  operation,
63
- retry
64
- });
65
- const missing = (operation, message) => storageError(operation, message, "never");
66
- const conflict = (operation, message) => storageError(operation, message, "after-user-action");
67
- const sqlErrorMessage = (error) => error.reason.cause instanceof Error ? `${error.reason._tag}: ${error.reason.cause.message}` : `${error.reason._tag}: ${error.reason.message ?? String(error.reason.cause)}`;
68
- const mapCapabilityError = (operation, error) => storageError(operation, error.message, error.retry);
69
- const mapSql = (operation, effect) => Effect.matchEffect(effect, {
70
- onFailure: (error) => {
71
- const mapped = SqlError.isSqlError(error) ? storageError(operation, sqlErrorMessage(error), "unknown") : error;
72
- return Effect.fail(mapped);
73
- },
74
- onSuccess: Effect.succeed
75
- });
76
- const sameAttachment = (left, right) => left.connectionId === right.connectionId && left.domain === right.domain && left.target.accountId === right.target.accountId && left.target.accountKind === right.target.accountKind && left.target.zoneId === right.target.zoneId && left.target.zoneName === right.target.zoneName;
77
- const decodeAuthorization = Effect.fn("CapsuleManagedDnsConnections.decodeAuthorization")(function* (row) {
78
- if (row.credential_ciphertext === null) return yield* Effect.fail(missing("decodeAuthorization", `Authorization ${row.id} has no credential ciphertext`));
34
+ message
35
+ }) });
36
+ const sqlMessage = (error) => error.reason.cause instanceof Error ? `${error.reason._tag}: ${error.reason.cause.message}` : `${error.reason._tag}: ${String(error.reason.cause)}`;
37
+ /**
38
+ * Collapse the SQL error channel into `DomainKit.Error`.
39
+ *
40
+ * A `DomainKit.Error` raised by the body is the deliberate outcome of an invariant and passes
41
+ * through unchanged; anything else is a driver failure and becomes `StorageFailed`.
42
+ */
43
+ const guard = (operation) => (effect) => Effect.catch(effect, (error) => Effect.fail(DomainKit.isError(error) ? error : storageFailed(operation, sqlMessage(error))));
44
+ /**
45
+ * A row is stored as its schema's encoded form spread over columns, so the core schema stays the
46
+ * only description of a row's shape. Reading reassembles the encoded object and decodes it.
47
+ */
48
+ const codec = (schema, subject) => {
49
+ const decode = Schema$1.decodeUnknownEffect(schema);
50
+ const encode = Schema$1.encodeUnknownEffect(schema);
79
51
  return {
80
- authorization: yield* Schema.decodeUnknownEffect(ManagedDnsConnections.Authorization.Schema)({
81
- authorizedById: row.subject_id,
82
- capabilityEvidence: row.capability_evidence,
83
- createdAt: row.created_at.toISOString(),
84
- id: row.id,
85
- method: row.kind,
86
- providerContext: row.provider_context,
87
- providerId: row.provider_id,
88
- requiredCapabilities: row.capabilities,
89
- revocation: row.revocation,
90
- scopes: row.scopes
91
- }).pipe(Effect.mapError((error) => missing("decodeAuthorization", `Authorization ${row.id} is invalid: ${error.message}`))),
92
- credentialCiphertext: row.credential_ciphertext,
93
- credentialExpiresAt: row.expires_at
52
+ read: (input) => decode(input).pipe(Effect.mapError((error) => storageFailed(`${subject}.decode`, `stored ${subject} is invalid: ${error.message}`))),
53
+ write: (value) => encode(value).pipe(Effect.mapError((error) => storageFailed(`${subject}.encode`, `${subject} cannot be stored: ${error.message}`)))
94
54
  };
55
+ };
56
+ const authorizationCodec = codec(Storage.Authorization, "authorization");
57
+ const connectionCodec = codec(Storage.Connection, "connection");
58
+ const attachmentCodec = codec(Storage.Attachment, "attachment");
59
+ const continuationCodec = codec(Storage.Continuation, "continuation");
60
+ const attemptCodec = codec(Storage.Attempt, "attempt");
61
+ const readinessCodec = codec(Storage.Readiness, "readiness");
62
+ const credentialCodec = codec(Storage.Credential, "credential");
63
+ /** A `TIMESTAMPTZ` column as the ISO string every DomainKit timestamp schema encodes to. */
64
+ const iso = (value) => value instanceof Date ? value.toISOString() : String(value);
65
+ const isoOrNull = (value) => value === null ? null : iso(value);
66
+ /** An ISO string from a schema's encoded form as the `Date` the driver binds to `TIMESTAMPTZ`. */
67
+ const at = (value) => new Date(value);
68
+ const atOrNull = (value) => value === null ? null : at(value);
69
+ const now = Effect.map(DateTime.now, DateTime.toDateUtc);
70
+ /**
71
+ * A `JSONB` column as a plain value.
72
+ *
73
+ * Drivers disagree about whether they parse JSON columns, so accept both and normalize here
74
+ * rather than depending on a host's client configuration.
75
+ */
76
+ const fromJson = (value) => typeof value === "string" ? JSON.parse(value) : value;
77
+ const fromJsonOrNull = (value) => value === null ? null : fromJson(value);
78
+ /** Bind a value to a `JSONB` column as text; PostgreSQL coerces the untyped parameter. */
79
+ const toJson = (value) => JSON.stringify(value ?? null);
80
+ const toJsonOrNull = (value) => value === null || value === void 0 ? null : toJson(value);
81
+ const authorizationOf = (row) => authorizationCodec.read({
82
+ id: row.id,
83
+ ownerId: row.owner_id,
84
+ provider: row.provider,
85
+ method: row.method,
86
+ capabilities: fromJson(row.capabilities),
87
+ context: fromJson(row.context),
88
+ revocation: row.revocation,
89
+ createdBy: row.created_by,
90
+ createdAt: iso(row.created_at)
95
91
  });
96
- const decodeAttachment = Effect.fn("CapsuleManagedDnsConnections.decodeAttachment")(function* (row) {
97
- const context = yield* Schema.decodeUnknownEffect(AttachmentContext)(row.provider_target_context).pipe(Effect.mapError((error) => missing("decodeAttachment", `Attachment ${row.id} context is invalid: ${error.message}`)));
98
- return yield* Schema.decodeUnknownEffect(Connection.DomainAttachment)({
99
- connectionId: row.connection_id,
100
- createdAt: row.created_at.toISOString(),
101
- domain: context.value.domain,
102
- id: row.id,
103
- target: {
104
- accountId: row.provider_account_id,
105
- accountKind: context.value.accountKind,
106
- ...context.value.evidence === void 0 ? {} : { evidence: context.value.evidence },
107
- zoneId: row.provider_zone_id,
108
- zoneName: row.provider_zone_name
109
- }
110
- }).pipe(Effect.mapError((error) => missing("decodeAttachment", `Attachment ${row.id} is invalid: ${error.message}`)));
92
+ const credentialOf = (row) => credentialCodec.read({
93
+ ciphertext: row.credential_ciphertext,
94
+ expiresAt: isoOrNull(row.credential_expires_at),
95
+ rotatedAt: iso(row.credential_rotated_at)
111
96
  });
112
- const encodeAuthorization = Effect.fn("CapsuleManagedDnsConnections.encodeAuthorization")(function* (authorization) {
113
- return yield* Schema.encodeUnknownEffect(ManagedDnsConnections.Authorization.Schema)(authorization).pipe(Effect.mapError((error) => conflict("encodeAuthorization", `Authorization cannot be encoded: ${error.message}`)));
97
+ const connectionOf = (row) => connectionCodec.read({
98
+ id: row.id,
99
+ ownerId: row.owner_id,
100
+ authorizationId: row.authorization_id,
101
+ createdAt: iso(row.created_at)
114
102
  });
115
- const loadAggregate = Effect.fn("CapsuleManagedDnsConnections.loadAggregate")(function* (sql, bindings, authorizationId, lock = "share") {
116
- const lockClause = lock === "update" ? "FOR UPDATE" : "FOR SHARE";
117
- const row = (yield* sql.unsafe(`SELECT id, provider_id, subject_id, kind, capabilities, capability_evidence, scopes,
118
- provider_context, revocation, credential_ciphertext, expires_at, created_at
119
- FROM domain_provider_authorizations WHERE id = $1 ${lockClause}`, [authorizationId]))[0];
120
- if (row === void 0) return null;
121
- const decoded = yield* decodeAuthorization(row);
122
- const connectionRows = yield* sql`SELECT id, organization_id, authorization_id,
123
- created_at FROM organization_domain_provider_connections
124
- WHERE authorization_id = ${authorizationId} ORDER BY id`;
125
- const connections = yield* Effect.forEach(connectionRows, (connection) => bindings.ownerId(connection.organization_id).pipe(Effect.mapError((error) => mapCapabilityError("loadAggregate", error)), Effect.map((ownerId) => ({
126
- authorizationId: connection.authorization_id,
127
- createdAt: connection.created_at,
128
- id: connection.id,
129
- method: decoded.authorization.method,
130
- ownerId,
131
- providerId: decoded.authorization.providerId
132
- }))));
133
- const attachmentRows = yield* sql`SELECT a.id, a.connection_id,
134
- a.provider_account_id, a.provider_zone_id, a.provider_zone_name,
135
- a.provider_target_context, a.created_at
136
- FROM domain_provider_attachments a
137
- JOIN organization_domain_provider_connections c ON c.id = a.connection_id
138
- WHERE c.authorization_id = ${authorizationId} ORDER BY a.id`;
139
- return {
140
- attachments: yield* Effect.forEach(attachmentRows, decodeAttachment),
141
- authorization: decoded.authorization,
142
- connections,
143
- credentialCiphertext: decoded.credentialCiphertext,
144
- credentialExpiresAt: decoded.credentialExpiresAt
145
- };
103
+ const attachmentOf = (row) => attachmentCodec.read({
104
+ id: row.id,
105
+ ownerId: row.owner_id,
106
+ connectionId: row.connection_id,
107
+ domain: row.domain,
108
+ zone: row.zone,
109
+ target: fromJson(row.target),
110
+ createdAt: iso(row.created_at)
146
111
  });
147
- const authorizationIdForConnection = Effect.fn("CapsuleManagedDnsConnections.authorizationIdForConnection")(function* (sql, connectionId) {
148
- return (yield* sql`SELECT authorization_id
149
- FROM organization_domain_provider_connections WHERE id = ${connectionId}`)[0]?.authorization_id ?? null;
112
+ const continuationOf = (row) => continuationCodec.read({
113
+ id: row.id,
114
+ ownerId: row.owner_id,
115
+ actorId: row.actor_id,
116
+ provider: row.provider,
117
+ payload: fromJson(row.payload),
118
+ returnTo: row.return_to,
119
+ expiresAt: iso(row.expires_at)
150
120
  });
151
- const authorizationIdForAttachment = Effect.fn("CapsuleManagedDnsConnections.authorizationIdForAttachment")(function* (sql, attachmentId) {
152
- return (yield* sql`SELECT c.authorization_id
153
- FROM domain_provider_attachments a
154
- JOIN organization_domain_provider_connections c ON c.id = a.connection_id
155
- WHERE a.id = ${attachmentId}`)[0]?.authorization_id ?? null;
121
+ const attemptOf = (row) => attemptCodec.read({
122
+ id: row.id,
123
+ ownerId: row.owner_id,
124
+ attachmentId: row.attachment_id,
125
+ kind: row.kind,
126
+ status: row.status,
127
+ plan: fromJson(row.plan),
128
+ approval: fromJsonOrNull(row.approval),
129
+ receipt: fromJsonOrNull(row.receipt),
130
+ rejection: fromJsonOrNull(row.rejection),
131
+ sourceReceiptId: row.source_receipt_id,
132
+ leaseExpiresAt: isoOrNull(row.lease_expires_at),
133
+ failure: row.failure,
134
+ updatedAt: iso(row.updated_at)
156
135
  });
157
- const openAggregate = Effect.fn("CapsuleManagedDnsConnections.openAggregate")(function* (custody, aggregate) {
158
- const credential = {
159
- ...yield* custody.open(aggregate.credentialCiphertext).pipe(Effect.mapError((error) => mapCapabilityError("openCredential", error))),
160
- expiresAt: aggregate.credentialExpiresAt
161
- };
162
- return {
163
- attachments: aggregate.attachments,
164
- authorization: aggregate.authorization,
165
- connections: aggregate.connections,
166
- credential
167
- };
168
- });
169
- const projectConnection = Effect.fn("CapsuleManagedDnsConnections.projectConnection")(function* (custody, aggregate, connection, authorization = aggregate.authorization) {
170
- const opened = yield* openAggregate(custody, aggregate);
171
- return Connection.project(connection, authorization, opened.credential);
172
- });
173
- const writeAuthorization = Effect.fn("CapsuleManagedDnsConnections.writeAuthorization")(function* (sql, authorization, credentialCiphertext, credentialExpiresAt) {
174
- const encoded = yield* encodeAuthorization(authorization);
175
- yield* sql`INSERT INTO domain_provider_authorizations (
176
- id, provider_id, account_id, subject_id, kind, capabilities, capability_evidence, scopes,
177
- provider_context, revocation, credential_ciphertext, expires_at, created_at, updated_at
178
- ) VALUES (
179
- ${authorization.id}, ${authorization.providerId}, NULL, ${authorization.authorizedById},
180
- ${authorization.method}, ${JSON.stringify(encoded.requiredCapabilities)}::jsonb,
181
- ${JSON.stringify(encoded.capabilityEvidence)}::jsonb,
182
- ${JSON.stringify(encoded.scopes)}::jsonb, ${JSON.stringify(encoded.providerContext)}::jsonb,
183
- ${JSON.stringify(encoded.revocation)}::jsonb,
184
- ${credentialCiphertext}, ${credentialExpiresAt}, ${authorization.createdAt}, NOW()
185
- ) ON CONFLICT (id) DO UPDATE SET
186
- provider_id = EXCLUDED.provider_id,
187
- subject_id = EXCLUDED.subject_id,
188
- kind = EXCLUDED.kind,
189
- capabilities = EXCLUDED.capabilities,
190
- capability_evidence = EXCLUDED.capability_evidence,
191
- scopes = EXCLUDED.scopes,
192
- provider_context = EXCLUDED.provider_context,
193
- revocation = EXCLUDED.revocation,
194
- credential_ciphertext = EXCLUDED.credential_ciphertext,
195
- expires_at = EXCLUDED.expires_at,
196
- updated_at = NOW()`;
136
+ const readinessOf = (row) => readinessCodec.read({
137
+ domain: row.domain,
138
+ attachmentId: row.attachment_id,
139
+ ownerId: row.owner_id,
140
+ overall: row.overall,
141
+ requirements: fromJson(row.requirements),
142
+ host: fromJson(row.host),
143
+ pendingSince: isoOrNull(row.pending_since),
144
+ checkedAt: iso(row.checked_at),
145
+ nextCheckAt: isoOrNull(row.next_check_at)
197
146
  });
198
- const makeRepository = Effect.gen(function* () {
199
- const sql = yield* SqlClient.SqlClient;
200
- const custody = yield* Service$1;
201
- const bindings = yield* Service;
202
- const withRevocationLock = (operation, authorizationId, effect) => sql.withTransaction(Effect.gen(function* () {
203
- if ((yield* sql`
204
- SELECT pg_try_advisory_xact_lock(hashtextextended(${authorizationId}, 0)) AS acquired`)[0]?.acquired !== true) return yield* Effect.fail(storageError(operation, "Provider revocation is already in progress", "safe"));
205
- return yield* effect;
206
- }));
207
- const getStored = (authorizationId, lock = "share") => loadAggregate(sql, bindings, authorizationId, lock);
208
- const get = (authorizationId) => mapSql("get", sql.withTransaction(getStored(authorizationId)).pipe(Effect.flatMap((aggregate) => aggregate === null ? Effect.succeed(null) : openAggregate(custody, aggregate))));
209
- const getByConnectionId = (connectionId) => mapSql("getByConnectionId", sql.withTransaction(Effect.gen(function* () {
210
- const authorizationId = yield* authorizationIdForConnection(sql, connectionId);
211
- if (authorizationId === null) return null;
212
- const aggregate = yield* getStored(authorizationId);
213
- if (aggregate === null || !aggregate.connections.some((connection) => connection.id === connectionId)) return null;
214
- return aggregate;
215
- })).pipe(Effect.flatMap((aggregate) => aggregate === null ? Effect.succeed(null) : openAggregate(custody, aggregate))));
216
- return ManagedDnsConnections.Service.of({
217
- attach: (input) => mapSql("attach", sql.withTransaction(Effect.gen(function* () {
218
- const authorizationId = yield* authorizationIdForConnection(sql, input.connectionId);
219
- if (authorizationId === null) return yield* Effect.fail(missing("attach", "Provider connection does not exist"));
220
- const aggregate = yield* getStored(authorizationId, "update");
221
- if (aggregate === null) return yield* Effect.fail(missing("attach", "Provider authorization does not exist"));
222
- const connection = aggregate.connections.find((candidate) => candidate.id === input.connectionId && candidate.ownerId === input.ownerId);
223
- if (connection === void 0) return yield* Effect.fail(missing("attach", "Provider connection does not belong to this owner"));
224
- if (aggregate.authorization.revocation._tag === "Pending") return yield* Effect.fail(conflict("attach", "Provider authorization is awaiting revocation recovery"));
225
- if (input.attachment.connectionId !== input.connectionId) return yield* Effect.fail(conflict("attach", "Domain attachment targets another connection"));
226
- const existing = aggregate.attachments.find(({ id }) => id === input.attachment.id);
227
- if (existing !== void 0) {
228
- if (!sameAttachment(existing, input.attachment)) return yield* Effect.fail(conflict("attach", "Domain attachment id is already in use"));
229
- return {
230
- attachment: existing,
231
- connection: yield* projectConnection(custody, aggregate, connection)
232
- };
233
- }
234
- const reference = yield* bindings.domain({
235
- domain: input.attachment.domain,
236
- ownerId: input.ownerId
237
- }).pipe(Effect.mapError((error) => mapCapabilityError("attach", error)));
238
- const context = {
239
- version: "domainkit.attachment.v1",
240
- value: {
241
- accountKind: input.attachment.target.accountKind,
242
- domain: input.attachment.domain,
243
- ...input.attachment.target.evidence === void 0 ? {} : { evidence: input.attachment.target.evidence }
244
- }
245
- };
246
- yield* sql`INSERT INTO domain_provider_attachments (
247
- id, organization_id, connection_id, domain_id, provider_account_id,
248
- provider_zone_id, provider_zone_name, provider_target_context, created_at, updated_at
249
- ) VALUES (
250
- ${input.attachment.id}, ${reference.ownerReference}, ${input.connectionId},
251
- ${reference.domainReference}, ${input.attachment.target.accountId},
252
- ${input.attachment.target.zoneId}, ${input.attachment.target.zoneName},
253
- ${JSON.stringify(context)}::jsonb,
254
- ${input.attachment.createdAt}, NOW()
255
- )`;
256
- return {
257
- attachment: input.attachment,
258
- connection: yield* projectConnection(custody, aggregate, connection)
259
- };
260
- }))),
261
- connect: (input) => custody.seal(input.credential).pipe(Effect.mapError((error) => mapCapabilityError("connect", error)), Effect.flatMap((credentialCiphertext) => mapSql("connect", sql.withTransaction(Effect.gen(function* () {
262
- yield* sql`SELECT pg_advisory_xact_lock(hashtextextended(${input.authorization.id}, 0))`;
263
- const current = yield* getStored(input.authorization.id, "update");
264
- if (current?.authorization.revocation._tag === "Pending" || input.expectedAuthorizationId !== void 0 && current === null) return yield* Effect.fail(conflict("connect", current === null ? "Provider authorization no longer exists" : "Provider authorization is awaiting revocation recovery"));
265
- if (input.authorization.id !== input.connection.authorizationId || input.authorization.providerId !== input.connection.providerId || input.authorization.method !== input.connection.method) return yield* Effect.fail(conflict("connect", "Connection does not match its authorization"));
266
- const existingConnection = current?.connections.find(({ ownerId }) => ownerId === input.connection.ownerId);
267
- if (input.expectedConnectionId !== void 0 && existingConnection?.id !== input.expectedConnectionId) return yield* Effect.fail(conflict("connect", "Organization connection changed during reconnect"));
268
- yield* writeAuthorization(sql, input.authorization, credentialCiphertext, input.credential.expiresAt);
269
- if (existingConnection === void 0) {
270
- const reference = yield* bindings.owner(input.connection.ownerId).pipe(Effect.mapError((error) => mapCapabilityError("connect", error)));
271
- yield* sql`INSERT INTO organization_domain_provider_connections (
272
- id, organization_id, authorization_id, created_at, updated_at
273
- ) VALUES (
274
- ${input.connection.id}, ${reference.ownerReference},
275
- ${input.authorization.id}, ${input.connection.createdAt}, NOW()
276
- )`;
277
- }
278
- return (yield* getStored(input.authorization.id, "update")) ?? (yield* Effect.fail(missing("connect", "Authorization write vanished")));
279
- })))), Effect.flatMap((aggregate) => openAggregate(custody, aggregate))),
280
- detach: ({ attachmentId, ownerId }) => mapSql("detach", sql.withTransaction(Effect.gen(function* () {
281
- const authorizationId = yield* authorizationIdForAttachment(sql, attachmentId);
282
- if (authorizationId === null) return yield* Effect.fail(missing("detach", "Domain attachment does not exist"));
283
- const aggregate = yield* getStored(authorizationId, "update");
284
- const attachment = aggregate?.attachments.find(({ id }) => id === attachmentId);
285
- if (aggregate === null || attachment === void 0) return yield* Effect.fail(missing("detach", "Domain attachment does not exist"));
286
- const connection = aggregate.connections.find(({ id, ownerId: candidate }) => id === attachment.connectionId && candidate === ownerId);
287
- if (connection === void 0) return yield* Effect.fail(missing("detach", "Domain attachment does not belong to this owner"));
288
- yield* sql`DELETE FROM domain_provider_attachments WHERE id = ${attachmentId}`;
289
- return {
290
- attachment,
291
- connection: yield* projectConnection(custody, aggregate, connection),
292
- remainingAttachments: aggregate.attachments.filter(({ connectionId, id }) => connectionId === connection.id && id !== attachmentId).length
293
- };
294
- }))),
295
- disconnect: ({ connectionId, ownerId, revoke }) => mapSql("disconnect", sql.withTransaction(Effect.gen(function* () {
296
- const authorizationId = yield* authorizationIdForConnection(sql, connectionId);
297
- if (authorizationId === null) return yield* Effect.fail(missing("disconnect", "Provider connection does not exist"));
298
- const aggregate = yield* getStored(authorizationId, "update");
299
- const connection = aggregate?.connections.find(({ id, ownerId: candidate }) => id === connectionId && candidate === ownerId);
300
- if (aggregate === null || connection === void 0) return yield* Effect.fail(missing("disconnect", "Provider connection does not belong to this owner"));
301
- if (aggregate.attachments.some(({ connectionId: id }) => id === connectionId)) return yield* Effect.fail(conflict("disconnect", "Detach all organization domains before disconnecting the provider"));
302
- if (aggregate.connections.length > 1) {
303
- yield* sql`DELETE FROM organization_domain_provider_connections
304
- WHERE id = ${connectionId}`;
305
- return {
306
- _tag: "Disconnected",
307
- connection: yield* projectConnection(custody, aggregate, connection),
308
- remainingConnections: aggregate.connections.length - 1
309
- };
310
- }
311
- if (aggregate.authorization.revocation._tag === "Pending") return yield* Effect.fail(conflict("disconnect", "Provider authorization is awaiting revocation recovery"));
312
- const pending = {
313
- ...aggregate.authorization,
314
- revocation: {
315
- _tag: "Pending",
316
- requestedAt: /* @__PURE__ */ new Date()
317
- }
318
- };
319
- yield* writeAuthorization(sql, pending, aggregate.credentialCiphertext, aggregate.credentialExpiresAt);
320
- return {
321
- _tag: "Prepared",
322
- authorization: pending,
323
- connection: yield* projectConnection(custody, aggregate, connection, pending)
324
- };
325
- })).pipe(Effect.flatMap((prepared) => {
326
- if (prepared._tag === "Disconnected") {
327
- const result = {
328
- connection: prepared.connection,
329
- remainingConnections: prepared.remainingConnections,
330
- revokedAuthorization: false
331
- };
332
- return Effect.succeed(result);
333
- }
334
- const result = {
335
- connection: prepared.connection,
336
- remainingConnections: 0,
337
- revokedAuthorization: true
338
- };
339
- return withRevocationLock("disconnect", prepared.authorization.id, getStored(prepared.authorization.id, "update").pipe(Effect.flatMap((current) => {
340
- if (current?.authorization.revocation._tag !== "Pending") return Effect.fail(conflict("disconnect", "Revocation state changed before provider call"));
341
- return revoke(current.authorization).pipe(Effect.andThen(Effect.gen(function* () {
342
- if ((yield* getStored(prepared.authorization.id, "update"))?.authorization.revocation._tag !== "Pending") return yield* Effect.fail(conflict("disconnect", "Revocation state changed before completion"));
343
- yield* sql`DELETE FROM domain_provider_attachments WHERE connection_id IN
344
- (SELECT id FROM organization_domain_provider_connections
345
- WHERE authorization_id = ${prepared.authorization.id})`;
346
- yield* sql`DELETE FROM organization_domain_provider_connections
347
- WHERE authorization_id = ${prepared.authorization.id}`;
348
- yield* sql`DELETE FROM domain_provider_authorizations
349
- WHERE id = ${prepared.authorization.id}`;
350
- })), Effect.as(result));
351
- })));
352
- }))),
353
- findConnection: (ownerId, authorizationId) => get(authorizationId).pipe(Effect.map((aggregate) => aggregate?.connections.find((connection) => connection.ownerId === ownerId) ?? null)),
354
- get,
355
- getAttachment: (attachmentId) => mapSql("getAttachment", sql.withTransaction(Effect.gen(function* () {
356
- const authorizationId = yield* authorizationIdForAttachment(sql, attachmentId);
357
- if (authorizationId === null) return null;
358
- return (yield* getStored(authorizationId))?.attachments.find(({ id }) => id === attachmentId) ?? null;
359
- }))),
360
- getByConnectionId,
361
- listAttachments: (connectionId, ownerId) => getByConnectionId(connectionId).pipe(Effect.flatMap((aggregate) => {
362
- if (aggregate === null) return Effect.fail(missing("listAttachments", "Provider connection does not exist"));
363
- if (!aggregate.connections.some((connection) => connection.id === connectionId && connection.ownerId === ownerId)) return Effect.fail(missing("listAttachments", "Provider connection does not belong to this owner"));
364
- return Effect.succeed(aggregate.attachments.filter((attachment) => attachment.connectionId === connectionId));
365
- })),
366
- promoteEvidence: (authorizationId, evidence) => mapSql("promoteEvidence", sql.withTransaction(Effect.gen(function* () {
367
- const aggregate = yield* getStored(authorizationId, "update");
368
- if (aggregate === null) return yield* Effect.fail(missing("promoteEvidence", "Provider authorization does not exist"));
369
- const byCapability = new Map(aggregate.authorization.capabilityEvidence.map((item) => [item.capability, item]));
370
- for (const item of evidence) byCapability.set(item.capability, item);
371
- yield* writeAuthorization(sql, {
372
- ...aggregate.authorization,
373
- capabilityEvidence: [...byCapability.values()]
374
- }, aggregate.credentialCiphertext, aggregate.credentialExpiresAt);
375
- return (yield* getStored(authorizationId, "update")) ?? (yield* Effect.fail(missing("promoteEvidence", "Authorization write vanished")));
376
- }))).pipe(Effect.flatMap((aggregate) => openAggregate(custody, aggregate))),
377
- recover: ({ authorizationId, revoke }) => mapSql("recover", withRevocationLock("recover", authorizationId, getStored(authorizationId, "update").pipe(Effect.flatMap((aggregate) => Effect.gen(function* () {
378
- if (aggregate === null) return yield* Effect.fail(missing("recover", "Provider authorization does not exist"));
379
- if (aggregate.authorization.revocation._tag !== "Pending") return yield* Effect.fail(missing("recover", "Provider authorization is not awaiting revocation"));
380
- const connection = aggregate.connections[0];
381
- if (connection === void 0) return yield* Effect.fail(missing("recover", "Pending authorization has no connection"));
382
- const projected = yield* projectConnection(custody, aggregate, connection);
383
- return yield* revoke(aggregate.authorization).pipe(Effect.andThen(Effect.gen(function* () {
384
- if ((yield* getStored(authorizationId, "update"))?.authorization.revocation._tag !== "Pending") return yield* Effect.fail(conflict("recover", "Revocation state changed before completion"));
385
- yield* sql`DELETE FROM domain_provider_attachments WHERE connection_id IN
386
- (SELECT id FROM organization_domain_provider_connections
387
- WHERE authorization_id = ${authorizationId})`;
388
- yield* sql`DELETE FROM organization_domain_provider_connections
389
- WHERE authorization_id = ${authorizationId}`;
390
- yield* sql`DELETE FROM domain_provider_authorizations WHERE id = ${authorizationId}`;
391
- })), Effect.as({
392
- connection: projected,
393
- remainingConnections: 0,
394
- revokedAuthorization: true
395
- }));
396
- }))))),
397
- rotate: (authorizationId, credential) => custody.seal(credential).pipe(Effect.mapError((error) => mapCapabilityError("rotate", error)), Effect.flatMap((credentialCiphertext) => mapSql("rotate", sql.withTransaction(Effect.gen(function* () {
398
- const aggregate = yield* getStored(authorizationId, "update");
399
- if (aggregate === null) return yield* Effect.fail(missing("rotate", "Provider authorization does not exist"));
400
- yield* writeAuthorization(sql, aggregate.authorization, credentialCiphertext, credential.expiresAt);
401
- return (yield* getStored(authorizationId, "update")) ?? (yield* Effect.fail(missing("rotate", "Authorization write vanished")));
402
- })))), Effect.flatMap((aggregate) => openAggregate(custody, aggregate)))
147
+ const stale = (attempt) => fail(new Reason.Stale({
148
+ planId: attempt.id,
149
+ digest: attempt.plan.digest
150
+ }));
151
+ /** `prefix_<uuid>`, the same identifier shape the memory implementation mints. */
152
+ const fresh = (prefix) => Effect.sync(() => `${prefix}_${crypto.randomUUID()}`);
153
+ const make$2 = (tables) => Effect.gen(function* () {
154
+ const sql = (yield* SqlClient.SqlClient).withoutTransforms();
155
+ const authorizations = sql(tables.authorizations.name);
156
+ const connections = sql(tables.connections.name);
157
+ const attachments = sql(tables.attachments.name);
158
+ const continuations = sql(tables.continuations.name);
159
+ const attempts = sql(tables.attempts.name);
160
+ const readiness = sql(tables.readiness.name);
161
+ /** The row this principal owns, locked for the rest of the transaction. */
162
+ const lockedAuthorization = (owner, id) => sql`
163
+ SELECT * FROM ${authorizations} WHERE id = ${id} AND owner_id = ${owner} FOR UPDATE
164
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("authorization", id) : Effect.succeed(rows[0])));
165
+ const lockedAttempt = (owner, id) => sql`
166
+ SELECT * FROM ${attempts} WHERE id = ${id} AND owner_id = ${owner} FOR UPDATE
167
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("plan", id) : Effect.succeed(rows[0])), Effect.flatMap(attemptOf));
168
+ const requireConnection = (owner, id) => sql`
169
+ SELECT * FROM ${connections} WHERE id = ${id} AND owner_id = ${owner}
170
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("connection", id) : Effect.succeed(rows[0])));
171
+ const requireAttachment = (owner, id) => sql`
172
+ SELECT * FROM ${attachments} WHERE id = ${id} AND owner_id = ${owner}
173
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("attachment", id) : Effect.succeed(rows[0])));
174
+ const writeAuthorization = (authorization, credential) => Effect.all({
175
+ row: authorizationCodec.write(authorization),
176
+ secret: credentialCodec.write(credential)
403
177
  });
178
+ /**
179
+ * Phase one of revocation: mark the row `pending` so a crash between the provider call and the
180
+ * delete is recoverable. Idempotent, and it never widens the row's state.
181
+ *
182
+ * Returns the ciphertext the caller is about to have revoked, which phase two checks.
183
+ */
184
+ const prepareRevocation = (owner, id) => sql.withTransaction(Effect.gen(function* () {
185
+ const row = yield* lockedAuthorization(owner, id);
186
+ yield* sql`
187
+ UPDATE ${authorizations} SET revocation = 'pending', updated_at = ${yield* now}
188
+ WHERE id = ${id} AND owner_id = ${owner}
189
+ `;
190
+ return row.credential_ciphertext;
191
+ }));
192
+ /**
193
+ * Phase two: delete the row, but only while it still holds the credential the provider was
194
+ * asked to revoke.
195
+ *
196
+ * A credential refresh runs concurrently with revocation and rotates the row after the provider
197
+ * call selected the old ciphertext. Deleting unconditionally would drop the row while the newly
198
+ * issued credential is still live at the provider, with no local state left to revoke it from.
199
+ * Leaving the row `pending` instead hands it to `recoverRevocations`, which revokes what the
200
+ * row now holds. Every ciphertext is AES-GCM with a fresh IV, so equality here is exact.
201
+ */
202
+ const completeRevocation = (owner, id, ciphertext) => sql`
203
+ DELETE FROM ${authorizations}
204
+ WHERE id = ${id} AND owner_id = ${owner} AND credential_ciphertext = ${ciphertext}
205
+ `;
206
+ return {
207
+ authorizations: {
208
+ upsert: ({ authorization, credential, expectedId }) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
209
+ if (authorization.ownerId !== ownerId) return yield* invalid("Authorization owner does not match the principal", "ownerId");
210
+ const { row, secret } = yield* writeAuthorization(authorization, credential);
211
+ const stamp = yield* now;
212
+ return yield* sql.withTransaction(Effect.gen(function* () {
213
+ if (expectedId !== void 0) {
214
+ if ((yield* lockedAuthorization(ownerId, expectedId)).revocation !== "active") return yield* busy(`authorization:${expectedId}`);
215
+ if (authorization.id !== expectedId) return yield* invalid("Authorization id must match expectedId", "id");
216
+ yield* sql`
217
+ UPDATE ${authorizations} SET
218
+ provider = ${row.provider},
219
+ method = ${row.method},
220
+ capabilities = ${toJson(row.capabilities)},
221
+ context = ${toJson(row.context)},
222
+ revocation = ${row.revocation},
223
+ created_by = ${row.createdBy},
224
+ created_at = ${at(row.createdAt)},
225
+ credential_ciphertext = ${secret.ciphertext},
226
+ credential_expires_at = ${atOrNull(secret.expiresAt)},
227
+ credential_rotated_at = ${at(secret.rotatedAt)},
228
+ updated_at = ${stamp}
229
+ WHERE id = ${expectedId} AND owner_id = ${ownerId}
230
+ `;
231
+ return authorization;
232
+ }
233
+ return (yield* sql`
234
+ INSERT INTO ${authorizations} (
235
+ id, owner_id, provider, method, capabilities, context, revocation,
236
+ created_by, created_at, credential_ciphertext, credential_expires_at,
237
+ credential_rotated_at, updated_at
238
+ ) VALUES (
239
+ ${row.id}, ${row.ownerId}, ${row.provider}, ${row.method},
240
+ ${toJson(row.capabilities)}, ${toJson(row.context)}, ${row.revocation},
241
+ ${row.createdBy}, ${at(row.createdAt)}, ${secret.ciphertext},
242
+ ${atOrNull(secret.expiresAt)}, ${at(secret.rotatedAt)}, ${stamp}
243
+ ) ON CONFLICT (id) DO NOTHING RETURNING id
244
+ `)[0] === void 0 ? yield* invalid(`Authorization ${authorization.id} already exists`, "id") : authorization;
245
+ }));
246
+ })).pipe(guard("authorizations.upsert")),
247
+ get: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
248
+ SELECT * FROM ${authorizations} WHERE id = ${id} AND owner_id = ${ownerId}
249
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("authorization", id) : authorizationOf(rows[0])))).pipe(guard("authorizations.get")),
250
+ credential: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
251
+ SELECT * FROM ${authorizations} WHERE id = ${id} AND owner_id = ${ownerId}
252
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("authorization", id) : credentialOf(rows[0])))).pipe(guard("authorizations.credential")),
253
+ rotate: (id, credential) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
254
+ const secret = yield* credentialCodec.write(credential);
255
+ if ((yield* sql`
256
+ UPDATE ${authorizations} SET
257
+ credential_ciphertext = ${secret.ciphertext},
258
+ credential_expires_at = ${atOrNull(secret.expiresAt)},
259
+ credential_rotated_at = ${at(secret.rotatedAt)},
260
+ updated_at = ${yield* now}
261
+ WHERE id = ${id} AND owner_id = ${ownerId} RETURNING id
262
+ `)[0] === void 0) return yield* notFound("authorization", id);
263
+ })).pipe(guard("authorizations.rotate")),
264
+ promoteCapabilities: (id, capabilities) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
265
+ const current = yield* authorizationOf(yield* lockedAuthorization(ownerId, id));
266
+ const merged = [.../* @__PURE__ */ new Set([...current.capabilities, ...capabilities])];
267
+ yield* sql`
268
+ UPDATE ${authorizations}
269
+ SET capabilities = ${toJson(merged)}, updated_at = ${yield* now}
270
+ WHERE id = ${id} AND owner_id = ${ownerId}
271
+ `;
272
+ }))).pipe(guard("authorizations.promoteCapabilities")),
273
+ revoke: (id, revoke) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
274
+ const ciphertext = yield* prepareRevocation(ownerId, id).pipe(guard("authorizations.revoke"));
275
+ yield* revoke;
276
+ yield* completeRevocation(ownerId, id, ciphertext).pipe(guard("authorizations.completeRevocation"));
277
+ })),
278
+ recoverRevocations: (revoke) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
279
+ const rows = yield* sql`
280
+ SELECT * FROM ${authorizations}
281
+ WHERE owner_id = ${ownerId} AND revocation = 'pending'
282
+ ORDER BY created_at
283
+ `.pipe(guard("authorizations.pendingRevocations"));
284
+ yield* Effect.forEach(rows, (row) => Effect.gen(function* () {
285
+ yield* revoke(yield* authorizationOf(row));
286
+ yield* completeRevocation(ownerId, row.id, row.credential_ciphertext).pipe(guard("authorizations.completeRevocation"));
287
+ }));
288
+ return rows.length;
289
+ }))
290
+ },
291
+ connections: {
292
+ create: (authorizationId) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
293
+ yield* lockedAuthorization(ownerId, authorizationId);
294
+ const row = new Storage.Connection({
295
+ id: yield* fresh("conn"),
296
+ ownerId,
297
+ authorizationId,
298
+ createdAt: yield* DateTime.now
299
+ });
300
+ const encoded = yield* connectionCodec.write(row);
301
+ yield* sql`
302
+ INSERT INTO ${connections} (id, owner_id, authorization_id, created_at)
303
+ VALUES (${encoded.id}, ${encoded.ownerId}, ${encoded.authorizationId},
304
+ ${at(encoded.createdAt)})
305
+ `;
306
+ return row;
307
+ }))).pipe(guard("connections.create")),
308
+ get: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.flatMap(requireConnection(ownerId, id), connectionOf)).pipe(guard("connections.get")),
309
+ list: (filter) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.flatMap(filter?.provider === void 0 ? sql`
310
+ SELECT * FROM ${connections} WHERE owner_id = ${ownerId} ORDER BY created_at
311
+ ` : sql`
312
+ SELECT c.* FROM ${connections} AS c
313
+ JOIN ${authorizations} AS a ON a.id = c.authorization_id
314
+ WHERE c.owner_id = ${ownerId} AND a.provider = ${filter.provider}
315
+ ORDER BY c.created_at
316
+ `, (rows) => Effect.forEach(rows, connectionOf))).pipe(guard("connections.list")),
317
+ remove: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
318
+ if ((yield* sql`
319
+ SELECT * FROM ${connections}
320
+ WHERE id = ${id} AND owner_id = ${ownerId} FOR UPDATE
321
+ `)[0] === void 0) return yield* notFound("connection", id);
322
+ if ((yield* sql`
323
+ SELECT id FROM ${attachments}
324
+ WHERE connection_id = ${id} AND owner_id = ${ownerId} LIMIT 1
325
+ `)[0] !== void 0) return yield* invalid(`Connection ${id} still has attachments`, "connectionId");
326
+ yield* sql`DELETE FROM ${connections} WHERE id = ${id} AND owner_id = ${ownerId}`;
327
+ }))).pipe(guard("connections.remove"))
328
+ },
329
+ attachments: {
330
+ create: (input) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
331
+ yield* requireConnection(ownerId, input.connectionId);
332
+ const row = new Storage.Attachment({
333
+ id: yield* fresh("att"),
334
+ ownerId,
335
+ connectionId: input.connectionId,
336
+ domain: input.domain,
337
+ zone: input.zone,
338
+ target: input.target,
339
+ createdAt: yield* DateTime.now
340
+ });
341
+ const encoded = yield* attachmentCodec.write(row);
342
+ return (yield* sql`
343
+ INSERT INTO ${attachments} (
344
+ id, owner_id, connection_id, domain, zone, target, created_at
345
+ ) VALUES (
346
+ ${encoded.id}, ${encoded.ownerId}, ${encoded.connectionId}, ${encoded.domain},
347
+ ${encoded.zone}, ${toJson(encoded.target)}, ${at(encoded.createdAt)}
348
+ ) ON CONFLICT (owner_id, domain) DO NOTHING RETURNING id
349
+ `)[0] === void 0 ? yield* invalid(`${input.domain} is already attached`, "domain") : row;
350
+ }))).pipe(guard("attachments.create")),
351
+ get: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.flatMap(requireAttachment(ownerId, id), attachmentOf)).pipe(guard("attachments.get")),
352
+ byDomain: (domain) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
353
+ SELECT * FROM ${attachments} WHERE owner_id = ${ownerId} AND domain = ${domain}
354
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? Effect.succeedNone : Effect.map(attachmentOf(rows[0]), Option.some)))).pipe(guard("attachments.byDomain")),
355
+ list: (connectionId) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
356
+ yield* requireConnection(ownerId, connectionId);
357
+ const rows = yield* sql`
358
+ SELECT * FROM ${attachments}
359
+ WHERE owner_id = ${ownerId} AND connection_id = ${connectionId}
360
+ ORDER BY created_at
361
+ `;
362
+ return yield* Effect.forEach(rows, attachmentOf);
363
+ })).pipe(guard("attachments.list")),
364
+ remove: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
365
+ yield* requireAttachment(ownerId, id);
366
+ yield* sql`
367
+ UPDATE ${readiness} SET attachment_id = NULL
368
+ WHERE attachment_id = ${id} AND owner_id = ${ownerId}
369
+ `;
370
+ yield* sql`DELETE FROM ${attachments} WHERE id = ${id} AND owner_id = ${ownerId}`;
371
+ }))).pipe(guard("attachments.remove"))
372
+ },
373
+ continuations: {
374
+ put: (continuation) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
375
+ if (continuation.ownerId !== ownerId) return yield* invalid("Continuation owner does not match the principal", "ownerId");
376
+ const encoded = yield* continuationCodec.write(continuation);
377
+ yield* sql`
378
+ INSERT INTO ${continuations} (
379
+ id, owner_id, actor_id, provider, payload, return_to, expires_at
380
+ ) VALUES (
381
+ ${encoded.id}, ${encoded.ownerId}, ${encoded.actorId}, ${encoded.provider},
382
+ ${toJson(encoded.payload)}, ${encoded.returnTo}, ${at(encoded.expiresAt)}
383
+ ) ON CONFLICT (id) DO UPDATE SET
384
+ owner_id = EXCLUDED.owner_id,
385
+ actor_id = EXCLUDED.actor_id,
386
+ provider = EXCLUDED.provider,
387
+ payload = EXCLUDED.payload,
388
+ return_to = EXCLUDED.return_to,
389
+ expires_at = EXCLUDED.expires_at
390
+ `;
391
+ })).pipe(guard("continuations.put")),
392
+ get: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
393
+ const rows = yield* sql`
394
+ SELECT * FROM ${continuations} WHERE id = ${id} AND owner_id = ${ownerId}
395
+ `;
396
+ if (rows[0] === void 0) return yield* notFound("continuation", id);
397
+ const continuation = yield* continuationOf(rows[0]);
398
+ const instant = yield* DateTime.now;
399
+ return DateTime.toEpochMillis(continuation.expiresAt) <= DateTime.toEpochMillis(instant) ? yield* fail(new Reason.Expired({
400
+ entity: "continuation",
401
+ id
402
+ })) : continuation;
403
+ })).pipe(guard("continuations.get")),
404
+ consume: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
405
+ const rows = yield* sql`
406
+ DELETE FROM ${continuations}
407
+ WHERE id = ${id} AND owner_id = ${ownerId} RETURNING *
408
+ `;
409
+ if (rows[0] === void 0) return yield* notFound("continuation", id);
410
+ const continuation = yield* continuationOf(rows[0]);
411
+ const instant = yield* DateTime.now;
412
+ return DateTime.toEpochMillis(continuation.expiresAt) <= DateTime.toEpochMillis(instant) ? yield* fail(new Reason.Expired({
413
+ entity: "continuation",
414
+ id
415
+ })) : continuation;
416
+ })).pipe(guard("continuations.consume"))
417
+ },
418
+ attempts: {
419
+ create: (attempt) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
420
+ if (attempt.ownerId !== ownerId) return yield* invalid("Attempt owner does not match the principal", "ownerId");
421
+ const encoded = yield* attemptCodec.write(attempt);
422
+ return (yield* sql`
423
+ INSERT INTO ${attempts} (
424
+ id, owner_id, attachment_id, kind, status, plan, approval, approval_id,
425
+ receipt, receipt_id, rejection, source_receipt_id, lease_expires_at, failure,
426
+ plan_created_at, updated_at
427
+ ) VALUES (
428
+ ${encoded.id}, ${encoded.ownerId}, ${encoded.attachmentId}, ${encoded.kind},
429
+ ${encoded.status}, ${toJson(encoded.plan)}, ${toJsonOrNull(encoded.approval)},
430
+ ${attempt.approval?.id ?? null}, ${toJsonOrNull(encoded.receipt)},
431
+ ${attempt.receipt?.id ?? null}, ${toJsonOrNull(encoded.rejection)},
432
+ ${encoded.sourceReceiptId},
433
+ ${atOrNull(encoded.leaseExpiresAt)}, ${encoded.failure},
434
+ ${at(encoded.plan.createdAt)}, ${at(encoded.updatedAt)}
435
+ ) ON CONFLICT (id) DO NOTHING RETURNING id
436
+ `)[0] === void 0 ? yield* invalid(`Plan ${attempt.id} already exists`, "id") : attempt;
437
+ })).pipe(guard("attempts.create")),
438
+ get: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
439
+ SELECT * FROM ${attempts} WHERE id = ${id} AND owner_id = ${ownerId}
440
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("plan", id) : attemptOf(rows[0])))).pipe(guard("attempts.get")),
441
+ byApproval: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
442
+ SELECT * FROM ${attempts} WHERE approval_id = ${id} AND owner_id = ${ownerId}
443
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("approval", id) : attemptOf(rows[0])))).pipe(guard("attempts.byApproval")),
444
+ byReceipt: (id) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
445
+ SELECT * FROM ${attempts} WHERE receipt_id = ${id} AND owner_id = ${ownerId}
446
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? notFound("receipt", id) : attemptOf(rows[0])))).pipe(guard("attempts.byReceipt")),
447
+ latest: (attachmentId, kind) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
448
+ SELECT * FROM ${attempts}
449
+ WHERE owner_id = ${ownerId} AND attachment_id = ${attachmentId} AND kind = ${kind}
450
+ ORDER BY plan_created_at DESC, updated_at DESC LIMIT 1
451
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? Effect.succeedNone : Effect.map(attemptOf(rows[0]), Option.some)))).pipe(guard("attempts.latest")),
452
+ approve: (id, approval) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
453
+ const current = yield* lockedAttempt(ownerId, id);
454
+ if (current.approval?.id === approval.id) return current;
455
+ if (current.status !== "planned") return yield* stale(current);
456
+ const next = new Storage.Attempt({
457
+ ...current,
458
+ status: "approved",
459
+ approval,
460
+ updatedAt: yield* DateTime.now
461
+ });
462
+ const encoded = yield* attemptCodec.write(next);
463
+ yield* sql`
464
+ UPDATE ${attempts} SET
465
+ status = ${encoded.status},
466
+ approval = ${toJsonOrNull(encoded.approval)},
467
+ approval_id = ${approval.id},
468
+ updated_at = ${at(encoded.updatedAt)}
469
+ WHERE id = ${id} AND owner_id = ${ownerId}
470
+ `;
471
+ return next;
472
+ }))).pipe(guard("attempts.approve")),
473
+ reject: (id, input) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
474
+ const current = yield* lockedAttempt(ownerId, id);
475
+ if (current.status === "rejected") return current;
476
+ if (current.status === "expired") return yield* fail(new Reason.Expired({
477
+ entity: "plan",
478
+ id
479
+ }));
480
+ if (current.status !== "planned" || current.plan.digest !== input.digest) return yield* stale(current);
481
+ const instant = yield* DateTime.now;
482
+ const next = new Storage.Attempt({
483
+ ...current,
484
+ status: "rejected",
485
+ rejection: {
486
+ actorId: input.actorId,
487
+ reason: input.reason,
488
+ at: instant
489
+ },
490
+ updatedAt: instant
491
+ });
492
+ const encoded = yield* attemptCodec.write(next);
493
+ yield* sql`
494
+ UPDATE ${attempts} SET
495
+ status = ${encoded.status},
496
+ rejection = ${toJsonOrNull(encoded.rejection)},
497
+ updated_at = ${at(encoded.updatedAt)}
498
+ WHERE id = ${id} AND owner_id = ${ownerId}
499
+ `;
500
+ return next;
501
+ }))).pipe(guard("attempts.reject")),
502
+ claim: (id, lease) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
503
+ const current = yield* lockedAttempt(ownerId, id);
504
+ const instant = yield* DateTime.now;
505
+ if (current.status === "applying") {
506
+ if (current.leaseExpiresAt !== null && DateTime.toEpochMillis(current.leaseExpiresAt) > DateTime.toEpochMillis(instant)) return yield* busy(`apply:${id}`);
507
+ } else if (current.status !== "approved" && current.status !== "failed") return yield* stale(current);
508
+ const next = new Storage.Attempt({
509
+ ...current,
510
+ status: "applying",
511
+ leaseExpiresAt: lease,
512
+ failure: null,
513
+ updatedAt: instant
514
+ });
515
+ const encoded = yield* attemptCodec.write(next);
516
+ yield* sql`
517
+ UPDATE ${attempts} SET
518
+ status = ${encoded.status},
519
+ lease_expires_at = ${atOrNull(encoded.leaseExpiresAt)},
520
+ failure = NULL,
521
+ updated_at = ${at(encoded.updatedAt)}
522
+ WHERE id = ${id} AND owner_id = ${ownerId}
523
+ `;
524
+ return next;
525
+ }))).pipe(guard("attempts.claim")),
526
+ complete: (id, receipt) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
527
+ const current = yield* lockedAttempt(ownerId, id);
528
+ if (current.status !== "applying") return yield* stale(current);
529
+ const next = new Storage.Attempt({
530
+ ...current,
531
+ status: receipt.status,
532
+ receipt,
533
+ leaseExpiresAt: null,
534
+ updatedAt: yield* DateTime.now
535
+ });
536
+ const encoded = yield* attemptCodec.write(next);
537
+ yield* sql`
538
+ UPDATE ${attempts} SET
539
+ status = ${encoded.status},
540
+ receipt = ${toJsonOrNull(encoded.receipt)},
541
+ receipt_id = ${receipt.id},
542
+ lease_expires_at = NULL,
543
+ updated_at = ${at(encoded.updatedAt)}
544
+ WHERE id = ${id} AND owner_id = ${ownerId}
545
+ `;
546
+ return next;
547
+ }))).pipe(guard("attempts.complete")),
548
+ fail: (id, message) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql.withTransaction(Effect.gen(function* () {
549
+ const current = yield* lockedAttempt(ownerId, id);
550
+ if (current.status !== "applying") return yield* stale(current);
551
+ const next = new Storage.Attempt({
552
+ ...current,
553
+ status: "failed",
554
+ failure: message,
555
+ leaseExpiresAt: null,
556
+ updatedAt: yield* DateTime.now
557
+ });
558
+ const encoded = yield* attemptCodec.write(next);
559
+ yield* sql`
560
+ UPDATE ${attempts} SET
561
+ status = ${encoded.status},
562
+ failure = ${message},
563
+ lease_expires_at = NULL,
564
+ updated_at = ${at(encoded.updatedAt)}
565
+ WHERE id = ${id} AND owner_id = ${ownerId}
566
+ `;
567
+ return next;
568
+ }))).pipe(guard("attempts.fail"))
569
+ },
570
+ readiness: {
571
+ put: (row) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.gen(function* () {
572
+ if (row.ownerId !== ownerId) return yield* invalid("Readiness owner does not match the principal", "ownerId");
573
+ if (row.attachmentId !== null) yield* requireAttachment(ownerId, row.attachmentId);
574
+ const encoded = yield* readinessCodec.write(row);
575
+ yield* sql`
576
+ INSERT INTO ${readiness} (
577
+ owner_id, domain, attachment_id, overall, requirements, host, pending_since,
578
+ checked_at, next_check_at
579
+ ) VALUES (
580
+ ${encoded.ownerId}, ${encoded.domain}, ${encoded.attachmentId},
581
+ ${encoded.overall}, ${toJson(encoded.requirements)}, ${toJson(encoded.host)},
582
+ ${atOrNull(encoded.pendingSince)}, ${at(encoded.checkedAt)},
583
+ ${atOrNull(encoded.nextCheckAt)}
584
+ ) ON CONFLICT (owner_id, domain) DO UPDATE SET
585
+ attachment_id = EXCLUDED.attachment_id,
586
+ overall = EXCLUDED.overall,
587
+ requirements = EXCLUDED.requirements,
588
+ host = EXCLUDED.host,
589
+ pending_since = EXCLUDED.pending_since,
590
+ checked_at = EXCLUDED.checked_at,
591
+ next_check_at = EXCLUDED.next_check_at
592
+ `;
593
+ })).pipe(guard("readiness.put")),
594
+ get: (domain) => Effect.flatMap(Principal.Service, ({ ownerId }) => sql`
595
+ SELECT * FROM ${readiness}
596
+ WHERE owner_id = ${ownerId} AND domain = ${domain}
597
+ `.pipe(Effect.flatMap((rows) => rows[0] === void 0 ? Effect.succeedNone : Effect.map(readinessOf(rows[0]), Option.some)))).pipe(guard("readiness.get"))
598
+ },
599
+ withLock: (key, effect) => Effect.flatMap(Principal.Service, ({ ownerId }) => Effect.scoped(Effect.gen(function* () {
600
+ const connection = yield* Effect.mapError(sql.reserve, (error) => storageFailed("withLock", sqlMessage(error)));
601
+ const scoped = `${ownerId}:${key}`;
602
+ yield* Effect.acquireRelease(connection.execute("SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS acquired", [scoped], void 0).pipe(Effect.mapError((error) => storageFailed("withLock", sqlMessage(error))), Effect.flatMap((rows) => rows[0]?.acquired === true ? Effect.void : busy(key))), () => connection.execute("SELECT pg_advisory_unlock(hashtextextended($1, 0))", [scoped], void 0).pipe(Effect.ignore));
603
+ return yield* effect;
604
+ })))
605
+ };
404
606
  });
405
- const migration = makeMigration({
406
- id: 1,
407
- name: "adopt-authorization-lifecycle",
408
- risk: "additive",
409
- providers: { Postgres: sqlMigrationBody([
410
- `CREATE TABLE IF NOT EXISTS domain_provider_authorizations (
411
- id TEXT PRIMARY KEY NOT NULL,
412
- provider_id TEXT NOT NULL,
413
- account_id TEXT,
414
- subject_id TEXT NOT NULL,
415
- kind TEXT NOT NULL,
416
- capabilities JSONB NOT NULL,
417
- capability_evidence JSONB NOT NULL,
418
- scopes JSONB NOT NULL,
419
- provider_context JSONB NOT NULL,
420
- revocation JSONB NOT NULL,
421
- credential_ciphertext TEXT,
422
- expires_at TIMESTAMPTZ,
423
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
424
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
425
- )`,
426
- `ALTER TABLE domain_provider_authorizations
427
- DROP CONSTRAINT IF EXISTS uq_domain_provider_authorizations_provider_account`,
428
- `ALTER TABLE domain_provider_authorizations ALTER COLUMN account_id DROP NOT NULL`,
429
- `CREATE TABLE IF NOT EXISTS organization_domain_provider_connections (
430
- id TEXT PRIMARY KEY NOT NULL,
431
- organization_id TEXT NOT NULL,
432
- authorization_id TEXT NOT NULL,
433
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
434
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
435
- CONSTRAINT uq_organization_domain_provider_connections_org_authorization
436
- UNIQUE (organization_id, authorization_id),
437
- CONSTRAINT uq_organization_domain_provider_connections_id_org
438
- UNIQUE (id, organization_id)
439
- )`,
440
- `CREATE INDEX IF NOT EXISTS idx_organization_domain_provider_connections_authorization
441
- ON organization_domain_provider_connections (authorization_id)`,
442
- `DO $$ BEGIN
443
- IF to_regclass('domain_connections') IS NOT NULL THEN
444
- INSERT INTO organization_domain_provider_connections (
445
- id, organization_id, authorization_id, created_at, updated_at
446
- ) SELECT id, organization_id, authorization_id, created_at, updated_at
447
- FROM domain_connections ON CONFLICT DO NOTHING;
448
- END IF;
449
- END $$`,
450
- `CREATE TABLE IF NOT EXISTS domain_provider_attachments (
451
- id TEXT PRIMARY KEY NOT NULL,
452
- organization_id TEXT NOT NULL,
453
- connection_id TEXT NOT NULL,
454
- domain_id TEXT NOT NULL,
455
- provider_account_id TEXT NOT NULL,
456
- provider_zone_id TEXT NOT NULL,
457
- provider_zone_name TEXT NOT NULL,
458
- provider_target_context JSONB NOT NULL,
459
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
460
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
461
- CONSTRAINT uq_domain_provider_attachments_org_domain UNIQUE (organization_id, domain_id),
462
- CONSTRAINT uq_domain_provider_attachments_connection_domain UNIQUE (connection_id, domain_id)
463
- )`,
464
- `CREATE INDEX IF NOT EXISTS idx_domain_provider_attachments_connection
465
- ON domain_provider_attachments (connection_id)`,
466
- `DO $$ BEGIN
467
- IF EXISTS (SELECT 1 FROM domain_provider_authorizations WHERE credential_ciphertext IS NULL)
468
- THEN RAISE EXCEPTION 'DomainKit adoption requires encrypted credential ciphertext';
469
- END IF;
470
- IF EXISTS (
471
- SELECT 1 FROM domain_provider_attachments
472
- WHERE provider_target_context #>> '{value,domain}' IS NULL
473
- ) THEN RAISE EXCEPTION 'DomainKit adoption requires semantic attachment domains';
474
- END IF;
475
- END $$`
476
- ]) }
607
+ //#endregion
608
+ //#region src/internal/tables.ts
609
+ /**
610
+ * The six tables DomainKit owns, declared once and rendered per dialect by CapsuleDB.
611
+ *
612
+ * Every table carries `owner_id` and every query filters by it: tenancy is a column, not a schema.
613
+ * There are no foreign keys to host tables, and none between DomainKit's own tables either, so a
614
+ * host can add its own in the emitted migration without fighting a constraint the package owns.
615
+ */
616
+ /** Default table prefix. Part of the physical layout, so it is immutable after the first deploy. */
617
+ const DEFAULT_PREFIX = "domainkit";
618
+ /**
619
+ * Declare the six tables under `prefix`.
620
+ *
621
+ * `Schema.table` validates every identifier it will quote, so an invalid prefix throws
622
+ * `CapsuleDefinitionError` here rather than reaching a renderer.
623
+ */
624
+ const make$1 = (prefix) => ({
625
+ authorizations: Schema.table(`${prefix}_authorizations`, {
626
+ columns: {
627
+ id: Schema.text(),
628
+ owner_id: Schema.text(),
629
+ provider: Schema.text(),
630
+ method: Schema.text(),
631
+ capabilities: Schema.json(),
632
+ context: Schema.json(),
633
+ revocation: Schema.text(),
634
+ created_by: Schema.text(),
635
+ created_at: Schema.timestamp(),
636
+ credential_ciphertext: Schema.text(),
637
+ credential_expires_at: Schema.timestamp({ nullable: true }),
638
+ credential_rotated_at: Schema.timestamp(),
639
+ updated_at: Schema.timestamp()
640
+ },
641
+ primaryKey: ["id"],
642
+ indexes: [{ columns: ["owner_id", "revocation"] }, { columns: ["owner_id", "provider"] }]
643
+ }),
644
+ connections: Schema.table(`${prefix}_connections`, {
645
+ columns: {
646
+ id: Schema.text(),
647
+ owner_id: Schema.text(),
648
+ authorization_id: Schema.text(),
649
+ created_at: Schema.timestamp()
650
+ },
651
+ primaryKey: ["id"],
652
+ indexes: [{ columns: ["owner_id"] }, { columns: ["authorization_id"] }]
653
+ }),
654
+ attachments: Schema.table(`${prefix}_attachments`, {
655
+ columns: {
656
+ id: Schema.text(),
657
+ owner_id: Schema.text(),
658
+ connection_id: Schema.text(),
659
+ domain: Schema.text(),
660
+ zone: Schema.text(),
661
+ target: Schema.json(),
662
+ created_at: Schema.timestamp()
663
+ },
664
+ primaryKey: ["id"],
665
+ uniques: [["owner_id", "domain"]],
666
+ indexes: [{ columns: ["owner_id", "connection_id"] }]
667
+ }),
668
+ continuations: Schema.table(`${prefix}_continuations`, {
669
+ columns: {
670
+ id: Schema.text(),
671
+ owner_id: Schema.text(),
672
+ actor_id: Schema.text(),
673
+ provider: Schema.text(),
674
+ payload: Schema.json(),
675
+ return_to: Schema.text({ nullable: true }),
676
+ expires_at: Schema.timestamp()
677
+ },
678
+ primaryKey: ["id"],
679
+ indexes: [{ columns: ["expires_at"] }]
680
+ }),
681
+ attempts: Schema.table(`${prefix}_attempts`, {
682
+ columns: {
683
+ id: Schema.text(),
684
+ owner_id: Schema.text(),
685
+ attachment_id: Schema.text(),
686
+ kind: Schema.text(),
687
+ status: Schema.text(),
688
+ plan: Schema.json(),
689
+ approval: Schema.json({ nullable: true }),
690
+ /** Denormalized from `approval` so `byApproval` is an index lookup. */
691
+ approval_id: Schema.text({ nullable: true }),
692
+ receipt: Schema.json({ nullable: true }),
693
+ /** Denormalized from `receipt` so `byReceipt` is an index lookup. */
694
+ receipt_id: Schema.text({ nullable: true }),
695
+ /** Who declined the plan, why, and when; set when an attempt reaches `rejected`. */
696
+ rejection: Schema.json({ nullable: true }),
697
+ source_receipt_id: Schema.text({ nullable: true }),
698
+ lease_expires_at: Schema.timestamp({ nullable: true }),
699
+ failure: Schema.text({ nullable: true }),
700
+ /** The plan's own `createdAt`; `latest` orders by it. */
701
+ plan_created_at: Schema.timestamp(),
702
+ updated_at: Schema.timestamp()
703
+ },
704
+ primaryKey: ["id"],
705
+ indexes: [
706
+ { columns: [
707
+ "owner_id",
708
+ "attachment_id",
709
+ "kind"
710
+ ] },
711
+ { columns: ["owner_id", "approval_id"] },
712
+ { columns: ["owner_id", "receipt_id"] }
713
+ ]
714
+ }),
715
+ readiness: Schema.table(`${prefix}_readiness`, {
716
+ columns: {
717
+ owner_id: Schema.text(),
718
+ domain: Schema.text(),
719
+ /** Set while an attachment covers the domain; observe-only readiness leaves it null. */
720
+ attachment_id: Schema.text({ nullable: true }),
721
+ overall: Schema.text(),
722
+ requirements: Schema.json(),
723
+ host: Schema.json(),
724
+ pending_since: Schema.timestamp({ nullable: true }),
725
+ checked_at: Schema.timestamp(),
726
+ next_check_at: Schema.timestamp({ nullable: true })
727
+ },
728
+ primaryKey: ["owner_id", "domain"],
729
+ indexes: [{ columns: ["owner_id", "next_check_at"] }, { columns: ["attachment_id"] }]
730
+ })
477
731
  });
478
- /** PostgreSQL-only authorization lifecycle capsule. The host must prepare its registry at startup. */
479
- const capsule = Effect.gen(function* () {
480
- const definedMigration = yield* migration;
481
- return yield* makeCapsule({
482
- id: "domainkit.authorization-lifecycle",
483
- migrations: [definedMigration],
484
- layer: Layer.effect(ManagedDnsConnections.Service, makeRepository)
732
+ const list = (tables) => [
733
+ tables.authorizations,
734
+ tables.connections,
735
+ tables.attachments,
736
+ tables.continuations,
737
+ tables.attempts,
738
+ tables.readiness
739
+ ];
740
+ //#endregion
741
+ //#region src/capsule.ts
742
+ /**
743
+ * DomainKit's CapsuleDB capsule: six tables and one `Storage.Service` implementation.
744
+ *
745
+ * The capsule is a plain value, so a host imports it, points `capsuledb emit` at it, or hands it
746
+ * to `Registry.layer` without running an Effect first.
747
+ */
748
+ /**
749
+ * Build the capsule for a table prefix.
750
+ *
751
+ * The prefix is part of the physical layout: it changes the rendered DDL and therefore the
752
+ * migration checksums, so a deployed registry must keep the prefix it was created with.
753
+ */
754
+ const make = (prefix = DEFAULT_PREFIX) => {
755
+ const tables = make$1(prefix);
756
+ return Capsule.make({
757
+ id: "domainkit.storage",
758
+ tables: list(tables),
759
+ migrations: [Migration.make({
760
+ id: 1,
761
+ name: "create-storage",
762
+ risk: "additive",
763
+ steps: list(tables).map(Migration.createTable)
764
+ })],
765
+ layer: Layer.effect(Storage.Service)(make$2(tables))
485
766
  });
767
+ };
768
+ /** The capsule under the default `domainkit` prefix. `capsuledb emit --export capsule` reads this. */
769
+ const capsule = make();
770
+ //#endregion
771
+ //#region src/PgStorage.ts
772
+ /**
773
+ * The layer a host installs: DomainKit's `Storage` on the host's own PostgreSQL client.
774
+ *
775
+ * `layer()` runs the capsule through CapsuleDB's registry, which creates the ledger, applies
776
+ * pending migrations, and only then provides `Storage`. A host that owns its migration pipeline
777
+ * applies `capsuledb emit` output instead and boots with `mode: "assert"`, which touches no schema
778
+ * and fails unless the database already matches the capsule.
779
+ *
780
+ * The layer requires only `SqlClient`: credentials arrive sealed, so Storage never needs `Custody`.
781
+ */
782
+ var PgStorage_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
783
+ const layer = (options = {}) => Registry.layer({
784
+ provider: Pg.profile,
785
+ capsules: [options.prefix === void 0 || options.prefix === "domainkit" ? capsule : make(options.prefix)],
786
+ ...options.mode === void 0 ? {} : { mode: options.mode },
787
+ ...options.registryPrefix === void 0 ? {} : { prefix: options.registryPrefix }
486
788
  });
487
789
  //#endregion
488
790
  //#region src/index.ts
791
+ /**
792
+ * DomainKit's `Storage` on PostgreSQL, as one declarative CapsuleDB capsule.
793
+ *
794
+ * Hosts install `PgStorage.layer()` under `DomainKit.layer` and supply their own `SqlClient`. The
795
+ * `capsule` value is the same declaration `capsuledb emit` reads to write SQL for a host-owned
796
+ * migration pipeline.
797
+ */
489
798
  const VERSION = version;
490
799
  //#endregion
491
- export { custody_exports as CredentialCustody, host_bindings_exports as HostBindings, VERSION, capsule };
800
+ export { PgStorage_exports as PgStorage, VERSION, capsule };
492
801
 
493
802
  //# sourceMappingURL=index.mjs.map