@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/README.md +109 -7
- package/dist/index.d.mts +20 -51
- package/dist/index.mjs +776 -467
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,11 +1,113 @@
|
|
|
1
1
|
# `@domainkit/capsuledb`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DomainKit's `Storage` on PostgreSQL, as one declarative [CapsuleDB](https://github.com/aryasaatvik/CapsuleDB)
|
|
4
|
+
capsule. Install it under `DomainKit.layer` and the whole durable lifecycle — provider
|
|
5
|
+
authorizations, connections, attachments, interactive-flow continuations, plan/approval/receipt
|
|
6
|
+
attempts, and observed readiness — lives in your database, scoped to your tenants.
|
|
4
7
|
|
|
5
|
-
The host owns and
|
|
6
|
-
|
|
7
|
-
Effect services. It does not export tables, rows, queries, an ORM adapter, or a Promise-first API.
|
|
8
|
+
The host owns the `SqlClient` and its lifetime. This package owns its own tables and never exposes
|
|
9
|
+
rows, queries, or a raw client.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add @domainkit/capsuledb capsuledb @effect/sql-pg
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Wire it up
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Config, Layer } from "effect";
|
|
21
|
+
import { PgClient } from "@effect/sql-pg";
|
|
22
|
+
import { Cloudflare, Custody, DomainKit, Vercel } from "domainkit";
|
|
23
|
+
import { PgStorage } from "@domainkit/capsuledb";
|
|
24
|
+
|
|
25
|
+
export const DomainKitLive = DomainKit.layer({
|
|
26
|
+
providers: [
|
|
27
|
+
Cloudflare.provider({
|
|
28
|
+
oauth: {
|
|
29
|
+
clientId: Config.string("CF_CLIENT_ID"),
|
|
30
|
+
clientSecret: Config.redacted("CF_CLIENT_SECRET"),
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
Vercel.provider(),
|
|
34
|
+
],
|
|
35
|
+
}).pipe(
|
|
36
|
+
// `provideMerge`, not `provide`: `domainkit/server`'s handlers read attempts and receipts
|
|
37
|
+
// straight from Storage, so the layer they are given has to still carry it.
|
|
38
|
+
Layer.provideMerge(Layer.mergeAll(PgStorage.layer(), Custody.layerConfig())),
|
|
39
|
+
Layer.provide(PgClient.layerConfig({ url: Config.redacted("DATABASE_URL") })),
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`PgStorage.layer()` prepares at boot: it creates CapsuleDB's ledger, applies pending migrations, and
|
|
44
|
+
only then provides `Storage`. A capsule service can never observe a database whose tables are
|
|
45
|
+
missing.
|
|
46
|
+
|
|
47
|
+
`Custody.layerConfig()` reads `DOMAINKIT_CUSTODY_KEY` and seals every provider credential before it
|
|
48
|
+
reaches a row. Swap it for a KMS with `Custody.layerFromAsync`; there is no plaintext mode, and this
|
|
49
|
+
package never sees one.
|
|
50
|
+
|
|
51
|
+
## Or emit the SQL and assert at boot
|
|
52
|
+
|
|
53
|
+
If your migrations are yours to run, take the SQL instead:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
capsuledb emit \
|
|
57
|
+
--module ./node_modules/@domainkit/capsuledb/dist/index.mjs \
|
|
58
|
+
--export capsule \
|
|
59
|
+
--dialect postgres \
|
|
60
|
+
--out ./drizzle
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That writes CapsuleDB's ledger DDL, the capsule's migration, and a readiness row, plus a
|
|
64
|
+
`capsuledb.emit.json` index naming the files it owns. Apply them with your own pipeline, then boot
|
|
65
|
+
in assert mode:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
PgStorage.layer({ mode: "assert" });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Assert mode applies nothing and fails unless the database already matches the capsule, so a missed
|
|
72
|
+
migration is a boot failure rather than a runtime surprise. `capsuledb check` compares an emitted
|
|
73
|
+
folder against the current capsule in CI.
|
|
74
|
+
|
|
75
|
+
## Tables
|
|
76
|
+
|
|
77
|
+
| Table | Key | Holds |
|
|
78
|
+
| -------------------------- | --------------------------------- | ----------------------------------------------------------------- |
|
|
79
|
+
| `domainkit_authorizations` | `id` | provider grant, capabilities, revocation state, sealed credential |
|
|
80
|
+
| `domainkit_connections` | `id` | the principal-facing handle over one authorization |
|
|
81
|
+
| `domainkit_attachments` | `id`, unique `(owner_id, domain)` | domain, zone, provider target |
|
|
82
|
+
| `domainkit_continuations` | `id` | interactive-flow state with a TTL |
|
|
83
|
+
| `domainkit_attempts` | `id` | plan, approval, receipt, status, lease, failure |
|
|
84
|
+
| `domainkit_readiness` | `(owner_id, domain)` | latest observation, per-requirement evidence, backoff |
|
|
85
|
+
|
|
86
|
+
Readiness is keyed by domain rather than by attachment, so a host observing public DNS alone gets
|
|
87
|
+
the same row; `attachment_id` links the attachment when one exists and is cleared when it is
|
|
88
|
+
removed.
|
|
89
|
+
|
|
90
|
+
Every table carries `owner_id`, and every query filters by the `Principal.Service` your host provides, so a
|
|
91
|
+
row belonging to another tenant reads as absent. No foreign keys are declared, to your tables or
|
|
92
|
+
between these; add the ones you want in the emitted SQL.
|
|
93
|
+
|
|
94
|
+
`PgStorage.layer({ prefix })` renames the tables. The prefix is part of the physical layout: it
|
|
95
|
+
changes the rendered DDL and the migration checksum, so fix it before the first deploy and never
|
|
96
|
+
change it after. `registryPrefix` does the same for CapsuleDB's own ledger tables and must match
|
|
97
|
+
`capsuledb emit --prefix`.
|
|
98
|
+
|
|
99
|
+
## What it guarantees
|
|
100
|
+
|
|
101
|
+
- Aggregate transitions — approve, claim, complete, fail, capability promotion, credential
|
|
102
|
+
replacement — run in one transaction over a `FOR UPDATE` row.
|
|
103
|
+
- A continuation is consumed by `DELETE ... RETURNING`, so a replayed OAuth callback fails
|
|
104
|
+
`NotFound` instead of connecting twice.
|
|
105
|
+
- Revocation is two-phase: mark `pending`, call the provider outside the transaction, then delete
|
|
106
|
+
the row only while it still holds the credential that was revoked. A crash in between, or a
|
|
107
|
+
refresh that rotated the credential mid-revoke, leaves a row `recoverRevocations` finishes later,
|
|
108
|
+
so a newly issued credential is never orphaned at the provider.
|
|
109
|
+
- `withLock` takes a session advisory lock on a reserved connection and fails `Busy` rather than
|
|
110
|
+
waiting, so a credential refresh single-flights without holding a transaction across an HTTP call.
|
|
111
|
+
|
|
112
|
+
The package passes `Testing.conformance.storage` from `domainkit/testing`, the same suite the
|
|
113
|
+
in-memory implementation passes.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,60 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Capsule, Registry, Schema } from "capsuledb";
|
|
2
|
+
import { Storage } from "domainkit";
|
|
3
3
|
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
declare namespace PgStorage_d_exports {
|
|
6
|
+
export { Options, layer };
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
readonly
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
readonly
|
|
17
|
-
readonly seal: (credential: Credential) => Effect.Effect<string, Error$1>;
|
|
8
|
+
interface Options {
|
|
9
|
+
/** Table prefix. Default `domainkit`. Part of the physical layout; immutable after first deploy. */
|
|
10
|
+
readonly prefix?: string;
|
|
11
|
+
/** `prepare` (default) migrates at boot; `assert` expects the host applied `capsuledb emit` output. */
|
|
12
|
+
readonly mode?: "prepare" | "assert";
|
|
13
|
+
/**
|
|
14
|
+
* Prefix for CapsuleDB's own ledger tables; default `capsuledb`. Set it only to match a prefix a
|
|
15
|
+
* host already uses for other capsules, and pass the same value to `capsuledb emit --prefix`.
|
|
16
|
+
*/
|
|
17
|
+
readonly registryPrefix?: string;
|
|
18
18
|
}
|
|
19
|
-
declare const
|
|
20
|
-
/** Host-owned encryption boundary. DomainKit never receives keys or stores plaintext. */
|
|
21
|
-
declare class Service$1 extends Service_base$1 {}
|
|
22
|
-
declare namespace host_bindings_d_exports {
|
|
23
|
-
export { DomainReference, Error, Interface, OwnerReference, Service };
|
|
24
|
-
}
|
|
25
|
-
declare const Error_base: Schema.Class<Error, Schema.TaggedStruct<"HostBindingError", {
|
|
26
|
-
readonly message: Schema.String;
|
|
27
|
-
readonly operation: Schema.String;
|
|
28
|
-
readonly retry: Schema.Literals<readonly ["never", "after-user-action", "safe", "unknown"]>;
|
|
29
|
-
}>, import("effect/Cause").YieldableError>;
|
|
30
|
-
declare class Error extends Error_base {}
|
|
31
|
-
interface OwnerReference {
|
|
32
|
-
readonly ownerReference: string;
|
|
33
|
-
}
|
|
34
|
-
interface DomainReference extends OwnerReference {
|
|
35
|
-
readonly domainReference: string;
|
|
36
|
-
}
|
|
37
|
-
interface Interface {
|
|
38
|
-
/** Resolve a durable host owner reference without capturing request identity. */
|
|
39
|
-
readonly owner: (ownerId: string) => Effect.Effect<OwnerReference, Error>;
|
|
40
|
-
/** Resolve a host domain reference inside the caller's active transaction context. */
|
|
41
|
-
readonly domain: (input: {
|
|
42
|
-
readonly domain: string;
|
|
43
|
-
readonly ownerId: string;
|
|
44
|
-
}) => Effect.Effect<DomainReference, Error>;
|
|
45
|
-
/** Project an opaque stored owner reference back to DomainKit's semantic owner id. */
|
|
46
|
-
readonly ownerId: (ownerReference: string) => Effect.Effect<string, Error>;
|
|
47
|
-
}
|
|
48
|
-
declare const Service_base: Context.ServiceClass<Service, "@domainkit/capsuledb/HostBindings", Interface>;
|
|
49
|
-
/** Stateless host mapping for tenant/domain foreign-key references. */
|
|
50
|
-
declare class Service extends Service_base {}
|
|
19
|
+
declare const layer: (options?: Options) => Layer.Layer<Storage.Service, Registry.RegistryRuntimeError, SqlClient.SqlClient>;
|
|
51
20
|
//#endregion
|
|
52
|
-
//#region src/
|
|
53
|
-
/**
|
|
54
|
-
declare const capsule:
|
|
21
|
+
//#region src/capsule.d.ts
|
|
22
|
+
/** The capsule under the default `domainkit` prefix. `capsuledb emit --export capsule` reads this. */
|
|
23
|
+
declare const capsule: Capsule.Capsule<Storage.Service, never, SqlClient.SqlClient>;
|
|
55
24
|
//#endregion
|
|
56
25
|
//#region src/index.d.ts
|
|
57
26
|
declare const VERSION: string;
|
|
58
27
|
//#endregion
|
|
59
|
-
export {
|
|
28
|
+
export { PgStorage_d_exports as PgStorage, VERSION, capsule };
|
|
60
29
|
//# sourceMappingURL=index.d.mts.map
|