@opendatalabs/personal-server-ts-server 1.33.7 → 1.34.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.
Files changed (167) hide show
  1. package/dist/__fixtures__/bound-record-store.d.ts +19 -0
  2. package/dist/__fixtures__/bound-record-store.d.ts.map +1 -0
  3. package/dist/__fixtures__/bound-record-store.js +60 -0
  4. package/dist/__fixtures__/bound-record-store.js.map +1 -0
  5. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.d.ts +41 -0
  6. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.d.ts.map +1 -0
  7. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.js +34 -0
  8. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.js.map +1 -0
  9. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.d.ts +112 -0
  10. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.d.ts.map +1 -0
  11. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.js +226 -0
  12. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.js.map +1 -0
  13. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.d.ts +135 -0
  14. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.d.ts.map +1 -0
  15. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.js +21 -0
  16. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.js.map +1 -0
  17. package/dist/api-auth.d.ts +18 -2
  18. package/dist/api-auth.d.ts.map +1 -1
  19. package/dist/api-auth.js +114 -22
  20. package/dist/api-auth.js.map +1 -1
  21. package/dist/app.d.ts +60 -1
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +139 -1
  24. package/dist/app.js.map +1 -1
  25. package/dist/bootstrap.d.ts +10 -0
  26. package/dist/bootstrap.d.ts.map +1 -1
  27. package/dist/bootstrap.js +358 -7
  28. package/dist/bootstrap.js.map +1 -1
  29. package/dist/enclave-main.d.ts +4 -0
  30. package/dist/enclave-main.d.ts.map +1 -1
  31. package/dist/enclave-main.js +152 -1
  32. package/dist/enclave-main.js.map +1 -1
  33. package/dist/middleware/body-limit.d.ts +1 -1
  34. package/dist/middleware/body-limit.d.ts.map +1 -1
  35. package/dist/middleware/body-limit.js +8 -3
  36. package/dist/middleware/body-limit.js.map +1 -1
  37. package/dist/middleware/cors.d.ts.map +1 -1
  38. package/dist/middleware/cors.js +1 -0
  39. package/dist/middleware/cors.js.map +1 -1
  40. package/dist/package.json +9 -1
  41. package/dist/pdpp/bootstrap.d.ts +72 -0
  42. package/dist/pdpp/bootstrap.d.ts.map +1 -0
  43. package/dist/pdpp/bootstrap.js +270 -0
  44. package/dist/pdpp/bootstrap.js.map +1 -0
  45. package/dist/pdpp/chain-enforcement.d.ts +95 -0
  46. package/dist/pdpp/chain-enforcement.d.ts.map +1 -0
  47. package/dist/pdpp/chain-enforcement.js +145 -0
  48. package/dist/pdpp/chain-enforcement.js.map +1 -0
  49. package/dist/pdpp/client-document-fetch.d.ts +63 -0
  50. package/dist/pdpp/client-document-fetch.d.ts.map +1 -0
  51. package/dist/pdpp/client-document-fetch.js +254 -0
  52. package/dist/pdpp/client-document-fetch.js.map +1 -0
  53. package/dist/pdpp/declaration-registry.d.ts +83 -0
  54. package/dist/pdpp/declaration-registry.d.ts.map +1 -0
  55. package/dist/pdpp/declaration-registry.js +251 -0
  56. package/dist/pdpp/declaration-registry.js.map +1 -0
  57. package/dist/pdpp/deployment.d.ts +70 -0
  58. package/dist/pdpp/deployment.d.ts.map +1 -0
  59. package/dist/pdpp/deployment.js +104 -0
  60. package/dist/pdpp/deployment.js.map +1 -0
  61. package/dist/pdpp/record-schema.d.ts +2 -0
  62. package/dist/pdpp/record-schema.d.ts.map +1 -0
  63. package/dist/pdpp/record-schema.js +2 -0
  64. package/dist/pdpp/record-schema.js.map +1 -0
  65. package/dist/pdpp/records-bootstrap.d.ts +111 -0
  66. package/dist/pdpp/records-bootstrap.d.ts.map +1 -0
  67. package/dist/pdpp/records-bootstrap.js +353 -0
  68. package/dist/pdpp/records-bootstrap.js.map +1 -0
  69. package/dist/pdpp/source-id-compat.d.ts +5 -0
  70. package/dist/pdpp/source-id-compat.d.ts.map +1 -0
  71. package/dist/pdpp/source-id-compat.js +16 -0
  72. package/dist/pdpp/source-id-compat.js.map +1 -0
  73. package/dist/pdpp/test-connections.d.ts +6 -0
  74. package/dist/pdpp/test-connections.d.ts.map +1 -0
  75. package/dist/pdpp/test-connections.js +37 -0
  76. package/dist/pdpp/test-connections.js.map +1 -0
  77. package/dist/routes/auth-device.d.ts.map +1 -1
  78. package/dist/routes/auth-device.js +3 -0
  79. package/dist/routes/auth-device.js.map +1 -1
  80. package/dist/routes/chunked-scope-import.d.ts +41 -0
  81. package/dist/routes/chunked-scope-import.d.ts.map +1 -0
  82. package/dist/routes/chunked-scope-import.js +901 -0
  83. package/dist/routes/chunked-scope-import.js.map +1 -0
  84. package/dist/routes/data.d.ts +23 -1
  85. package/dist/routes/data.d.ts.map +1 -1
  86. package/dist/routes/data.js +112 -1
  87. package/dist/routes/data.js.map +1 -1
  88. package/dist/routes/grants.d.ts.map +1 -1
  89. package/dist/routes/grants.js +2 -0
  90. package/dist/routes/grants.js.map +1 -1
  91. package/dist/routes/mcp.d.ts.map +1 -1
  92. package/dist/routes/mcp.js +21 -12
  93. package/dist/routes/mcp.js.map +1 -1
  94. package/dist/routes/pdpp-auth.d.ts +157 -0
  95. package/dist/routes/pdpp-auth.d.ts.map +1 -0
  96. package/dist/routes/pdpp-auth.js +953 -0
  97. package/dist/routes/pdpp-auth.js.map +1 -0
  98. package/dist/routes/pdpp-blobs.d.ts +26 -0
  99. package/dist/routes/pdpp-blobs.d.ts.map +1 -0
  100. package/dist/routes/pdpp-blobs.js +179 -0
  101. package/dist/routes/pdpp-blobs.js.map +1 -0
  102. package/dist/routes/pdpp-connections.d.ts +28 -0
  103. package/dist/routes/pdpp-connections.d.ts.map +1 -0
  104. package/dist/routes/pdpp-connections.js +144 -0
  105. package/dist/routes/pdpp-connections.js.map +1 -0
  106. package/dist/routes/pdpp-declarations.d.ts +58 -0
  107. package/dist/routes/pdpp-declarations.d.ts.map +1 -0
  108. package/dist/routes/pdpp-declarations.js +94 -0
  109. package/dist/routes/pdpp-declarations.js.map +1 -0
  110. package/dist/routes/pdpp-instance-bindings.d.ts +26 -0
  111. package/dist/routes/pdpp-instance-bindings.d.ts.map +1 -0
  112. package/dist/routes/pdpp-instance-bindings.js +124 -0
  113. package/dist/routes/pdpp-instance-bindings.js.map +1 -0
  114. package/dist/routes/pdpp-records.d.ts +158 -0
  115. package/dist/routes/pdpp-records.d.ts.map +1 -0
  116. package/dist/routes/pdpp-records.js +1537 -0
  117. package/dist/routes/pdpp-records.js.map +1 -0
  118. package/dist/routes/pdpp-well-known.d.ts +13 -0
  119. package/dist/routes/pdpp-well-known.d.ts.map +1 -0
  120. package/dist/routes/pdpp-well-known.js +20 -0
  121. package/dist/routes/pdpp-well-known.js.map +1 -0
  122. package/dist/storage/declaration-witness.d.ts +56 -0
  123. package/dist/storage/declaration-witness.d.ts.map +1 -0
  124. package/dist/storage/declaration-witness.js +273 -0
  125. package/dist/storage/declaration-witness.js.map +1 -0
  126. package/dist/storage/hierarchy.d.ts +25 -1
  127. package/dist/storage/hierarchy.d.ts.map +1 -1
  128. package/dist/storage/hierarchy.js +210 -16
  129. package/dist/storage/hierarchy.js.map +1 -1
  130. package/dist/storage/import-operations.d.ts +11 -0
  131. package/dist/storage/import-operations.d.ts.map +1 -0
  132. package/dist/storage/import-operations.js +261 -0
  133. package/dist/storage/import-operations.js.map +1 -0
  134. package/dist/storage/index-manager.d.ts +24 -2
  135. package/dist/storage/index-manager.d.ts.map +1 -1
  136. package/dist/storage/index-manager.js +279 -30
  137. package/dist/storage/index-manager.js.map +1 -1
  138. package/dist/storage/index-schema.d.ts +1 -1
  139. package/dist/storage/index-schema.d.ts.map +1 -1
  140. package/dist/storage/index-schema.js +189 -8
  141. package/dist/storage/index-schema.js.map +1 -1
  142. package/dist/storage/legacy-scope-authority.d.ts +14 -0
  143. package/dist/storage/legacy-scope-authority.d.ts.map +1 -0
  144. package/dist/storage/legacy-scope-authority.js +298 -0
  145. package/dist/storage/legacy-scope-authority.js.map +1 -0
  146. package/dist/storage/node-data-storage.d.ts +6 -0
  147. package/dist/storage/node-data-storage.d.ts.map +1 -1
  148. package/dist/storage/node-data-storage.js +161 -2
  149. package/dist/storage/node-data-storage.js.map +1 -1
  150. package/dist/storage/pdpp-grant-binding-sqlite-store.d.ts +17 -0
  151. package/dist/storage/pdpp-grant-binding-sqlite-store.d.ts.map +1 -0
  152. package/dist/storage/pdpp-grant-binding-sqlite-store.js +159 -0
  153. package/dist/storage/pdpp-grant-binding-sqlite-store.js.map +1 -0
  154. package/dist/storage/pdpp-records-sqlite-store.d.ts +112 -0
  155. package/dist/storage/pdpp-records-sqlite-store.d.ts.map +1 -0
  156. package/dist/storage/pdpp-records-sqlite-store.js +1440 -0
  157. package/dist/storage/pdpp-records-sqlite-store.js.map +1 -0
  158. package/dist/storage/pdpp-stream-completion.d.ts +5 -0
  159. package/dist/storage/pdpp-stream-completion.d.ts.map +1 -0
  160. package/dist/storage/pdpp-stream-completion.js +10 -0
  161. package/dist/storage/pdpp-stream-completion.js.map +1 -0
  162. package/dist/storage/scope-deletion-history.d.ts +8 -0
  163. package/dist/storage/scope-deletion-history.d.ts.map +1 -0
  164. package/dist/storage/scope-deletion-history.js +40 -0
  165. package/dist/storage/scope-deletion-history.js.map +1 -0
  166. package/dist/ui/ps-lite-debug.js +3697 -876
  167. package/package.json +12 -4
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Bounded production fetcher for a URL-hosted client identity document
3
+ * (Core §6). Implements `ClientDocumentFetcher` from
4
+ * `@opendatalabs/personal-server-ts-core/pdpp`.
5
+ *
6
+ * Invariants:
7
+ * - HTTPS only.
8
+ * - Every address actually dialed is checked: a hostname's DNS answers via
9
+ * a custom `lookup` on the `undici` dispatcher (defends DNS rebinding —
10
+ * `net`/`tls`/`undici` dial exactly the address `lookup` returns, so
11
+ * there is no separate resolution to race), and a literal-IP URL
12
+ * hostname separately, since that never reaches `dns.lookup` at all.
13
+ * - No redirect is followed; a redirect or an oversized/rejected body is
14
+ * drained (`discardBody`), never left dangling on the socket.
15
+ * - One `AbortSignal`/timeout spans connect + headers + body + cleanup.
16
+ * - The body is read incrementally and dropped past the byte cap.
17
+ * - No credentials are forwarded.
18
+ */
19
+ import dns from "node:dns";
20
+ import net from "node:net";
21
+ import { Agent, fetch as undiciFetch } from "undici";
22
+ export const FETCH_TIMEOUT_MS = 5_000;
23
+ export const MAX_RESPONSE_BYTES = 64 * 1024;
24
+ /** Thrown by the connect-time guard; surfaces as a `fetch_failed` result. */
25
+ export class BlockedAddressError extends Error {
26
+ }
27
+ /**
28
+ * Non-public address space, checked against the literal address a DNS
29
+ * answer resolved to — not the hostname string. `net.BlockList` normalizes
30
+ * both IPv4-mapped IPv6 forms (`::ffff:a.b.c.d` and `::ffff:7f00:1`) and the
31
+ * fully-expanded form (`0:0:0:0:0:0:0:1`) against the same IPv4/IPv6 rules,
32
+ * so listing each CIDR once covers all three.
33
+ */
34
+ const blockedAddresses = new net.BlockList();
35
+ for (const cidr of [
36
+ "0.0.0.0/8", // "this network" / unspecified-as-source
37
+ "10.0.0.0/8", // RFC1918 private
38
+ "100.64.0.0/10", // shared address space / CGNAT (RFC6598)
39
+ "127.0.0.0/8", // loopback
40
+ "169.254.0.0/16", // link-local, incl. cloud metadata
41
+ "172.16.0.0/12", // RFC1918 private
42
+ "192.0.0.0/24", // IETF protocol assignments
43
+ "192.0.2.0/24", // documentation (TEST-NET-1)
44
+ "192.168.0.0/16", // RFC1918 private
45
+ "198.18.0.0/15", // benchmarking
46
+ "198.51.100.0/24", // documentation (TEST-NET-2)
47
+ "203.0.113.0/24", // documentation (TEST-NET-3)
48
+ "224.0.0.0/4", // multicast
49
+ "240.0.0.0/4", // reserved
50
+ "255.255.255.255/32", // limited broadcast
51
+ ]) {
52
+ const [address, prefix] = cidr.split("/");
53
+ blockedAddresses.addSubnet(address, Number(prefix), "ipv4");
54
+ }
55
+ for (const cidr of [
56
+ "::/128", // unspecified
57
+ "::1/128", // loopback
58
+ "fc00::/7", // unique local (ULA)
59
+ "fe80::/10", // link-local
60
+ "ff00::/8", // multicast
61
+ ]) {
62
+ const [address, prefix] = cidr.split("/");
63
+ blockedAddresses.addSubnet(address, Number(prefix), "ipv6");
64
+ }
65
+ export function isBlockedAddress(address) {
66
+ const version = net.isIP(address);
67
+ if (version === 0)
68
+ return true; // not a literal address — refuse, don't guess
69
+ return blockedAddresses.check(address, version === 4 ? "ipv4" : "ipv6");
70
+ }
71
+ /**
72
+ * A URL hostname that is itself a literal IP (e.g. `https://127.0.0.1/`,
73
+ * `https://[::1]/`) never reaches `dns.lookup` — `net`/`tls`/`undici` dial it
74
+ * directly, so the guarded `lookup` in {@link buildGuardedLookup} never runs.
75
+ * Must be checked separately, before the dial, against the same predicate.
76
+ */
77
+ function literalHostAddress(hostname) {
78
+ const unbracketed = hostname.startsWith("[") && hostname.endsWith("]")
79
+ ? hostname.slice(1, -1)
80
+ : hostname;
81
+ return net.isIP(unbracketed) === 0 ? null : unbracketed;
82
+ }
83
+ /**
84
+ * Build a `lookup`-compatible function (the shape `node:net`/`node:tls` and
85
+ * `undici`'s connector accept) that refuses to resolve to a blocked address.
86
+ * Delegates the actual resolution to `node:dns`, always requesting every
87
+ * answer so a rebinding attempt cannot hide a blocked address behind a
88
+ * permitted one that happens to sort first.
89
+ *
90
+ * `isBlocked` defaults to {@link isBlockedAddress}; overriding it is a
91
+ * test-only seam (see `__testing`) so tests can run a real loopback server
92
+ * without disabling the guard's own logic, which is what the override would
93
+ * do if it replaced `lookup` entirely instead of only the address predicate.
94
+ */
95
+ function buildGuardedLookup(isBlocked = isBlockedAddress) {
96
+ return (hostname, options, callback) => {
97
+ // Always ask `dns.lookup` for every answer so a blocked address cannot
98
+ // hide behind a permitted one that happens to sort first, regardless of
99
+ // whether the caller itself wanted `all`.
100
+ dns.lookup(hostname, { ...options, all: true }, (err, addresses) => {
101
+ if (err)
102
+ return callback(err, "", undefined);
103
+ const list = addresses;
104
+ if (list.length === 0) {
105
+ return callback(new BlockedAddressError(`${hostname} resolved to no addresses`), "", undefined);
106
+ }
107
+ const blocked = list.find((a) => isBlocked(a.address));
108
+ if (blocked) {
109
+ return callback(new BlockedAddressError(`${hostname} resolved to a blocked address: ${blocked.address}`), "", undefined);
110
+ }
111
+ // Reply in the shape the caller actually requested: the array form
112
+ // when `options.all` was set (as undici's connector does), otherwise
113
+ // the single-address form `net`/`tls` expect.
114
+ if (options.all) {
115
+ return callback(null, list, undefined);
116
+ }
117
+ callback(null, list[0].address, list[0].family);
118
+ });
119
+ };
120
+ }
121
+ /**
122
+ * Build a dispatcher that only ever connects to an address that passes the
123
+ * guard, bundled with the exact predicate it was built with so a caller
124
+ * can't pass a mismatched one to the pre-dial literal-IP check in
125
+ * {@link createBoundedFetcher}. `tlsOpts` is exposed solely so tests can
126
+ * trust a local self-signed certificate; `isBlocked` lets tests run against
127
+ * loopback without disabling the guard mechanism itself. Production code
128
+ * must call {@link boundedClientDocumentFetcher}, which passes neither.
129
+ */
130
+ function buildGuardedAgent(tlsOpts) {
131
+ const { isBlocked = isBlockedAddress, ...rest } = tlsOpts ?? {};
132
+ return {
133
+ dispatcher: new Agent({
134
+ connect: { lookup: buildGuardedLookup(isBlocked), ...rest },
135
+ }),
136
+ isBlocked,
137
+ };
138
+ }
139
+ /** One dispatcher, reused across calls; the guard is stateless per lookup. */
140
+ const guarded = buildGuardedAgent();
141
+ /**
142
+ * Read a response body, aborting the moment it exceeds `limit` bytes.
143
+ * Mirrors `readCapped` in core's `declaration.ts` — same bound, same
144
+ * reasoning: `.text()` would buffer the whole body before the cap could act.
145
+ */
146
+ async function readCapped(response, limit) {
147
+ const stream = response.body;
148
+ if (!stream) {
149
+ const buffered = await response.text();
150
+ return Buffer.byteLength(buffered, "utf8") > limit ? null : buffered;
151
+ }
152
+ const reader = stream.getReader();
153
+ const chunks = [];
154
+ let total = 0;
155
+ try {
156
+ for (;;) {
157
+ const { done, value } = await reader.read();
158
+ if (done)
159
+ break;
160
+ if (!value)
161
+ continue;
162
+ total += value.byteLength;
163
+ if (total > limit) {
164
+ await reader.cancel();
165
+ return null;
166
+ }
167
+ chunks.push(value);
168
+ }
169
+ }
170
+ finally {
171
+ reader.releaseLock();
172
+ }
173
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
174
+ }
175
+ /**
176
+ * Build a bounded `ClientDocumentFetcher`. HTTPS-only, no redirects, one
177
+ * connect+headers+body timeout, a streamed byte cap, and address validation
178
+ * on every DNS answer *and* on a literal-IP hostname (which never reaches
179
+ * `dns.lookup`, so the dispatcher's guarded `lookup` alone would miss it).
180
+ *
181
+ * Takes {@link buildGuardedAgent}'s return value as a whole (not just its
182
+ * `dispatcher`) so the pre-dial literal-IP check always uses the exact same
183
+ * predicate the dispatcher's `lookup` guard was built with — there is no
184
+ * call shape that lets the two diverge.
185
+ */
186
+ function createBoundedFetcher({ dispatcher, isBlocked, }) {
187
+ return async (url) => {
188
+ const parsed = new URL(url);
189
+ if (parsed.protocol !== "https:") {
190
+ throw new Error(`client_id document fetch requires https, got ${parsed.protocol}`);
191
+ }
192
+ const literal = literalHostAddress(parsed.hostname);
193
+ if (literal && isBlocked(literal)) {
194
+ throw new BlockedAddressError(`client_id document fetch refuses a blocked literal address: ${literal}`);
195
+ }
196
+ const controller = new AbortController();
197
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
198
+ try {
199
+ const response = await undiciFetch(parsed.toString(), {
200
+ redirect: "manual",
201
+ signal: controller.signal,
202
+ dispatcher,
203
+ // No forwarded credentials: no cookies, no stored authorization.
204
+ });
205
+ if (response.status >= 300 && response.status < 400) {
206
+ await discardBody(response);
207
+ throw new Error(`client_id document fetch refuses to follow a redirect (status ${response.status})`);
208
+ }
209
+ const declaredLength = response.headers.get("content-length");
210
+ if (declaredLength && Number(declaredLength) > MAX_RESPONSE_BYTES) {
211
+ await discardBody(response);
212
+ return { status: response.status, body: "", finalUrl: url };
213
+ }
214
+ const body = await readCapped(response, MAX_RESPONSE_BYTES);
215
+ if (body === null) {
216
+ // Oversized: reported as a non-200 empty body so the resolver's
217
+ // generic "unexpected status" handling covers it without a new code
218
+ // path duplicating the size check `resolveUrlHostedClientIdentity`
219
+ // already performs on the string it receives.
220
+ return { status: 0, body: "", finalUrl: url };
221
+ }
222
+ return { status: response.status, body, finalUrl: parsed.toString() };
223
+ }
224
+ finally {
225
+ // Cleared last: the deadline must still apply while a rejected
226
+ // response's body is drained/destroyed above, not just during the
227
+ // initial connect+headers await.
228
+ clearTimeout(timer);
229
+ }
230
+ };
231
+ }
232
+ /**
233
+ * Drop a response body we're not going to read, rather than leaving it
234
+ * unconsumed. Called before `finally` clears the timer, so a hostile server
235
+ * stalling this drain still hits the request's own deadline instead of
236
+ * hanging past it.
237
+ */
238
+ async function discardBody(response) {
239
+ const stream = response.body;
240
+ if (!stream)
241
+ return;
242
+ try {
243
+ await stream.cancel();
244
+ }
245
+ catch {
246
+ // A destroyed/errored stream may reject cancel(); the socket is already
247
+ // gone either way.
248
+ }
249
+ }
250
+ /** The production fetcher: no test-only overrides reach this path. */
251
+ export const boundedClientDocumentFetcher = createBoundedFetcher(guarded);
252
+ /** Test-only seam: same bounds, with a dispatcher that trusts a test CA. */
253
+ export const __testing = { createBoundedFetcher, buildGuardedAgent };
254
+ //# sourceMappingURL=client-document-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-document-fetch.js","sourceRoot":"","sources":["../../src/pdpp/client-document-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAE,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AAKrD,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AACtC,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE5C,6EAA6E;AAC7E,MAAM,OAAO,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;AAC7C,KAAK,MAAM,IAAI,IAAI;IACjB,WAAW,EAAE,yCAAyC;IACtD,YAAY,EAAE,kBAAkB;IAChC,eAAe,EAAE,yCAAyC;IAC1D,aAAa,EAAE,WAAW;IAC1B,gBAAgB,EAAE,mCAAmC;IACrD,eAAe,EAAE,kBAAkB;IACnC,cAAc,EAAE,4BAA4B;IAC5C,cAAc,EAAE,6BAA6B;IAC7C,gBAAgB,EAAE,kBAAkB;IACpC,eAAe,EAAE,eAAe;IAChC,iBAAiB,EAAE,6BAA6B;IAChD,gBAAgB,EAAE,6BAA6B;IAC/C,aAAa,EAAE,YAAY;IAC3B,aAAa,EAAE,WAAW;IAC1B,oBAAoB,EAAE,oBAAoB;CAC3C,EAAE,CAAC;IACF,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1C,gBAAgB,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC;AACD,KAAK,MAAM,IAAI,IAAI;IACjB,QAAQ,EAAE,cAAc;IACxB,SAAS,EAAE,WAAW;IACtB,UAAU,EAAE,qBAAqB;IACjC,WAAW,EAAE,aAAa;IAC1B,UAAU,EAAE,YAAY;CACzB,EAAE,CAAC;IACF,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1C,gBAAgB,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,8CAA8C;IAC9E,OAAO,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CAAC,QAAgB;IAC1C,MAAM,WAAW,GACf,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAChD,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvB,CAAC,CAAC,QAAQ,CAAC;IACf,OAAO,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,kBAAkB,CACzB,YAA0C,gBAAgB;IAE1D,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;QACrC,uEAAuE;QACvE,wEAAwE;QACxE,0CAA0C;QAC1C,GAAG,CAAC,MAAM,CACR,QAAQ,EACR,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,EACzB,CAAC,GAAiC,EAAE,SAAS,EAAE,EAAE;YAC/C,IAAI,GAAG;gBAAE,OAAO,QAAQ,CAAC,GAAG,EAAE,EAAW,EAAE,SAAkB,CAAC,CAAC;YAC/D,MAAM,IAAI,GAAG,SAAkD,CAAC;YAChE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtB,OAAO,QAAQ,CACb,IAAI,mBAAmB,CACrB,GAAG,QAAQ,2BAA2B,CACd,EAC1B,EAAW,EACX,SAAkB,CACnB,CAAC;YACJ,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YACvD,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,QAAQ,CACb,IAAI,mBAAmB,CACrB,GAAG,QAAQ,mCAAmC,OAAO,CAAC,OAAO,EAAE,CACvC,EAC1B,EAAW,EACX,SAAkB,CACnB,CAAC;YACJ,CAAC;YACD,mEAAmE;YACnE,qEAAqE;YACrE,8CAA8C;YAC9C,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;gBAChB,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAa,EAAE,SAAkB,CAAC,CAAC;YAC3D,CAAC;YACD,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAE,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC,CACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,OAG1B;IACC,MAAM,EAAE,SAAS,GAAG,gBAAgB,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;IAChE,OAAO;QACL,UAAU,EAAE,IAAI,KAAK,CAAC;YACpB,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,CAAC,SAAS,CAAC,EAAE,GAAG,IAAI,EAAE;SAC5D,CAAC;QACF,SAAS;KACV,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;AAEpC;;;;GAIG;AACH,KAAK,UAAU,UAAU,CACvB,QAAwB,EACxB,KAAa;IAEb,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC7B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;IACvE,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;IAClC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;YAC1B,IAAI,KAAK,GAAG,KAAK,EAAE,CAAC;gBAClB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;gBACtB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,oBAAoB,CAAC,EAC5B,UAAU,EACV,SAAS,GAC4B;IACrC,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE;QACnB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACb,gDAAgD,MAAM,CAAC,QAAQ,EAAE,CAClE,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,mBAAmB,CAC3B,+DAA+D,OAAO,EAAE,CACzE,CAAC;QACJ,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,gBAAgB,CAAC,CAAC;QACrE,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE;gBACpD,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,UAAU;gBACV,iEAAiE;aAClE,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;gBACpD,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;gBAC5B,MAAM,IAAI,KAAK,CACb,iEAAiE,QAAQ,CAAC,MAAM,GAAG,CACpF,CAAC;YACJ,CAAC;YAED,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC9D,IAAI,cAAc,IAAI,MAAM,CAAC,cAAc,CAAC,GAAG,kBAAkB,EAAE,CAAC;gBAClE,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;gBAC5B,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;YAC9D,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;YAC5D,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,gEAAgE;gBAChE,oEAAoE;gBACpE,mEAAmE;gBACnE,8CAA8C;gBAC9C,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;YAChD,CAAC;YAED,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACxE,CAAC;gBAAS,CAAC;YACT,+DAA+D;YAC/D,kEAAkE;YAClE,iCAAiC;YACjC,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,WAAW,CAAC,QAAwB;IACjD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,mBAAmB;IACrB,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,MAAM,4BAA4B,GACvC,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAEhC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A declaration registry that can change while the server is running.
3
+ *
4
+ * `buildDeclarationRegistry` (deployment.ts) parses `config.pdpp
5
+ * .declarationPaths` once at construction and pins the survivors. That is the
6
+ * right shape for a deployment whose declarations are config, and it stays
7
+ * exactly as it was. What it cannot do is accept a candidate: offering one
8
+ * means editing config and restarting, so a caller cannot ask "would you
9
+ * accept this?" and get an answer in the same server lifetime.
10
+ *
11
+ * This is that acceptance surface's storage. Two properties it must have that
12
+ * the boot-time registry does not:
13
+ *
14
+ * - **Mutable in place.** The AS's `resolveDeclaration` lookup must see an
15
+ * accepted declaration immediately, or an acceptance is indistinguishable
16
+ * from a refusal until the next restart.
17
+ * - **Durable.** An operator who submitted a declaration must not silently
18
+ * lose it on restart, which would make acceptance look transactional and
19
+ * behave like a cache.
20
+ *
21
+ * ## Why this is a separate table from `pdpp_declarations`
22
+ *
23
+ * The auth store's `pdpp_declarations` retains a snapshot per
24
+ * `(source_id, version)` and is deliberately immutable — it is the evidence an
25
+ * ISSUED GRANT points at, and rewriting it would change what an owner
26
+ * consented to after the fact. This table answers a different question:
27
+ * *which* version is currently authoritative for a source. Those must not
28
+ * share a row, because accepting a new version has to move the current
29
+ * pointer while leaving every retained revision a live grant depends on
30
+ * exactly where it was.
31
+ */
32
+ import type { DeclarationFailure, DeclarationSnapshot } from "@opendatalabs/personal-server-ts-core/pdpp";
33
+ import type { Logger } from "pino";
34
+ export interface MutableDeclarationRegistry {
35
+ /** The live lookup the AS consumes. Null when nothing is retained. */
36
+ resolve(sourceId: string): DeclarationSnapshot | null;
37
+ /** The exact retained bytes for a source, for digest verification. */
38
+ documentFor(sourceId: string): string | null;
39
+ /** Every currently-authoritative declaration. */
40
+ list(): DeclarationSnapshot[];
41
+ /**
42
+ * Validate a candidate and, on success, make it authoritative for its
43
+ * source. A refusal changes nothing — in particular it must not displace an
44
+ * already-retained declaration, or one malformed submission would disarm a
45
+ * working grant surface.
46
+ */
47
+ submit(document: string): DeclarationSubmissionResult;
48
+ close(): void;
49
+ }
50
+ export type DeclarationSubmissionResult = {
51
+ ok: true;
52
+ snapshot: DeclarationSnapshot;
53
+ } | {
54
+ ok: false;
55
+ failure: DeclarationFailure;
56
+ };
57
+ export interface OpenDeclarationRegistryOptions {
58
+ /** SQLite file. Use `:memory:` for a registry that does not outlive it. */
59
+ path: string;
60
+ /**
61
+ * The connector inventory this PS serves. A SUBMITTED declaration naming a
62
+ * source this server holds no data for is refused: a grant over data that
63
+ * cannot exist here is at best useless, and at worst a consent screen
64
+ * showing an owner something they are not sharing. Seeds are not gated;
65
+ * see `seed`.
66
+ */
67
+ supportedConnectors: string[];
68
+ logger: Logger;
69
+ /**
70
+ * Declarations from `config.pdpp.declarationPaths`, seeded on open.
71
+ * Configuration is the operator's admission decision, so seeds skip the
72
+ * connector gate: a fresh server with no legacy data must still mount the
73
+ * sources it was configured for. Submitted declarations win over seeds for
74
+ * the same source: an operator's explicit later decision is the more
75
+ * recent one.
76
+ */
77
+ seed?: {
78
+ sourceId: string;
79
+ document: string;
80
+ }[];
81
+ }
82
+ export declare function openDeclarationRegistry(options: OpenDeclarationRegistryOptions): MutableDeclarationRegistry;
83
+ //# sourceMappingURL=declaration-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declaration-registry.d.ts","sourceRoot":"","sources":["../../src/pdpp/declaration-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAyBnC,MAAM,WAAW,0BAA0B;IACzC,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAAC;IACtD,sEAAsE;IACtE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC7C,iDAAiD;IACjD,IAAI,IAAI,mBAAmB,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,2BAA2B,CAAC;IACtD,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,MAAM,2BAA2B,GACnC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAA;CAAE,GAC3C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,8BAA8B;IAC7C,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjD;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,8BAA8B,GACtC,0BAA0B,CAkK5B"}
@@ -0,0 +1,251 @@
1
+ /**
2
+ * A declaration registry that can change while the server is running.
3
+ *
4
+ * `buildDeclarationRegistry` (deployment.ts) parses `config.pdpp
5
+ * .declarationPaths` once at construction and pins the survivors. That is the
6
+ * right shape for a deployment whose declarations are config, and it stays
7
+ * exactly as it was. What it cannot do is accept a candidate: offering one
8
+ * means editing config and restarting, so a caller cannot ask "would you
9
+ * accept this?" and get an answer in the same server lifetime.
10
+ *
11
+ * This is that acceptance surface's storage. Two properties it must have that
12
+ * the boot-time registry does not:
13
+ *
14
+ * - **Mutable in place.** The AS's `resolveDeclaration` lookup must see an
15
+ * accepted declaration immediately, or an acceptance is indistinguishable
16
+ * from a refusal until the next restart.
17
+ * - **Durable.** An operator who submitted a declaration must not silently
18
+ * lose it on restart, which would make acceptance look transactional and
19
+ * behave like a cache.
20
+ *
21
+ * ## Why this is a separate table from `pdpp_declarations`
22
+ *
23
+ * The auth store's `pdpp_declarations` retains a snapshot per
24
+ * `(source_id, version)` and is deliberately immutable — it is the evidence an
25
+ * ISSUED GRANT points at, and rewriting it would change what an owner
26
+ * consented to after the fact. This table answers a different question:
27
+ * *which* version is currently authoritative for a source. Those must not
28
+ * share a row, because accepting a new version has to move the current
29
+ * pointer while leaving every retained revision a live grant depends on
30
+ * exactly where it was.
31
+ */
32
+ import Database from "better-sqlite3";
33
+ import { isDeepStrictEqual } from "node:util";
34
+ import { parseDeclaration } from "@opendatalabs/personal-server-ts-core/pdpp";
35
+ const SCHEMA_SQL = `
36
+ CREATE TABLE IF NOT EXISTS pdpp_current_declarations (
37
+ source_id TEXT NOT NULL,
38
+ version TEXT NOT NULL,
39
+ digest TEXT NOT NULL,
40
+ snapshot_json TEXT NOT NULL,
41
+ -- The exact submitted bytes. snapshot_json is the parsed projection, so
42
+ -- re-digesting it does NOT reproduce digest; a producer's claimed digest
43
+ -- can only honestly be checked against what was actually submitted.
44
+ document TEXT NOT NULL,
45
+ accepted_at TEXT NOT NULL,
46
+ is_current INTEGER NOT NULL DEFAULT 1,
47
+ -- 'configured' (seeded from config at boot) or 'submitted' (runtime
48
+ -- route). Boot config follows the current configured declaration, but
49
+ -- never displaces a submission.
50
+ origin TEXT NOT NULL DEFAULT 'configured',
51
+ PRIMARY KEY (source_id, version)
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS pdpp_current_declarations_current_idx
55
+ ON pdpp_current_declarations (source_id, is_current);
56
+ `;
57
+ export function openDeclarationRegistry(options) {
58
+ const db = new Database(options.path);
59
+ db.pragma("journal_mode = WAL");
60
+ migrateSchema(db);
61
+ const supported = new Set(options.supportedConnectors);
62
+ function accept(document, admission) {
63
+ // The submitted document names its own source, so there is no separately
64
+ // supplied id to cross-check it against. Reading the claim first and
65
+ // parsing against it means `source_id_mismatch` stays reachable for a
66
+ // document whose nested `source.id` disagrees with its normalized form,
67
+ // rather than being unreachable here by construction.
68
+ const claimed = claimedSourceId(document);
69
+ if (claimed === null) {
70
+ return {
71
+ ok: false,
72
+ failure: {
73
+ code: "invalid_document",
74
+ message: "declaration is not valid JSON, or declares no source id",
75
+ },
76
+ };
77
+ }
78
+ const parsed = parseDeclaration(document, claimed);
79
+ if (!parsed.ok)
80
+ return parsed;
81
+ const connector = connectorNameFor(parsed.snapshot.source_id);
82
+ if (admission === "submitted" && !supported.has(connector)) {
83
+ // `untrusted_source` rather than `invalid_document`: the document may
84
+ // be perfectly well formed. What is refused is its authority here.
85
+ return {
86
+ ok: false,
87
+ failure: {
88
+ code: "untrusted_source",
89
+ message: `this server does not serve the '${connector}' connector, so it does not accept a declaration for it`,
90
+ },
91
+ };
92
+ }
93
+ const existing = db
94
+ .prepare(`SELECT snapshot_json, is_current FROM pdpp_current_declarations
95
+ WHERE source_id = ? AND version = ?`)
96
+ .get(parsed.snapshot.source_id, parsed.snapshot.version);
97
+ if (existing) {
98
+ const retained = JSON.parse(existing.snapshot_json);
99
+ if (!sameDeclarationContent(retained, parsed.snapshot)) {
100
+ return {
101
+ ok: false,
102
+ failure: {
103
+ code: "declaration_equivocation",
104
+ message: "different content was already accepted under this (source.id, declaration_version) key",
105
+ },
106
+ };
107
+ }
108
+ // Configuration names the version that is current now, so a
109
+ // configured downgrade re-points to the retained earlier revision.
110
+ if (admission === "configured" && !existing.is_current) {
111
+ db.transaction(() => {
112
+ db.prepare("UPDATE pdpp_current_declarations SET is_current = 0 WHERE source_id = ?").run(parsed.snapshot.source_id);
113
+ db.prepare(`UPDATE pdpp_current_declarations SET is_current = 1
114
+ WHERE source_id = ? AND version = ?`).run(parsed.snapshot.source_id, parsed.snapshot.version);
115
+ })();
116
+ }
117
+ return { ok: true, snapshot: retained };
118
+ }
119
+ db.transaction(() => {
120
+ db.prepare("UPDATE pdpp_current_declarations SET is_current = 0 WHERE source_id = ?").run(parsed.snapshot.source_id);
121
+ db.prepare(`INSERT INTO pdpp_current_declarations
122
+ (source_id, version, digest, snapshot_json, document, accepted_at, is_current, origin)
123
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?)`).run(parsed.snapshot.source_id, parsed.snapshot.version, parsed.snapshot.digest, JSON.stringify(parsed.snapshot), document, new Date().toISOString(), admission);
124
+ })();
125
+ return { ok: true, snapshot: parsed.snapshot };
126
+ }
127
+ // Seeds go through the same `accept` as a submission — one acceptance path,
128
+ // so a configured declaration and a submitted one cannot diverge on what
129
+ // counts as valid. A seed for a source whose current declaration was
130
+ // submitted is skipped: the operator's later explicit decision wins over
131
+ // boot config. Otherwise the seed becomes current, so an installer that
132
+ // upgrades a declaration and restarts the server is followed.
133
+ for (const seed of options.seed ?? []) {
134
+ const submitted = db
135
+ .prepare(`SELECT 1 FROM pdpp_current_declarations
136
+ WHERE source_id = ? AND is_current = 1 AND origin = 'submitted'`)
137
+ .get(seed.sourceId);
138
+ if (submitted)
139
+ continue;
140
+ const result = accept(seed.document, "configured");
141
+ if (!result.ok) {
142
+ options.logger.warn({ sourceId: seed.sourceId, reason: result.failure.message }, "PDPP declaration rejected: not retained");
143
+ }
144
+ }
145
+ return {
146
+ resolve(sourceId) {
147
+ const row = db
148
+ .prepare(`SELECT snapshot_json FROM pdpp_current_declarations
149
+ WHERE source_id = ? AND is_current = 1`)
150
+ .get(sourceId);
151
+ return row
152
+ ? JSON.parse(row.snapshot_json)
153
+ : null;
154
+ },
155
+ documentFor(sourceId) {
156
+ const row = db
157
+ .prepare(`SELECT document FROM pdpp_current_declarations
158
+ WHERE source_id = ? AND is_current = 1`)
159
+ .get(sourceId);
160
+ return row?.document ?? null;
161
+ },
162
+ list() {
163
+ const rows = db
164
+ .prepare(`SELECT snapshot_json FROM pdpp_current_declarations
165
+ WHERE is_current = 1 ORDER BY source_id`)
166
+ .all();
167
+ return rows.map((r) => JSON.parse(r.snapshot_json));
168
+ },
169
+ submit: (document) => accept(document, "submitted"),
170
+ close() {
171
+ db.close();
172
+ },
173
+ };
174
+ }
175
+ function migrateSchema(db) {
176
+ const columns = db
177
+ .prepare("PRAGMA table_info(pdpp_current_declarations)")
178
+ .all();
179
+ if (columns.length === 0) {
180
+ db.exec(SCHEMA_SQL);
181
+ return;
182
+ }
183
+ if (columns.some((column) => column.name === "is_current")) {
184
+ if (!columns.some((column) => column.name === "origin")) {
185
+ // Rows written before `origin` existed cannot be told apart. Treating
186
+ // them as configured lets boot config move past them; a submission
187
+ // that should outrank config must be submitted again.
188
+ db.exec("ALTER TABLE pdpp_current_declarations ADD COLUMN origin TEXT NOT NULL DEFAULT 'configured'");
189
+ }
190
+ db.exec(SCHEMA_SQL);
191
+ return;
192
+ }
193
+ db.transaction(() => {
194
+ db.exec("ALTER TABLE pdpp_current_declarations RENAME TO pdpp_current_declarations_legacy");
195
+ db.exec(SCHEMA_SQL);
196
+ db.exec(`INSERT INTO pdpp_current_declarations
197
+ (source_id, version, digest, snapshot_json, document, accepted_at, is_current)
198
+ SELECT source_id, version, digest, snapshot_json, document, accepted_at, 1
199
+ FROM pdpp_current_declarations_legacy`);
200
+ db.exec("DROP TABLE pdpp_current_declarations_legacy");
201
+ })();
202
+ }
203
+ function sameDeclarationContent(a, b) {
204
+ const { digest: _aDigest, ...aContent } = a;
205
+ const { digest: _bDigest, ...bContent } = b;
206
+ return isDeepStrictEqual(aContent, bContent);
207
+ }
208
+ /**
209
+ * The source id a document claims, read without validating anything else.
210
+ *
211
+ * Accepts both the normative nested shape (`source: { id }`) and the internal
212
+ * flat one (`source_id`), because `parseDeclaration` normalizes between them
213
+ * and this has to agree with whichever it will read.
214
+ */
215
+ function claimedSourceId(document) {
216
+ let doc;
217
+ try {
218
+ doc = JSON.parse(document);
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ if (typeof doc !== "object" || doc === null)
224
+ return null;
225
+ const record = doc;
226
+ const nested = record.source?.id;
227
+ if (typeof nested === "string" && nested.length > 0)
228
+ return nested;
229
+ if (typeof record.source_id === "string" && record.source_id.length > 0) {
230
+ return record.source_id;
231
+ }
232
+ return null;
233
+ }
234
+ /**
235
+ * The connector a source id belongs to — the last path segment of the URI
236
+ * (`https://registry.pdpp.dev/connectors/spotify` → `spotify`). Falls back to
237
+ * the whole id for an unparseable value, so an odd source fails the supported
238
+ * check rather than silently matching a connector it is not.
239
+ */
240
+ function connectorNameFor(sourceId) {
241
+ try {
242
+ const segments = new URL(sourceId).pathname
243
+ .split("/")
244
+ .filter((s) => s.length > 0);
245
+ return segments[segments.length - 1] ?? sourceId;
246
+ }
247
+ catch {
248
+ return sourceId;
249
+ }
250
+ }
251
+ //# sourceMappingURL=declaration-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declaration-registry.js","sourceRoot":"","sources":["../../src/pdpp/declaration-registry.ts"],"names":[],"mappings":"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"}