@byok-sdk/cloud-dataplane 0.4.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.
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/cleanup.d.ts +106 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +4284 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +54 -0
- package/dist/migrations-dir.d.ts +7 -0
- package/dist/pool.d.ts +39 -0
- package/dist/sql/0001_cloud_local.sql +184 -0
- package/dist/sql/0002_core_domain.sql +416 -0
- package/dist/sql/0003_cloud_cleanup.sql +97 -0
- package/dist/sql/0004_device_proof_truth.sql +39 -0
- package/dist/sql/0005_skill_packs.sql +78 -0
- package/dist/sql/0006_device_presence_toolsets.sql +18 -0
- package/dist/stores/core/board.d.ts +38 -0
- package/dist/stores/core/index.d.ts +37 -0
- package/dist/stores/core/mailbox-sequence.d.ts +10 -0
- package/dist/stores/core/mailbox.d.ts +32 -0
- package/dist/stores/core/objects.d.ts +49 -0
- package/dist/stores/core/presence.d.ts +34 -0
- package/dist/stores/core/quota.d.ts +56 -0
- package/dist/stores/core/skill-pack.d.ts +41 -0
- package/dist/stores/core/truth.d.ts +32 -0
- package/dist/stores/dedup.d.ts +22 -0
- package/dist/stores/devices.d.ts +23 -0
- package/dist/stores/index.d.ts +55 -0
- package/dist/stores/nonces.d.ts +26 -0
- package/dist/stores/pairing-codes.d.ts +26 -0
- package/dist/stores/proof-receipts.d.ts +12 -0
- package/dist/stores/r2-blobs.d.ts +249 -0
- package/dist/stores/receipts.d.ts +25 -0
- package/dist/stores/task-attempts.d.ts +36 -0
- package/dist/truth-committer.d.ts +15 -0
- package/package.json +59 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
/** One file on disk, already ordered and checksummed. */
|
|
3
|
+
export interface MigrationFile {
|
|
4
|
+
/** The filename, e.g. `0001_cloud_local.sql`. This is the ledger's `version`. */
|
|
5
|
+
readonly version: string;
|
|
6
|
+
/** The four-digit prefix as a number, used for ordering only. */
|
|
7
|
+
readonly ordinal: number;
|
|
8
|
+
readonly checksum: string;
|
|
9
|
+
readonly sql: string;
|
|
10
|
+
}
|
|
11
|
+
/** What a run did. `applied` is empty when the database was already up to date. */
|
|
12
|
+
export interface MigrationResult {
|
|
13
|
+
readonly applied: readonly string[];
|
|
14
|
+
readonly alreadyApplied: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A published migration file changed after it was applied.
|
|
18
|
+
*
|
|
19
|
+
* Fail-closed by design: the runner cannot know whether the edit was a harmless
|
|
20
|
+
* comment or a dropped column, and a deployment that guesses wrong corrupts a
|
|
21
|
+
* schema. The fix is a NEW migration file, never an edit to an old one.
|
|
22
|
+
*/
|
|
23
|
+
export declare class MigrationChecksumMismatchError extends Error {
|
|
24
|
+
readonly version: string;
|
|
25
|
+
readonly expectedChecksum: string;
|
|
26
|
+
readonly actualChecksum: string;
|
|
27
|
+
constructor(version: string, expectedChecksum: string, actualChecksum: string);
|
|
28
|
+
}
|
|
29
|
+
/** A file in the migration directory that the naming contract does not admit. */
|
|
30
|
+
export declare class MigrationFilenameError extends Error {
|
|
31
|
+
readonly filename: string;
|
|
32
|
+
constructor(filename: string, reason: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reads and orders the migration files in `directory`.
|
|
36
|
+
*
|
|
37
|
+
* Ordering is by the four-digit prefix, not by whatever order the filesystem
|
|
38
|
+
* hands back — `readdir` makes no promise, and on some filesystems it is
|
|
39
|
+
* effectively insertion order. A non-conforming filename or a duplicated prefix
|
|
40
|
+
* is an error rather than a skip: silently ignoring a `.sql` file someone
|
|
41
|
+
* dropped in this directory is how a migration goes missing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readMigrationFiles(directory: string): Promise<readonly MigrationFile[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Applies every pending migration in `directory`, in prefix order.
|
|
46
|
+
*
|
|
47
|
+
* @param pool The pool to migrate. One client is checked out for the whole run
|
|
48
|
+
* because the advisory lock is session-scoped.
|
|
49
|
+
* @param directory Absolute path to the migration directory (this repo's
|
|
50
|
+
* `deploy/sql/`). Required rather than defaulted: a published package cannot
|
|
51
|
+
* guess where its consumer keeps deployment assets, and guessing wrong would
|
|
52
|
+
* mean silently migrating nothing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function migrate(pool: Pool, directory: string): Promise<MigrationResult>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute path to the migration directory shipped inside this package, ready
|
|
3
|
+
* to hand to {@link migrate}. The directory exists in an installed package and
|
|
4
|
+
* in this repository after a build; it does not exist in an unbuilt checkout,
|
|
5
|
+
* where `deploy/sql/` is the thing to read.
|
|
6
|
+
*/
|
|
7
|
+
export declare function migrationsDir(): string;
|
package/dist/pool.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Pool, PoolClient, PoolConfig } from 'pg';
|
|
2
|
+
export interface ByokPoolOptions extends Omit<PoolConfig, 'types'> {
|
|
3
|
+
/** `postgres://user:password@host:port/database`. */
|
|
4
|
+
readonly connectionString: string;
|
|
5
|
+
/**
|
|
6
|
+
* Where an IDLE client's error goes. See {@link createByokPool} — a pool
|
|
7
|
+
* MUST carry an `'error'` listener or an idle-backend reset crashes the host.
|
|
8
|
+
* This is where the host takes over that policy (log to its own sink, page,
|
|
9
|
+
* increment a metric). It is NOT a `PoolConfig` field, so it is stripped
|
|
10
|
+
* before the config reaches `pg.Pool`, exactly as `types` is kept off the
|
|
11
|
+
* process-wide registry.
|
|
12
|
+
*
|
|
13
|
+
* Left unset, the pool still gets a listener — an observable default, not a
|
|
14
|
+
* silent swallow.
|
|
15
|
+
*/
|
|
16
|
+
readonly onPoolError?: (err: Error, client?: PoolClient) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a pool wired with the int8 parser above.
|
|
20
|
+
*
|
|
21
|
+
* The caller owns the pool's lifetime: this package never holds a module-level
|
|
22
|
+
* pool, because a process that composes two deployments (a migration runner and
|
|
23
|
+
* a serving path, say) has to be able to close one without breaking the other.
|
|
24
|
+
*
|
|
25
|
+
* The `'error'` listener is MANDATORY, not defensive. A `pg.Pool` emits
|
|
26
|
+
* `'error'` when a client that is sitting IDLE in the pool has its backend
|
|
27
|
+
* connection reset from under it — a failover, an operator
|
|
28
|
+
* `pg_terminate_backend`, an idle-timeout on a proxy, a network blip. There is
|
|
29
|
+
* no `await` in flight to reject at that moment, so `pg` surfaces it on the
|
|
30
|
+
* pool, and Node's rule for an `EventEmitter` `'error'` with no listener is to
|
|
31
|
+
* rethrow it as an uncaught exception — which terminates the whole host
|
|
32
|
+
* process. A bare `new pg.Pool(...)` therefore hands the host a latent crash on
|
|
33
|
+
* an event it cannot see coming and did not cause. The listener converts that
|
|
34
|
+
* into a handled, observable event: routed to the host's {@link
|
|
35
|
+
* ByokPoolOptions.onPoolError} when supplied, or to a labelled `console.error`
|
|
36
|
+
* so the reset is never invisible. It does not paper over live query errors —
|
|
37
|
+
* those still reject their own `await`; this is only the idle-client path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createByokPool(options: ByokPoolOptions): Pool;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
-- 0001_cloud_local.sql — the seven cloud-local port tables.
|
|
2
|
+
--
|
|
3
|
+
-- Frozen at merge. Migrations are forward-only (sprint S4A.6): a correction is
|
|
4
|
+
-- a NEW file, never an edit to this one, and the runner enforces that by
|
|
5
|
+
-- checksumming every applied file against its ledger row
|
|
6
|
+
-- (packages/cloud-postgres/src/migrate.ts).
|
|
7
|
+
--
|
|
8
|
+
-- Key design follows docs/researches/s4a-dataplane-design.md §5 and the
|
|
9
|
+
-- tenant-first discipline in docs/architecture/sdk-architecture.md §12.6.2.
|
|
10
|
+
-- The rule this file is written to obey:
|
|
11
|
+
--
|
|
12
|
+
-- EVERY unique index or constraint on a tenant-owned table starts with
|
|
13
|
+
-- tenant_id.
|
|
14
|
+
--
|
|
15
|
+
-- A naked unique key is what turns "look up by id, then check the tenant" into
|
|
16
|
+
-- a reachable code path; when the first column is tenant_id, a cross-tenant
|
|
17
|
+
-- read addresses a different key space and finds nothing rather than finding a
|
|
18
|
+
-- row it then has to be trusted to reject. Two exceptions are whitelisted
|
|
19
|
+
-- below, each justified at its table, and both of them are single-step
|
|
20
|
+
-- pre-tenant resolutions of a CLOUD-MINTED credential — never a two-step
|
|
21
|
+
-- compare. S4A-b lifts that rule into an executable catalog assertion in
|
|
22
|
+
-- tests/sql/control_plane_invariants.sql with the same two-entry whitelist.
|
|
23
|
+
--
|
|
24
|
+
-- Timestamps are timestamptz. Every instant written here comes from the
|
|
25
|
+
-- composition's injected clock rather than the database's now(), so TTL
|
|
26
|
+
-- behavior is assertable under a test clock instead of requiring the suite to
|
|
27
|
+
-- sleep. The migration ledger's own applied_at is the one exception, and it is
|
|
28
|
+
-- not port state.
|
|
29
|
+
|
|
30
|
+
-- ---------------------------------------------------------------------------
|
|
31
|
+
-- devices (cloud.devices) — the device directory
|
|
32
|
+
-- ---------------------------------------------------------------------------
|
|
33
|
+
--
|
|
34
|
+
-- UNIQUE (device_id) is the first whitelisted exception. deviceId is minted by
|
|
35
|
+
-- the cloud (`packages/cloud/src/auth/plane.ts` mints `dev_<uuid>`) and is
|
|
36
|
+
-- never a wire field a device can choose, so global uniqueness is
|
|
37
|
+
-- constructive rather than assumed. It backs `resolveByDeviceId`, one of the
|
|
38
|
+
-- three pre-tenant methods `stores/ports.ts` documents: POST /byok/challenge
|
|
39
|
+
-- and POST /byok/token carry only a deviceId, and the row this returns CARRIES
|
|
40
|
+
-- its tenant, so every step after it is tenant-first.
|
|
41
|
+
--
|
|
42
|
+
-- If a future protocol ever let a device choose its own id, this constraint
|
|
43
|
+
-- becomes a cross-tenant denial of service (one tenant claiming another's id)
|
|
44
|
+
-- and would have to go. It is safe only because of the minting rule above.
|
|
45
|
+
CREATE TABLE device (
|
|
46
|
+
tenant_id text NOT NULL,
|
|
47
|
+
device_id text NOT NULL,
|
|
48
|
+
product_id text NOT NULL,
|
|
49
|
+
device_name text NOT NULL,
|
|
50
|
+
device_public_key text NOT NULL,
|
|
51
|
+
revoked boolean NOT NULL DEFAULT false,
|
|
52
|
+
PRIMARY KEY (tenant_id, device_id),
|
|
53
|
+
CONSTRAINT device_device_id_key UNIQUE (device_id)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
-- ---------------------------------------------------------------------------
|
|
57
|
+
-- pairing_code (cloud.pairingCodes) — single-use tenant-bearing credentials
|
|
58
|
+
-- ---------------------------------------------------------------------------
|
|
59
|
+
--
|
|
60
|
+
-- PRIMARY KEY (code) is the second whitelisted exception, and here the naked
|
|
61
|
+
-- key is the point: the code IS the tenant lookup. It was minted out-of-band by
|
|
62
|
+
-- the host's control plane, which is the only party that knows which tenant a
|
|
63
|
+
-- human is acting for, and `PairRequest` has no tenant field at all — so a
|
|
64
|
+
-- device can never name the tenant it lands in. The row carries tenant_id, so
|
|
65
|
+
-- redemption resolves the tenant in one step.
|
|
66
|
+
--
|
|
67
|
+
-- redeemed_at doubles as the consumption guard: redemption is a single
|
|
68
|
+
-- `UPDATE ... WHERE redeemed_at IS NULL` whose zero-row result is the typed
|
|
69
|
+
-- rejection. A read-then-write would let two concurrent redemptions both
|
|
70
|
+
-- observe an unused code, and single-use is exactly what makes the caller's
|
|
71
|
+
-- "redeem, then register the device" sequence exclusive.
|
|
72
|
+
CREATE TABLE pairing_code (
|
|
73
|
+
code text PRIMARY KEY,
|
|
74
|
+
tenant_id text NOT NULL,
|
|
75
|
+
product_id text NOT NULL,
|
|
76
|
+
expires_at timestamptz NOT NULL,
|
|
77
|
+
redeemed_at timestamptz
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- ---------------------------------------------------------------------------
|
|
81
|
+
-- auth_nonce (cloud.nonces) — single-use challenge nonces
|
|
82
|
+
-- ---------------------------------------------------------------------------
|
|
83
|
+
--
|
|
84
|
+
-- No index on nonce alone: a nonce is only ever validated for the (tenant,
|
|
85
|
+
-- device) it was issued to, so a global lookup would be a capability nothing
|
|
86
|
+
-- needs and a leaked nonce could otherwise be probed against. `markUsed`
|
|
87
|
+
-- addresses (tenant_id, nonce), which the primary key's leading column already
|
|
88
|
+
-- serves.
|
|
89
|
+
CREATE TABLE auth_nonce (
|
|
90
|
+
tenant_id text NOT NULL,
|
|
91
|
+
device_id text NOT NULL,
|
|
92
|
+
nonce text NOT NULL,
|
|
93
|
+
expires_at timestamptz NOT NULL,
|
|
94
|
+
used boolean NOT NULL DEFAULT false,
|
|
95
|
+
PRIMARY KEY (tenant_id, device_id, nonce)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
-- ---------------------------------------------------------------------------
|
|
99
|
+
-- inbound_dedup (cloud.dedup) — bounded at-most-once processing
|
|
100
|
+
-- ---------------------------------------------------------------------------
|
|
101
|
+
--
|
|
102
|
+
-- The wire is at-least-once (docs/protocol.md §9), so this table makes
|
|
103
|
+
-- processing at-most-once. recorded_seq exists to make the retention bound
|
|
104
|
+
-- expressible: reclaim deletes everything older than the newest N rows for a
|
|
105
|
+
-- device, oldest first, so the ids most likely to be redelivered are the ones
|
|
106
|
+
-- still remembered. Without an insertion order there is no defensible answer to
|
|
107
|
+
-- "which row do I drop", and an unbounded set lets one chatty device grow the
|
|
108
|
+
-- table without limit.
|
|
109
|
+
CREATE TABLE inbound_dedup (
|
|
110
|
+
tenant_id text NOT NULL,
|
|
111
|
+
device_id text NOT NULL,
|
|
112
|
+
envelope_id text NOT NULL,
|
|
113
|
+
recorded_seq bigserial NOT NULL,
|
|
114
|
+
PRIMARY KEY (tenant_id, device_id, envelope_id)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
-- Tenant-first, and it is the reclaim's ordering index — not a unique one.
|
|
118
|
+
CREATE INDEX inbound_dedup_reclaim_idx
|
|
119
|
+
ON inbound_dedup (tenant_id, device_id, recorded_seq);
|
|
120
|
+
|
|
121
|
+
-- ---------------------------------------------------------------------------
|
|
122
|
+
-- task (cloud.tasks) — the ownership authority the inbound gate reads
|
|
123
|
+
-- ---------------------------------------------------------------------------
|
|
124
|
+
--
|
|
125
|
+
-- owner_device_id is NULL until the first claim, and the claim is a single
|
|
126
|
+
-- `UPDATE ... WHERE owner_device_id IS NULL` so that two devices racing the
|
|
127
|
+
-- same offer produce one owner rather than a last writer. Ownership never
|
|
128
|
+
-- transfers: an owner reassignment is the one operation that would make the
|
|
129
|
+
-- gate's cross-device assertion unfalsifiable.
|
|
130
|
+
--
|
|
131
|
+
-- status carries the values in TASK_ATTEMPT_STATUSES. Deliberately no CHECK
|
|
132
|
+
-- constraint restating that list: the port type is the vocabulary's single
|
|
133
|
+
-- authority, and a copy here could drift from it silently.
|
|
134
|
+
CREATE TABLE task (
|
|
135
|
+
tenant_id text NOT NULL,
|
|
136
|
+
task_id text NOT NULL,
|
|
137
|
+
device_id text NOT NULL,
|
|
138
|
+
owner_device_id text,
|
|
139
|
+
status text NOT NULL,
|
|
140
|
+
updated_at timestamptz NOT NULL,
|
|
141
|
+
PRIMARY KEY (tenant_id, task_id)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
-- ---------------------------------------------------------------------------
|
|
145
|
+
-- device_request_receipts (cloud.receipts) — the terminal idempotency seam
|
|
146
|
+
-- ---------------------------------------------------------------------------
|
|
147
|
+
--
|
|
148
|
+
-- First write wins, expressed as `INSERT ... ON CONFLICT DO NOTHING`. The first
|
|
149
|
+
-- terminal a device reports is the fact (§12.6.4: 不覆写第一份事实), and the
|
|
150
|
+
-- retry that the at-least-once wire guarantees must not restamp it. An upsert
|
|
151
|
+
-- that updated would pass a naive "record twice" check while rewriting history.
|
|
152
|
+
CREATE TABLE device_request_receipts (
|
|
153
|
+
tenant_id text NOT NULL,
|
|
154
|
+
key text NOT NULL,
|
|
155
|
+
body text NOT NULL,
|
|
156
|
+
recorded_at timestamptz NOT NULL,
|
|
157
|
+
PRIMARY KEY (tenant_id, key)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
-- ---------------------------------------------------------------------------
|
|
161
|
+
-- device_stream (cloud.sequence, and S4A-b's mailbox cursor)
|
|
162
|
+
-- ---------------------------------------------------------------------------
|
|
163
|
+
--
|
|
164
|
+
-- next_seq is the delivery number allocator: `cloud.sequence.next` bumps it in
|
|
165
|
+
-- one statement and returns the pre-bump value, so concurrent allocations
|
|
166
|
+
-- cannot hand out the same number twice. The daemon's redelivery cursor IS this
|
|
167
|
+
-- number, and two envelopes sharing one make the cursor ambiguous.
|
|
168
|
+
--
|
|
169
|
+
-- acked_seq belongs to core.mailbox, which S4A-b implements. It is created HERE
|
|
170
|
+
-- anyway, on purpose: this file is frozen at merge, and a later slice adding a
|
|
171
|
+
-- column would have to either ALTER a frozen file or open 0002 to widen a table
|
|
172
|
+
-- 0001 already owns. Creating the row's full shape once costs one unused column
|
|
173
|
+
-- for one slice and avoids both.
|
|
174
|
+
--
|
|
175
|
+
-- next_seq/acked_seq are bigint, not int: a per-device delivery counter that
|
|
176
|
+
-- wraps is a redelivery bug, and the pool decodes int8 to a JS bigint (see
|
|
177
|
+
-- packages/cloud-postgres/src/pool.ts) rather than to a string.
|
|
178
|
+
CREATE TABLE device_stream (
|
|
179
|
+
tenant_id text NOT NULL,
|
|
180
|
+
device_id text NOT NULL,
|
|
181
|
+
next_seq bigint NOT NULL DEFAULT 1,
|
|
182
|
+
acked_seq bigint NOT NULL DEFAULT 0,
|
|
183
|
+
PRIMARY KEY (tenant_id, device_id)
|
|
184
|
+
);
|