@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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Postgres {@link BoardStore} (§12.3).
3
+ *
4
+ * Every mutation is one guarded `UPDATE ... WHERE`, and the guard carries the
5
+ * caller's whole expectation: who holds the item, what status it is in, and —
6
+ * for `claim` — that it is claimable at all. Zero rows is the rejection, and
7
+ * the store then re-reads the item to say WHICH rejection and to hand the
8
+ * caller the snapshot it lost to. A conflict that reports only "conflict"
9
+ * forces a second round trip and invites a retry loop that eventually
10
+ * overwrites the winner (§12.3: no silent last-write-wins).
11
+ *
12
+ * The concurrent-claim property comes out of Postgres' own re-check: three
13
+ * sessions issue the same `UPDATE ... WHERE holder_id IS NULL`, one wins, and
14
+ * the other two re-evaluate the qual against the winner's committed row and
15
+ * match nothing. The suite asserts the outcome, not the mechanism — the
16
+ * in-memory reference gets the same outcome from its `await` boundaries.
17
+ *
18
+ * **`board_seq` is allocated in its own statement, before the guarded write.**
19
+ * Folding the allocator into a data-modifying CTE would let one session lock
20
+ * `tenant_stream` then `board_item` while another locks them in the order the
21
+ * planner picked for it, which is a deadlock rather than a conflict. Allocating
22
+ * first, in an autocommitted statement that releases immediately, gives every
23
+ * writer one lock order. A rejected write therefore burns a sequence number:
24
+ * `boardSeq` is contractually monotonic, never contractually gapless, and the
25
+ * incremental feed reads `> afterSeq` either way.
26
+ */
27
+ import { type BoardClaimInput, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type BoardStatusUpdateInput, type BoardStore, type BoardUnclaimInput, type Clock, type TenantId } from '@byok-sdk/core';
28
+ import type { Pool } from 'pg';
29
+ export declare class PostgresBoardStore implements BoardStore {
30
+ #private;
31
+ constructor(pool: Pool, clock: Clock);
32
+ create(tenant: TenantId, input: BoardItemInput): Promise<BoardItem>;
33
+ get(tenant: TenantId, itemId: string): Promise<BoardItem | undefined>;
34
+ list(tenant: TenantId, query: BoardListQuery): Promise<BoardPage>;
35
+ claim(tenant: TenantId, input: BoardClaimInput): Promise<BoardItem>;
36
+ unclaim(tenant: TenantId, input: BoardUnclaimInput): Promise<BoardItem>;
37
+ updateStatus(tenant: TenantId, input: BoardStatusUpdateInput): Promise<BoardItem>;
38
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The Postgres composition of the eight `@byok-sdk/core` ports.
3
+ *
4
+ * All eight, or none: `runCoreConformance`'s port-inventory dimension asserts a
5
+ * composition supplies exactly `CORE_STORE_NAMES` and exactly the methods
6
+ * `CORE_PORT_METHODS` declares, so there is no such thing as a partial core
7
+ * composition to certify. The original seven landed in one slice (design §11);
8
+ * `skillPacks` is the eighth, added in Phase 2 of `skill-pack-delivery-channel`
9
+ * when it graduated from a bridge port to a mandatory `CoreStores` member. This
10
+ * function returns a full `CoreStores` where the cloud-local sibling returns a
11
+ * named subset.
12
+ *
13
+ * Everything that reads time reads the injected clock. Nothing calls SQL
14
+ * `now()` — presence expiry, activity expiry and reservation expiry are all
15
+ * contract behavior the suite asserts by moving a test clock, and a store that
16
+ * asked the database for the time would make every one of those assertions
17
+ * either a sleep or a flake.
18
+ */
19
+ import type { Clock, CoreStores } from '@byok-sdk/core';
20
+ import type { Pool } from 'pg';
21
+ export { PostgresMailboxStore } from './mailbox';
22
+ export { PostgresBoardStore } from './board';
23
+ export { PostgresTruthStore } from './truth';
24
+ export { PostgresPresenceStore, PostgresActivityStore } from './presence';
25
+ export { PostgresObjectStore } from './objects';
26
+ export { PostgresQuotaStore } from './quota';
27
+ export { PostgresSkillPackStore } from './skill-pack';
28
+ export interface PostgresCoreStoreOptions {
29
+ readonly pool: Pool;
30
+ /**
31
+ * The clock every TTL and timestamp in this composition reads. Injected, not
32
+ * the database's `now()`: expiry has to be assertable under a test clock, and
33
+ * a store that asks the server for the time cannot be.
34
+ */
35
+ readonly clock: Clock;
36
+ }
37
+ export declare function createPostgresCoreStores(options: PostgresCoreStoreOptions): CoreStores;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The one Postgres mailbox sequence allocator.
3
+ *
4
+ * The caller must already be in a transaction and must keep that transaction
5
+ * open until its outbox row is inserted. The returned row lock is what makes a
6
+ * lower sequence commit before any higher sequence for the same device.
7
+ */
8
+ import type { TenantId } from '@byok-sdk/core';
9
+ import type { PoolClient } from 'pg';
10
+ export declare function allocateMailboxSequence(client: PoolClient, tenant: TenantId, deviceId: string, now: string): Promise<number>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Postgres {@link MailboxStore} (§12.7.3).
3
+ *
4
+ * The load-bearing rule, and the one a composition can break silently:
5
+ * **reading is not acknowledging.** `readAfter` is a `SELECT` and nothing else.
6
+ * The only ack is `advanceCursor`, which the daemon calls after it has durably
7
+ * journaled the envelope, and it is monotonic — a lower cursor is refused with
8
+ * the cursor it lost to rather than quietly re-delivering work the device has
9
+ * already run.
10
+ *
11
+ * `collectRetired` deletes acked rows and **marks** unacked ones `expired`. It
12
+ * never deletes an unacked row. §12.7.5 requires an envelope that aged out
13
+ * before anyone consumed it to stay visible; deleting it would make "we dropped
14
+ * work" indistinguishable from "there was no work", and dead-lettering those
15
+ * rows is S4B's job (O-009), not this one's.
16
+ *
17
+ * Every row lives under `(tenant_id, device_id, seq)`, so a cross-tenant read
18
+ * is not denied — it addresses a different key space and finds nothing. That is
19
+ * the SQL expression of §12.6.2 layer 3: there is no bare device index to
20
+ * accidentally query.
21
+ */
22
+ import { type Clock, type MailboxAdvanceCursorInput, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type MailboxRetentionInput, type MailboxRetentionResult, type MailboxStore, type TenantId } from '@byok-sdk/core';
23
+ import type { Pool } from 'pg';
24
+ export declare class PostgresMailboxStore implements MailboxStore {
25
+ #private;
26
+ constructor(pool: Pool, clock: Clock);
27
+ append(tenant: TenantId, input: MailboxAppendInput): Promise<MailboxMessage>;
28
+ readAfter(tenant: TenantId, query: MailboxReadQuery): Promise<MailboxPage>;
29
+ advanceCursor(tenant: TenantId, input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
30
+ readCursor(tenant: TenantId, deviceId: string): Promise<MailboxCursorState>;
31
+ collectRetired(tenant: TenantId, input: MailboxRetentionInput): Promise<MailboxRetentionResult>;
32
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Postgres {@link ObjectStore} — the manifest, and only the manifest
3
+ * (§12.7.4, §12.7.8).
4
+ *
5
+ * Zero bytes cross this file. The manifest is the transaction authority and the
6
+ * object store holds the payload; the R2 adapter that moves bytes is S4A-c.
7
+ * What lives here is the state machine that stands between a failed object-store
8
+ * delete and either a leaked object or a deleted one a truth record still points
9
+ * at.
10
+ *
11
+ * Three properties the SQL is shaped around:
12
+ *
13
+ * - **`ref_count` is recomputed, never incremented.** Every reference mutation
14
+ * sets it to `count(*)` over `object_reference`. An increment drifts the
15
+ * moment a retried `addReference` lands twice, and a drifted count strands
16
+ * the object forever because `markDeletePending` refuses at `refCount != 0`.
17
+ * The reference table's primary key already makes the write idempotent; the
18
+ * count just reads it.
19
+ * - **Reference mutations hold the manifest row under `FOR UPDATE`.** The
20
+ * recomputation above is only authoritative because of this lock, not on its
21
+ * own: under READ COMMITTED the `count(*)` subquery is evaluated on the
22
+ * snapshot its statement started with, so an unlocked `addReference` reads a
23
+ * `committed` state, and `markDeletePending`'s `ref_count = 0` guard can pass
24
+ * in the window before the reference row lands — leaving `delete_pending`
25
+ * with a live reference, which is a truth record pointing at bytes S4B's GC
26
+ * is entitled to delete. Taking the manifest row exclusively first makes the
27
+ * tombstone and the reference write queue against each other, exactly as
28
+ * `PostgresQuotaStore.reserve` serializes reservers on the entitlement row.
29
+ * - **Every state move is a guarded `UPDATE`.** The legal transitions are the
30
+ * guard, so an illegal one writes nothing and the row is re-read to say which
31
+ * typed rejection applies. `commit` additionally guards on the DECLARED size
32
+ * and type, because the whole point of the check is that what the composition
33
+ * observed on the object store and what the client declared can differ
34
+ * (§12.7.7 step 4).
35
+ */
36
+ import { type Clock, type ContentHash, type ObjectCommitInput, type ObjectListQuery, type ObjectManifestEntry, type ObjectManifestInput, type ObjectReferenceInput, type ObjectStore, type TenantId } from '@byok-sdk/core';
37
+ import type { Pool } from 'pg';
38
+ export declare class PostgresObjectStore implements ObjectStore {
39
+ #private;
40
+ constructor(pool: Pool, clock: Clock);
41
+ putManifest(tenant: TenantId, input: ObjectManifestInput): Promise<ObjectManifestEntry>;
42
+ commit(tenant: TenantId, input: ObjectCommitInput): Promise<ObjectManifestEntry>;
43
+ get(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry | undefined>;
44
+ list(tenant: TenantId, query: ObjectListQuery): Promise<readonly ObjectManifestEntry[]>;
45
+ addReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
46
+ removeReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
47
+ markDeletePending(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
48
+ markDeleted(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
49
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Postgres {@link PresenceStore} and {@link ActivityStore} (§12.3).
3
+ *
4
+ * Both are lossy, TTL-bounded, unsigned, and never authoritative, and both are
5
+ * bounded upserts: one row per device, one row per task. Nothing here may be
6
+ * used to derive coordination state, execution state, authorization, billing or
7
+ * recovery — which is why this file shares no vocabulary with `board.ts` and
8
+ * none with the frozen wire states.
9
+ *
10
+ * **Expiry is absence, and it is expressed as a read filter.** A hint past its
11
+ * `expiresAt` is invisible to every read, so no reader can observe a stale
12
+ * level and mistake it for a live one. The in-memory reference deletes the
13
+ * entry lazily on read; here the row stays and the predicate excludes it. Same
14
+ * observable answer, and a `SELECT` that does not write to answer itself.
15
+ *
16
+ * Every instant is the injected clock's. Asserting TTL behavior against a wall
17
+ * clock means sleeping or accepting flakes, which is why the port takes `ttlMs`
18
+ * and the composition supplies the clock.
19
+ */
20
+ import { type ActivityAppendInput, type ActivityStore, type ActivityTail, type Clock, type PresenceHint, type PresenceHintInput, type PresenceStore, type TenantId } from '@byok-sdk/core';
21
+ import type { Pool } from 'pg';
22
+ export declare class PostgresPresenceStore implements PresenceStore {
23
+ #private;
24
+ constructor(pool: Pool, clock: Clock);
25
+ publish(tenant: TenantId, input: PresenceHintInput): Promise<PresenceHint>;
26
+ read(tenant: TenantId, deviceId: string): Promise<PresenceHint | undefined>;
27
+ list(tenant: TenantId): Promise<readonly PresenceHint[]>;
28
+ }
29
+ export declare class PostgresActivityStore implements ActivityStore {
30
+ #private;
31
+ constructor(pool: Pool, clock: Clock);
32
+ append(tenant: TenantId, input: ActivityAppendInput): Promise<ActivityTail>;
33
+ read(tenant: TenantId, taskId: string): Promise<ActivityTail | undefined>;
34
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Postgres {@link QuotaStore} — entitlement, usage, reservation (§12.7.6-12.7.7).
3
+ *
4
+ * The invariant this file exists to hold is
5
+ * `committed + reserved + expected <= hardLimitBytes`, and the reason it is the
6
+ * hardest statement in the slice is that the obvious shapes all get it wrong in
7
+ * a way nothing throws about.
8
+ *
9
+ * **Why `reserve` opens a transaction.** Admission is one guarded
10
+ * `INSERT ... SELECT ... WHERE`, exactly as every other CAS in this package is
11
+ * one guarded statement. But the guard's operand is an AGGREGATE over the
12
+ * tenant's live reservations, and under READ COMMITTED a statement's snapshot
13
+ * is taken when the statement starts. Two concurrent reservers therefore both
14
+ * read a pre-insert world and both pass — the classic oversell. Postgres'
15
+ * EvalPlanQual re-check, which is what makes `UPDATE ... WHERE status = $x` a
16
+ * genuine CAS elsewhere in this package, re-evaluates the qual only against the
17
+ * updated TARGET row; a subquery over another table keeps the original
18
+ * snapshot. So no single statement over this shape can serialize reservers.
19
+ *
20
+ * The two ways out are a counter column CAS'd in place, or a lock. The counter
21
+ * loses: it has to be decremented on all three of finalize / abort / expire,
22
+ * and a path that settles without decrementing leaves a tenant permanently
23
+ * short of quota it actually released — durable drift, invisible until someone
24
+ * audits. So `storage_usage` has NO `reserved_bytes` column and
25
+ * `TenantStorageUsage.reservedBytes` is a `SUM` over live reservations, which
26
+ * cannot drift, and admission takes `FOR UPDATE` on the tenant's entitlement
27
+ * row first. The next statement in that transaction takes a FRESH snapshot,
28
+ * acquired behind the lock, so it sees every committed reservation. This is the
29
+ * "row-locked transaction" `packages/core/src/in-memory/quota.ts` names as the
30
+ * Postgres shape.
31
+ *
32
+ * What is NOT happening here: a read whose result is compared in TypeScript and
33
+ * then written. Every admission decision lives in the SQL guard. The reads on
34
+ * the rejection path exist only to answer "which of the five typed rejections
35
+ * is this", after the write has already been refused.
36
+ *
37
+ * Nothing in this file calls SQL `now()`. Every instant is the injected clock's,
38
+ * so reservation expiry is assertable under a test clock — and
39
+ * `__tests__/constraints.test.ts` scans this directory to keep it that way.
40
+ */
41
+ import { type Clock, type MailboxUsageDeltaInput, type QuotaStore, type StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type StorageStatus, type TenantId, type TenantStorageEntitlement, type TenantStorageEntitlementInput, type TenantStorageUsage } from '@byok-sdk/core';
42
+ import type { Pool } from 'pg';
43
+ export declare class PostgresQuotaStore implements QuotaStore {
44
+ #private;
45
+ constructor(pool: Pool, clock: Clock);
46
+ readEntitlement(tenant: TenantId): Promise<TenantStorageEntitlement | undefined>;
47
+ writeEntitlement(tenant: TenantId, input: TenantStorageEntitlementInput): Promise<TenantStorageEntitlement>;
48
+ readUsage(tenant: TenantId): Promise<TenantStorageUsage>;
49
+ readStatus(tenant: TenantId): Promise<StorageStatus>;
50
+ readReservation(tenant: TenantId, reservationId: string): Promise<StorageReservation | undefined>;
51
+ reserve(tenant: TenantId, input: StorageReservationInput): Promise<StorageReservation>;
52
+ finalizeReservation(tenant: TenantId, input: StorageFinalizeInput): Promise<StorageFinalizeResult>;
53
+ abortReservation(tenant: TenantId, reservationId: string): Promise<StorageReservation>;
54
+ expireReservations(tenant: TenantId): Promise<readonly StorageReservation[]>;
55
+ applyMailboxDelta(tenant: TenantId, input: MailboxUsageDeltaInput): Promise<TenantStorageUsage>;
56
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Postgres {@link SkillPackStore} (plan `skill-pack-delivery-channel`, Phase 2).
3
+ *
4
+ * A faithful projection of {@link InMemorySkillPackStore}: same validators, same
5
+ * refusals, same read shape. What `publish` checks and what it rejects is core's
6
+ * decision, not this store's — it reuses `checkSkillPackManifest` /
7
+ * `checkSkillPackEntry` and the manifest/file cross-checks the reference
8
+ * performs, because this store cannot hash (integrity is the installing device's
9
+ * job) but it CAN refuse to persist a pack whose declared path set or byte sizes
10
+ * already disagree with the bytes it was handed. Storing that pair unchecked
11
+ * would hand a device a corrupt publication it could not tell from a tampered
12
+ * response.
13
+ *
14
+ * Two tables, one per level of the manifest (`deploy/sql/0005_skill_packs.sql`):
15
+ * `skill_pack` carries the pack-level fields a `SkillPackManifest` needs to be
16
+ * reconstructed (version, description, the pack content hash), and
17
+ * `skill_pack_file` carries one row per declared file with its content hash,
18
+ * byte size, and the UTF-8 text itself. A publish is a single transaction that
19
+ * upserts the pack row and REPLACES its file set, so a re-publish of a changed
20
+ * pack under the same name never leaves a stale file behind.
21
+ *
22
+ * `byte_size` is `integer`, not `bigint`: `SkillPackFile.byteSize` is a core
23
+ * `number` bounded by `SKILL_PACK_FILE_MAX_BYTES` (256 KiB), unlike the quota
24
+ * and truth byte fields the contract declares as `bigint`. `integer` is the
25
+ * type that round-trips a `number` without a cast at the boundary — the same
26
+ * call `object_manifest.ref_count` makes for a small count.
27
+ *
28
+ * No injected clock: a manifest carries no timestamp, so this store writes none
29
+ * and asks the database for none. That mirrors the reference, which takes no
30
+ * clock either.
31
+ */
32
+ import { type SkillPackFileContent, type SkillPackListQuery, type SkillPackManifest, type SkillPackPublishInput, type SkillPackStore, type TenantId } from '@byok-sdk/core';
33
+ import type { Pool } from 'pg';
34
+ export declare class PostgresSkillPackStore implements SkillPackStore {
35
+ #private;
36
+ constructor(pool: Pool);
37
+ publish(tenant: TenantId, input: SkillPackPublishInput): Promise<SkillPackManifest>;
38
+ get(tenant: TenantId, name: string): Promise<SkillPackManifest | undefined>;
39
+ list(tenant: TenantId, query: SkillPackListQuery): Promise<readonly SkillPackManifest[]>;
40
+ readFile(tenant: TenantId, name: string, path: string): Promise<SkillPackFileContent | undefined>;
41
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Postgres {@link TruthStore} (§12.3, §12.6.4).
3
+ *
4
+ * Two write models over one table, and the primary key does the deciding for
5
+ * both.
6
+ *
7
+ * - `task.terminal` is first-write-wins: `INSERT ... ON CONFLICT DO NOTHING`
8
+ * plus an equality re-read. A replay of the identical hash returns the
9
+ * ORIGINAL row — same `rev`, same `writtenAt`, same `requestId` — and a
10
+ * different hash for the same task is refused with the record already
11
+ * committed attached. An upsert would pass a naive "write it twice" check
12
+ * while quietly restamping the first fact, which §12.6.4 forbids outright.
13
+ * - `profile` / `memory` are per-key snapshots under an `expectedRev` CAS:
14
+ * `UPDATE ... WHERE rev = $expectedRev`, with `expectedRev = 0` expressed as
15
+ * the insert. Zero rows is the conflict, and the caller gets the current
16
+ * record — or `undefined` when it claimed a revision of a record that does
17
+ * not exist.
18
+ *
19
+ * The store never merges bodies. A conflict hands back what it lost to and
20
+ * stops, because the device holding the context is the only party that can
21
+ * decide what the merged truth should be (§12.3).
22
+ */
23
+ import { type Clock, type SnapshotWriteInput, type TenantId, type TerminalWriteInput, type TruthManifestEntry, type TruthManifestQuery, type TruthRecord, type TruthRecordSelector, type TruthStore } from '@byok-sdk/core';
24
+ import type { Pool } from 'pg';
25
+ export declare class PostgresTruthStore implements TruthStore {
26
+ #private;
27
+ constructor(pool: Pool, clock: Clock);
28
+ writeTerminal(tenant: TenantId, input: TerminalWriteInput): Promise<TruthRecord>;
29
+ writeSnapshot(tenant: TenantId, input: SnapshotWriteInput): Promise<TruthRecord>;
30
+ getRecord(tenant: TenantId, selector: TruthRecordSelector): Promise<TruthRecord | undefined>;
31
+ listManifest(tenant: TenantId, query: TruthManifestQuery): Promise<readonly TruthManifestEntry[]>;
32
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Postgres {@link InboundDedupStore} (N3): bounded at-most-once processing.
3
+ *
4
+ * Check-and-record is one `INSERT ... ON CONFLICT DO NOTHING`, so a
5
+ * composition cannot accidentally split it into a racy read-then-write: the
6
+ * primary key does the deciding, and zero returned rows means "already seen".
7
+ *
8
+ * Reclaim runs only on the path that actually grew the table, and it deletes
9
+ * oldest-first down to `DEDUP_RING_CAPACITY` rows for that device — the same
10
+ * bound the in-memory ring holds. The ids most likely to be redelivered are the
11
+ * recent ones, so dropping the oldest is the retention that matches the wire's
12
+ * behavior. An unbounded set would pass every duplicate assertion and still let
13
+ * one chatty device grow this table without limit.
14
+ */
15
+ import { type InboundDedupStore } from '@byok-sdk/cloud';
16
+ import type { TenantId } from '@byok-sdk/core';
17
+ import type { Pool } from 'pg';
18
+ export declare class PostgresInboundDedupStore implements InboundDedupStore {
19
+ #private;
20
+ constructor(pool: Pool, capacity?: number);
21
+ checkAndRecord(tenant: TenantId, deviceId: string, envelopeId: string): Promise<boolean>;
22
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Postgres {@link DeviceDirectory}.
3
+ *
4
+ * Rows live under the composite key `(tenant_id, device_id)`, so a cross-tenant
5
+ * read is not "denied" — it addresses a different key space and finds nothing
6
+ * (§12.6.2 layer 3). `resolveByDeviceId` is the documented pre-tenant entry
7
+ * point, and it is safe for exactly one reason: the row it returns CARRIES its
8
+ * tenant, so the caller never compares a tenant it was handed against one it
9
+ * guessed. One row, two access paths, never two copies to keep in sync — a
10
+ * stale pre-tenant index would be a revoked device that can still get a token.
11
+ */
12
+ import type { TenantId } from '@byok-sdk/core';
13
+ import type { DeviceDirectory, DeviceRecord, DeviceRegistration } from '@byok-sdk/cloud';
14
+ import type { Pool } from 'pg';
15
+ export declare class PostgresDeviceDirectory implements DeviceDirectory {
16
+ #private;
17
+ constructor(pool: Pool);
18
+ register(tenant: TenantId, input: DeviceRegistration): Promise<DeviceRecord>;
19
+ get(tenant: TenantId, deviceId: string): Promise<DeviceRecord | undefined>;
20
+ revoke(tenant: TenantId, deviceId: string): Promise<void>;
21
+ list(tenant: TenantId): Promise<readonly DeviceRecord[]>;
22
+ resolveByDeviceId(deviceId: string): Promise<DeviceRecord | undefined>;
23
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The Postgres composition of the cloud-local ports.
3
+ *
4
+ * Seven durable stores, one object-storage blob store, one in-memory limiter:
5
+ *
6
+ * - Six durable ones have rows in `deploy/sql/0001_cloud_local.sql`; S6's
7
+ * proof receipt authority is the seventh, in `0004_device_proof_truth.sql`.
8
+ * - `blobs` is the R2 adapter over the `object_manifest` row the core store
9
+ * already owns: metadata in Postgres, bytes in the object store, one
10
+ * reserve/verify protocol binding them (design §6). It supplies grants only,
11
+ * and this composition deliberately hands `createByokCloud` NO
12
+ * `BlobContentProxy` — a device uploading straight to R2 is what having no
13
+ * byte-proxy path means, and saying so by absence is what keeps the two
14
+ * `/content` routes from mounting on a deployment that cannot serve them.
15
+ * - `rateLimiter` gets the allow-all reference and NO table, by design
16
+ * (docs/researches/s4a-dataplane-design.md §5). Persisting an allow-all would
17
+ * create a table that is always empty, and a real limiter is edge/infra work
18
+ * whose implementation would not be a per-request Postgres write either.
19
+ */
20
+ import { type CloudStores } from '@byok-sdk/cloud';
21
+ import type { CloudCrypto } from '@byok-sdk/cloud';
22
+ import type { Clock } from '@byok-sdk/core';
23
+ import type { Pool } from 'pg';
24
+ import { type R2BlobStoreOptions } from './r2-blobs';
25
+ export { PostgresDeviceDirectory } from './devices';
26
+ export { PostgresInboundDedupStore } from './dedup';
27
+ export { PostgresNonceStore } from './nonces';
28
+ export { PostgresPairingCodeStore } from './pairing-codes';
29
+ export { PostgresRequestReceiptStore } from './receipts';
30
+ export { PostgresProofRequestReceiptStore } from './proof-receipts';
31
+ export { PostgresTaskAttemptStore } from './task-attempts';
32
+ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRESIGN_TTL_SECONDS, DEFAULT_RETRY_DELAY_MS, MAX_PRESIGN_TTL_SECONDS, MIN_PRESIGN_TTL_SECONDS, ObjectStoreRequestError, R2_BLOB_ERROR_CODES, R2BlobStoreError, R2CloudBlobStore, R2ObjectMaintenanceStore, } from './r2-blobs';
33
+ export type { ObjectStoreFetch, R2BlobErrorCode, R2BlobStoreOptions } from './r2-blobs';
34
+ export type { R2DeleteResult, R2ListedObject, R2ObjectMaintenance, R2ObjectMaintenanceOptions, R2ObjectPage, } from './r2-blobs';
35
+ /** Every cloud-local port. All nine, or it is not a composition. */
36
+ export type PostgresCloudStores = CloudStores;
37
+ /** Everything the blob store needs that is not already a composition-wide input. */
38
+ export type PostgresObjectStorageOptions = Omit<R2BlobStoreOptions, 'objects'>;
39
+ export interface PostgresCloudStoreOptions {
40
+ readonly pool: Pool;
41
+ /**
42
+ * The clock every TTL and timestamp in this composition reads. Injected, not
43
+ * the database's own: expiry has to be assertable under a test clock, and a
44
+ * store that asks the server for the time cannot be.
45
+ */
46
+ readonly clock: Clock;
47
+ readonly crypto: CloudCrypto;
48
+ /**
49
+ * Where the bytes go. Required, because a composition that cannot say where
50
+ * its objects live cannot honestly claim the `blobs` port — and the
51
+ * conformance suite certifies compositions whole, never in parts.
52
+ */
53
+ readonly objectStorage: PostgresObjectStorageOptions;
54
+ }
55
+ export declare function createPostgresCloudStores(options: PostgresCloudStoreOptions): PostgresCloudStores;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Postgres {@link NonceStore}: single-use challenge nonces bound to the
3
+ * (tenant, device) they were issued for, expiring after `NONCE_TTL_MS`
4
+ * (docs/protocol.md §6.2).
5
+ *
6
+ * Expiry is compared against the INJECTED clock, never the database's `now()`.
7
+ * A store that asked the server for the time would be unassertable under a test
8
+ * clock, and the conformance suite's TTL dimension would have to sleep — which
9
+ * is how a replay-window regression ships unnoticed.
10
+ *
11
+ * `issue` sweeps this device's spent and expired rows inline, same posture as
12
+ * the in-memory reference and the reference server: a long-lived deployment
13
+ * never calls a sweep on a timer, and the sweep is bounded to one (tenant,
14
+ * device) so it stays proportional to the caller that triggered it.
15
+ */
16
+ import { type NonceStore } from '@byok-sdk/cloud';
17
+ import type { Clock, TenantId } from '@byok-sdk/core';
18
+ import type { CloudCrypto } from '@byok-sdk/cloud';
19
+ import type { Pool } from 'pg';
20
+ export declare class PostgresNonceStore implements NonceStore {
21
+ #private;
22
+ constructor(pool: Pool, clock: Clock, crypto: CloudCrypto, ttlMs?: number);
23
+ issue(tenant: TenantId, deviceId: string): Promise<string>;
24
+ validate(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
25
+ markUsed(tenant: TenantId, nonce: string): Promise<void>;
26
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Postgres {@link PairingCodeStore}: single-use codes bound to the tenant and
3
+ * product they were minted for.
4
+ *
5
+ * Redemption is one guarded statement. `UPDATE ... WHERE redeemed_at IS NULL
6
+ * AND expires_at >= $now RETURNING ...` consumes and reports in the same
7
+ * round trip, and zero rows is the typed rejection. A read-then-write would let
8
+ * two concurrent redemptions both observe an unused code, and single-use is
9
+ * exactly what makes the caller's "redeem, then register the device" sequence
10
+ * exclusive.
11
+ *
12
+ * Unknown, expired, and already-used all answer `undefined`. The reference
13
+ * server distinguishes them in its 401 text; a hosted multi-tenant surface
14
+ * deliberately does not — the code is a bearer credential addressable across
15
+ * every tenant, and "already used" versus "never existed" is precisely the
16
+ * difference an attacker enumerating codes would pay for.
17
+ */
18
+ import type { Clock, TenantId } from '@byok-sdk/core';
19
+ import type { PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore } from '@byok-sdk/cloud';
20
+ import type { Pool } from 'pg';
21
+ export declare class PostgresPairingCodeStore implements PairingCodeStore {
22
+ #private;
23
+ constructor(pool: Pool, clock: Clock);
24
+ issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
25
+ redeem(code: string): Promise<PairingCodeClaims | undefined>;
26
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProofRequestReceipt, ProofRequestReceiptInput, ProofRequestReceiptStore } from '@byok-sdk/cloud';
2
+ import type { Clock, TenantId } from '@byok-sdk/core';
3
+ import type { Pool } from 'pg';
4
+ export declare class PostgresProofRequestReceiptStore implements ProofRequestReceiptStore {
5
+ #private;
6
+ constructor(pool: Pool, clock: Clock);
7
+ record(tenant: TenantId, input: ProofRequestReceiptInput): Promise<{
8
+ readonly receipt: ProofRequestReceipt;
9
+ readonly created: boolean;
10
+ }>;
11
+ get(tenant: TenantId, deviceId: string, requestId: string): Promise<ProofRequestReceipt | undefined>;
12
+ }