@opendatalabs/personal-server-ts-core 1.33.8 → 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 (330) hide show
  1. package/dist/additions/budget.d.ts +28 -0
  2. package/dist/additions/budget.d.ts.map +1 -0
  3. package/dist/additions/budget.js +119 -0
  4. package/dist/additions/budget.js.map +1 -0
  5. package/dist/additions/durable.d.ts +64 -0
  6. package/dist/additions/durable.d.ts.map +1 -0
  7. package/dist/additions/durable.js +591 -0
  8. package/dist/additions/durable.js.map +1 -0
  9. package/dist/additions/first-added.d.ts +32 -48
  10. package/dist/additions/first-added.d.ts.map +1 -1
  11. package/dist/additions/first-added.js +68 -123
  12. package/dist/additions/first-added.js.map +1 -1
  13. package/dist/additions/index.d.ts +3 -0
  14. package/dist/additions/index.d.ts.map +1 -1
  15. package/dist/additions/index.js +3 -0
  16. package/dist/additions/index.js.map +1 -1
  17. package/dist/additions/ledger-store.js +1 -1
  18. package/dist/additions/record-rules.d.ts.map +1 -1
  19. package/dist/additions/record-rules.js +7 -0
  20. package/dist/additions/record-rules.js.map +1 -1
  21. package/dist/additions/summary.d.ts +88 -1
  22. package/dist/additions/summary.d.ts.map +1 -1
  23. package/dist/additions/summary.js +173 -8
  24. package/dist/additions/summary.js.map +1 -1
  25. package/dist/additions/version-inventory.d.ts +80 -0
  26. package/dist/additions/version-inventory.d.ts.map +1 -0
  27. package/dist/additions/version-inventory.js +232 -0
  28. package/dist/additions/version-inventory.js.map +1 -0
  29. package/dist/api/index.d.ts +70 -2
  30. package/dist/api/index.d.ts.map +1 -1
  31. package/dist/api/index.js +506 -79
  32. package/dist/api/index.js.map +1 -1
  33. package/dist/auth/index.d.ts +1 -1
  34. package/dist/auth/index.d.ts.map +1 -1
  35. package/dist/auth/index.js +1 -1
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/auth/request.d.ts +6 -0
  38. package/dist/auth/request.d.ts.map +1 -1
  39. package/dist/auth/request.js +66 -4
  40. package/dist/auth/request.js.map +1 -1
  41. package/dist/contracts/data.d.ts +54 -8
  42. package/dist/contracts/data.d.ts.map +1 -1
  43. package/dist/contracts/data.js +178 -35
  44. package/dist/contracts/data.js.map +1 -1
  45. package/dist/contracts/index.d.ts +1 -1
  46. package/dist/contracts/index.d.ts.map +1 -1
  47. package/dist/derivatives/registration.d.ts +0 -5
  48. package/dist/derivatives/registration.d.ts.map +1 -1
  49. package/dist/derivatives/registration.js +2 -1
  50. package/dist/derivatives/registration.js.map +1 -1
  51. package/dist/errors/pdpp-catalog.d.ts +36 -0
  52. package/dist/errors/pdpp-catalog.d.ts.map +1 -0
  53. package/dist/errors/pdpp-catalog.js +86 -0
  54. package/dist/errors/pdpp-catalog.js.map +1 -0
  55. package/dist/grants/index.d.ts +2 -0
  56. package/dist/grants/index.d.ts.map +1 -1
  57. package/dist/grants/index.js +4 -0
  58. package/dist/grants/index.js.map +1 -1
  59. package/dist/grants/pdpp-binding-store.d.ts +89 -0
  60. package/dist/grants/pdpp-binding-store.d.ts.map +1 -0
  61. package/dist/grants/pdpp-binding-store.js +112 -0
  62. package/dist/grants/pdpp-binding-store.js.map +1 -0
  63. package/dist/grants/pdpp-binding.d.ts +169 -0
  64. package/dist/grants/pdpp-binding.d.ts.map +1 -0
  65. package/dist/grants/pdpp-binding.js +236 -0
  66. package/dist/grants/pdpp-binding.js.map +1 -0
  67. package/dist/legacy-projection/bindings.d.ts +28 -1
  68. package/dist/legacy-projection/bindings.d.ts.map +1 -1
  69. package/dist/legacy-projection/bindings.js +622 -189
  70. package/dist/legacy-projection/bindings.js.map +1 -1
  71. package/dist/legacy-projection/claude-source-record.d.ts +22 -0
  72. package/dist/legacy-projection/claude-source-record.d.ts.map +1 -0
  73. package/dist/legacy-projection/claude-source-record.js +360 -0
  74. package/dist/legacy-projection/claude-source-record.js.map +1 -0
  75. package/dist/legacy-projection/declared-text.d.ts +41 -0
  76. package/dist/legacy-projection/declared-text.d.ts.map +1 -0
  77. package/dist/legacy-projection/declared-text.js +199 -0
  78. package/dist/legacy-projection/declared-text.js.map +1 -0
  79. package/dist/legacy-projection/github-browser-bindings.d.ts.map +1 -1
  80. package/dist/legacy-projection/github-browser-bindings.js +89 -5
  81. package/dist/legacy-projection/github-browser-bindings.js.map +1 -1
  82. package/dist/legacy-projection/github-legacy-fields.d.ts +6 -0
  83. package/dist/legacy-projection/github-legacy-fields.d.ts.map +1 -0
  84. package/dist/legacy-projection/github-legacy-fields.js +202 -0
  85. package/dist/legacy-projection/github-legacy-fields.js.map +1 -0
  86. package/dist/legacy-projection/index.d.ts +3 -1
  87. package/dist/legacy-projection/index.d.ts.map +1 -1
  88. package/dist/legacy-projection/index.js +49 -1
  89. package/dist/legacy-projection/index.js.map +1 -1
  90. package/dist/legacy-projection/oura-browser-bindings.d.ts.map +1 -1
  91. package/dist/legacy-projection/oura-browser-bindings.js +43 -41
  92. package/dist/legacy-projection/oura-browser-bindings.js.map +1 -1
  93. package/dist/legacy-projection/types.d.ts +38 -2
  94. package/dist/legacy-projection/types.d.ts.map +1 -1
  95. package/dist/mcp/resources.d.ts.map +1 -1
  96. package/dist/mcp/resources.js +3 -0
  97. package/dist/mcp/resources.js.map +1 -1
  98. package/dist/mcp/tools.d.ts.map +1 -1
  99. package/dist/mcp/tools.js +11 -2
  100. package/dist/mcp/tools.js.map +1 -1
  101. package/dist/pdpp/approval.d.ts +271 -0
  102. package/dist/pdpp/approval.d.ts.map +1 -0
  103. package/dist/pdpp/approval.js +467 -0
  104. package/dist/pdpp/approval.js.map +1 -0
  105. package/dist/pdpp/client-identity.d.ts +88 -0
  106. package/dist/pdpp/client-identity.d.ts.map +1 -0
  107. package/dist/pdpp/client-identity.js +196 -0
  108. package/dist/pdpp/client-identity.js.map +1 -0
  109. package/dist/pdpp/client-metadata.d.ts +95 -0
  110. package/dist/pdpp/client-metadata.d.ts.map +1 -0
  111. package/dist/pdpp/client-metadata.js +96 -0
  112. package/dist/pdpp/client-metadata.js.map +1 -0
  113. package/dist/pdpp/commitments.d.ts +75 -0
  114. package/dist/pdpp/commitments.d.ts.map +1 -0
  115. package/dist/pdpp/commitments.js +113 -0
  116. package/dist/pdpp/commitments.js.map +1 -0
  117. package/dist/pdpp/declaration.d.ts +118 -0
  118. package/dist/pdpp/declaration.d.ts.map +1 -0
  119. package/dist/pdpp/declaration.js +836 -0
  120. package/dist/pdpp/declaration.js.map +1 -0
  121. package/dist/pdpp/index.d.ts +37 -0
  122. package/dist/pdpp/index.d.ts.map +1 -0
  123. package/dist/pdpp/index.js +23 -0
  124. package/dist/pdpp/index.js.map +1 -0
  125. package/dist/pdpp/issuance.d.ts +126 -0
  126. package/dist/pdpp/issuance.d.ts.map +1 -0
  127. package/dist/pdpp/issuance.js +180 -0
  128. package/dist/pdpp/issuance.js.map +1 -0
  129. package/dist/pdpp/pkce.d.ts +53 -0
  130. package/dist/pdpp/pkce.d.ts.map +1 -0
  131. package/dist/pdpp/pkce.js +109 -0
  132. package/dist/pdpp/pkce.js.map +1 -0
  133. package/dist/pdpp/redirect.d.ts +60 -0
  134. package/dist/pdpp/redirect.d.ts.map +1 -0
  135. package/dist/pdpp/redirect.js +125 -0
  136. package/dist/pdpp/redirect.js.map +1 -0
  137. package/dist/pdpp/resolve.d.ts +109 -0
  138. package/dist/pdpp/resolve.d.ts.map +1 -0
  139. package/dist/pdpp/resolve.js +393 -0
  140. package/dist/pdpp/resolve.js.map +1 -0
  141. package/dist/pdpp/review.d.ts +280 -0
  142. package/dist/pdpp/review.d.ts.map +1 -0
  143. package/dist/pdpp/review.js +205 -0
  144. package/dist/pdpp/review.js.map +1 -0
  145. package/dist/pdpp/selection.d.ts +50 -0
  146. package/dist/pdpp/selection.d.ts.map +1 -0
  147. package/dist/pdpp/selection.js +442 -0
  148. package/dist/pdpp/selection.js.map +1 -0
  149. package/dist/pdpp/store.d.ts +228 -0
  150. package/dist/pdpp/store.d.ts.map +1 -0
  151. package/dist/pdpp/store.js +542 -0
  152. package/dist/pdpp/store.js.map +1 -0
  153. package/dist/pdpp/tokens.d.ts +181 -0
  154. package/dist/pdpp/tokens.d.ts.map +1 -0
  155. package/dist/pdpp/tokens.js +419 -0
  156. package/dist/pdpp/tokens.js.map +1 -0
  157. package/dist/pdpp/types.d.ts +441 -0
  158. package/dist/pdpp/types.d.ts.map +1 -0
  159. package/dist/pdpp/types.js +61 -0
  160. package/dist/pdpp/types.js.map +1 -0
  161. package/dist/pdpp-version.d.ts +28 -0
  162. package/dist/pdpp-version.d.ts.map +1 -0
  163. package/dist/pdpp-version.js +28 -0
  164. package/dist/pdpp-version.js.map +1 -0
  165. package/dist/policy/data-read.d.ts +24 -0
  166. package/dist/policy/data-read.d.ts.map +1 -1
  167. package/dist/policy/data-read.js +60 -3
  168. package/dist/policy/data-read.js.map +1 -1
  169. package/dist/policy/data-write.d.ts.map +1 -1
  170. package/dist/policy/data-write.js +3 -0
  171. package/dist/policy/data-write.js.map +1 -1
  172. package/dist/policy/index.d.ts +1 -0
  173. package/dist/policy/index.d.ts.map +1 -1
  174. package/dist/policy/index.js +1 -0
  175. package/dist/policy/index.js.map +1 -1
  176. package/dist/policy/reserved-scopes.d.ts +3 -0
  177. package/dist/policy/reserved-scopes.d.ts.map +1 -0
  178. package/dist/policy/reserved-scopes.js +5 -0
  179. package/dist/policy/reserved-scopes.js.map +1 -0
  180. package/dist/ports/index.d.ts +63 -0
  181. package/dist/ports/index.d.ts.map +1 -1
  182. package/dist/ports/index.js.map +1 -1
  183. package/dist/ports/pdpp-auth.d.ts +129 -0
  184. package/dist/ports/pdpp-auth.d.ts.map +1 -0
  185. package/dist/ports/pdpp-auth.js +60 -0
  186. package/dist/ports/pdpp-auth.js.map +1 -0
  187. package/dist/ports/pdpp-auth.test-utils.d.ts +9 -0
  188. package/dist/ports/pdpp-auth.test-utils.d.ts.map +1 -0
  189. package/dist/ports/pdpp-auth.test-utils.js +18 -0
  190. package/dist/ports/pdpp-auth.test-utils.js.map +1 -0
  191. package/dist/schemas/server-config.d.ts +44 -0
  192. package/dist/schemas/server-config.d.ts.map +1 -1
  193. package/dist/schemas/server-config.js +91 -0
  194. package/dist/schemas/server-config.js.map +1 -1
  195. package/dist/signing/request-signer.d.ts +2 -0
  196. package/dist/signing/request-signer.d.ts.map +1 -1
  197. package/dist/signing/request-signer.js +1 -0
  198. package/dist/signing/request-signer.js.map +1 -1
  199. package/dist/storage/adapters/interface.d.ts +3 -0
  200. package/dist/storage/adapters/interface.d.ts.map +1 -1
  201. package/dist/storage/adapters/vana-audience.d.ts +12 -0
  202. package/dist/storage/adapters/vana-audience.d.ts.map +1 -0
  203. package/dist/storage/adapters/vana-audience.js +72 -0
  204. package/dist/storage/adapters/vana-audience.js.map +1 -0
  205. package/dist/storage/adapters/vana.d.ts +2 -0
  206. package/dist/storage/adapters/vana.d.ts.map +1 -1
  207. package/dist/storage/adapters/vana.js +53 -20
  208. package/dist/storage/adapters/vana.js.map +1 -1
  209. package/dist/storage/canonical-legacy-projection.d.ts +262 -0
  210. package/dist/storage/canonical-legacy-projection.d.ts.map +1 -0
  211. package/dist/storage/canonical-legacy-projection.js +761 -0
  212. package/dist/storage/canonical-legacy-projection.js.map +1 -0
  213. package/dist/storage/index/manager.d.ts +29 -2
  214. package/dist/storage/index/manager.d.ts.map +1 -1
  215. package/dist/storage/index/schema.d.ts +1 -1
  216. package/dist/storage/index/schema.js +1 -1
  217. package/dist/storage/index/types.d.ts +7 -1
  218. package/dist/storage/index/types.d.ts.map +1 -1
  219. package/dist/storage/legacy-projection.d.ts +4 -3
  220. package/dist/storage/legacy-projection.d.ts.map +1 -1
  221. package/dist/storage/legacy-projection.js +145 -42
  222. package/dist/storage/legacy-projection.js.map +1 -1
  223. package/dist/storage/legacy-scope-coverage-exceptions.d.ts +8 -0
  224. package/dist/storage/legacy-scope-coverage-exceptions.d.ts.map +1 -0
  225. package/dist/storage/legacy-scope-coverage-exceptions.js +10 -0
  226. package/dist/storage/legacy-scope-coverage-exceptions.js.map +1 -0
  227. package/dist/storage/legacy-scope-coverage.d.ts +53 -0
  228. package/dist/storage/legacy-scope-coverage.d.ts.map +1 -0
  229. package/dist/storage/legacy-scope-coverage.js +403 -0
  230. package/dist/storage/legacy-scope-coverage.js.map +1 -0
  231. package/dist/storage/legacy-scope-entity-keys.d.ts +4 -0
  232. package/dist/storage/legacy-scope-entity-keys.d.ts.map +1 -0
  233. package/dist/storage/legacy-scope-entity-keys.js +92 -0
  234. package/dist/storage/legacy-scope-entity-keys.js.map +1 -0
  235. package/dist/storage/legacy-scope-view.d.ts +154 -0
  236. package/dist/storage/legacy-scope-view.d.ts.map +1 -0
  237. package/dist/storage/legacy-scope-view.js +735 -0
  238. package/dist/storage/legacy-scope-view.js.map +1 -0
  239. package/dist/storage/pdpp-records/completion.d.ts +91 -0
  240. package/dist/storage/pdpp-records/completion.d.ts.map +1 -0
  241. package/dist/storage/pdpp-records/completion.js +170 -0
  242. package/dist/storage/pdpp-records/completion.js.map +1 -0
  243. package/dist/storage/pdpp-records/cursor.d.ts +41 -0
  244. package/dist/storage/pdpp-records/cursor.d.ts.map +1 -0
  245. package/dist/storage/pdpp-records/cursor.js +49 -0
  246. package/dist/storage/pdpp-records/cursor.js.map +1 -0
  247. package/dist/storage/pdpp-records/declaration-witness.d.ts +58 -0
  248. package/dist/storage/pdpp-records/declaration-witness.d.ts.map +1 -0
  249. package/dist/storage/pdpp-records/declaration-witness.js +230 -0
  250. package/dist/storage/pdpp-records/declaration-witness.js.map +1 -0
  251. package/dist/storage/pdpp-records/enforcement.d.ts +52 -0
  252. package/dist/storage/pdpp-records/enforcement.d.ts.map +1 -0
  253. package/dist/storage/pdpp-records/enforcement.js +116 -0
  254. package/dist/storage/pdpp-records/enforcement.js.map +1 -0
  255. package/dist/storage/pdpp-records/index.d.ts +5 -0
  256. package/dist/storage/pdpp-records/index.d.ts.map +1 -0
  257. package/dist/storage/pdpp-records/index.js +5 -0
  258. package/dist/storage/pdpp-records/index.js.map +1 -0
  259. package/dist/storage/pdpp-records/ingest-plan.d.ts +39 -0
  260. package/dist/storage/pdpp-records/ingest-plan.d.ts.map +1 -0
  261. package/dist/storage/pdpp-records/ingest-plan.js +103 -0
  262. package/dist/storage/pdpp-records/ingest-plan.js.map +1 -0
  263. package/dist/storage/pdpp-records/interface.d.ts +104 -0
  264. package/dist/storage/pdpp-records/interface.d.ts.map +1 -0
  265. package/dist/storage/pdpp-records/interface.js +2 -0
  266. package/dist/storage/pdpp-records/interface.js.map +1 -0
  267. package/dist/storage/pdpp-records/memory-store.d.ts +9 -0
  268. package/dist/storage/pdpp-records/memory-store.d.ts.map +1 -0
  269. package/dist/storage/pdpp-records/memory-store.js +466 -0
  270. package/dist/storage/pdpp-records/memory-store.js.map +1 -0
  271. package/dist/storage/pdpp-records/portable.d.ts +9 -0
  272. package/dist/storage/pdpp-records/portable.d.ts.map +1 -0
  273. package/dist/storage/pdpp-records/portable.js +8 -0
  274. package/dist/storage/pdpp-records/portable.js.map +1 -0
  275. package/dist/storage/pdpp-records/read-port.d.ts +44 -0
  276. package/dist/storage/pdpp-records/read-port.d.ts.map +1 -0
  277. package/dist/storage/pdpp-records/read-port.js +22 -0
  278. package/dist/storage/pdpp-records/read-port.js.map +1 -0
  279. package/dist/storage/pdpp-records/record-key.d.ts +52 -0
  280. package/dist/storage/pdpp-records/record-key.d.ts.map +1 -0
  281. package/dist/storage/pdpp-records/record-key.js +97 -0
  282. package/dist/storage/pdpp-records/record-key.js.map +1 -0
  283. package/dist/storage/pdpp-records/schema.d.ts +41 -0
  284. package/dist/storage/pdpp-records/schema.d.ts.map +1 -0
  285. package/dist/storage/pdpp-records/schema.js +153 -0
  286. package/dist/storage/pdpp-records/schema.js.map +1 -0
  287. package/dist/storage/pdpp-records/stream-declaration.d.ts +88 -0
  288. package/dist/storage/pdpp-records/stream-declaration.d.ts.map +1 -0
  289. package/dist/storage/pdpp-records/stream-declaration.js +57 -0
  290. package/dist/storage/pdpp-records/stream-declaration.js.map +1 -0
  291. package/dist/storage/pdpp-records/types.d.ts +138 -0
  292. package/dist/storage/pdpp-records/types.d.ts.map +1 -0
  293. package/dist/storage/pdpp-records/types.js +38 -0
  294. package/dist/storage/pdpp-records/types.js.map +1 -0
  295. package/dist/sync/data-point-feed.d.ts +3 -0
  296. package/dist/sync/data-point-feed.d.ts.map +1 -1
  297. package/dist/sync/data-point-feed.js +17 -1
  298. package/dist/sync/data-point-feed.js.map +1 -1
  299. package/dist/sync/delete-data-port.d.ts +2 -0
  300. package/dist/sync/delete-data-port.d.ts.map +1 -1
  301. package/dist/sync/delete-data-port.js +1 -1
  302. package/dist/sync/delete-data-port.js.map +1 -1
  303. package/dist/sync/engine/sync-manager.d.ts.map +1 -1
  304. package/dist/sync/engine/sync-manager.js +5 -2
  305. package/dist/sync/engine/sync-manager.js.map +1 -1
  306. package/dist/sync/index.d.ts +3 -1
  307. package/dist/sync/index.d.ts.map +1 -1
  308. package/dist/sync/index.js +4 -0
  309. package/dist/sync/index.js.map +1 -1
  310. package/dist/sync/pdpp-import.d.ts +192 -0
  311. package/dist/sync/pdpp-import.d.ts.map +1 -0
  312. package/dist/sync/pdpp-import.js +441 -0
  313. package/dist/sync/pdpp-import.js.map +1 -0
  314. package/dist/sync/scope-deletions.d.ts +10 -0
  315. package/dist/sync/scope-deletions.d.ts.map +1 -1
  316. package/dist/sync/scope-deletions.js +40 -7
  317. package/dist/sync/scope-deletions.js.map +1 -1
  318. package/dist/sync/workers/download.d.ts +14 -0
  319. package/dist/sync/workers/download.d.ts.map +1 -1
  320. package/dist/sync/workers/download.js +149 -13
  321. package/dist/sync/workers/download.js.map +1 -1
  322. package/dist/sync/workers/upload.d.ts +2 -0
  323. package/dist/sync/workers/upload.d.ts.map +1 -1
  324. package/dist/sync/workers/upload.js +7 -0
  325. package/dist/sync/workers/upload.js.map +1 -1
  326. package/dist/test-utils/memory-storage.d.ts +1 -1
  327. package/dist/test-utils/memory-storage.d.ts.map +1 -1
  328. package/dist/test-utils/memory-storage.js +11 -5
  329. package/dist/test-utils/memory-storage.js.map +1 -1
  330. package/package.json +54 -4
@@ -0,0 +1,836 @@
1
+ /**
2
+ * Bounded SourceDeclaration retrieval (§5 declaration acceptance, §10
3
+ * declaration retrieval hygiene).
4
+ *
5
+ * An AS that fetches a client-supplied URL is an SSRF engine unless it is
6
+ * fenced. The rules here are the fence: HTTPS only, no private or loopback or
7
+ * link-local address literals, a bounded redirect chain that is re-checked at
8
+ * every hop, a response size cap, and a timeout. Each redirect is validated
9
+ * again because a public hostname resolving to a public IP can redirect to
10
+ * `http://169.254.169.254/` and a one-shot check at the start would miss it.
11
+ *
12
+ * What this module deliberately does NOT do is decide whether a declaration is
13
+ * *trusted*. That is deployment policy (`DeclarationTrustPolicy`), applied
14
+ * explicitly by the caller, because "we fetched it safely" and "we accept its
15
+ * authority over this source" are different questions.
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { isIP } from "node:net";
19
+ export const MAX_DECLARATION_BYTES = 256 * 1024;
20
+ export const MAX_REDIRECTS = 3;
21
+ export const DECLARATION_FETCH_TIMEOUT_MS = 5_000;
22
+ /**
23
+ * Private, loopback, link-local, and unique-local ranges. Checked against
24
+ * address *literals* in the URL. A hostname that resolves into these ranges is
25
+ * a DNS-rebinding concern the fetch layer must also handle; we reject the
26
+ * literal form here and keep the redirect chain bounded so a rebind has a much
27
+ * smaller window to work in.
28
+ */
29
+ function isPrivateAddressLiteral(host) {
30
+ const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
31
+ const version = isIP(bare);
32
+ if (version === 4) {
33
+ const parts = bare.split(".").map(Number);
34
+ const [a, b] = parts;
35
+ if (a === 10 || a === 127 || a === 0)
36
+ return true;
37
+ if (a === 172 && b >= 16 && b <= 31)
38
+ return true;
39
+ if (a === 192 && b === 168)
40
+ return true;
41
+ // 169.254.0.0/16 — link-local, and the cloud metadata endpoint.
42
+ if (a === 169 && b === 254)
43
+ return true;
44
+ return false;
45
+ }
46
+ if (version === 6) {
47
+ const lower = bare.toLowerCase();
48
+ if (lower === "::1" || lower === "::")
49
+ return true;
50
+ // fc00::/7 unique-local, fe80::/10 link-local.
51
+ if (lower.startsWith("fc") || lower.startsWith("fd"))
52
+ return true;
53
+ if (lower.startsWith("fe8") || lower.startsWith("fe9"))
54
+ return true;
55
+ if (lower.startsWith("fea") || lower.startsWith("feb"))
56
+ return true;
57
+ return false;
58
+ }
59
+ // Not a literal. Hostname forms that always mean "this machine".
60
+ const lowerHost = bare.toLowerCase();
61
+ return lowerHost === "localhost" || lowerHost.endsWith(".localhost");
62
+ }
63
+ /** Validate one URL in the chain. Applied to the origin URL and every hop. */
64
+ export function checkDeclarationUrl(raw, policy) {
65
+ let url;
66
+ try {
67
+ url = new URL(raw);
68
+ }
69
+ catch {
70
+ return {
71
+ code: "fetch_failed",
72
+ message: `malformed declaration URL: ${raw}`,
73
+ };
74
+ }
75
+ if (url.protocol !== "https:" && !policy.allowInsecureForTesting) {
76
+ return {
77
+ code: "insecure_scheme",
78
+ message: `declaration URL must use https, got ${url.protocol}`,
79
+ };
80
+ }
81
+ if (isPrivateAddressLiteral(url.hostname) &&
82
+ !policy.allowInsecureForTesting) {
83
+ return {
84
+ code: "private_address",
85
+ message: `declaration URL resolves to a private or loopback address: ${url.hostname}`,
86
+ };
87
+ }
88
+ if (!policy.allowAnyHttpsHost) {
89
+ const trusted = policy.trustedHosts ?? [];
90
+ if (!trusted.includes(url.hostname)) {
91
+ return {
92
+ code: "untrusted_source",
93
+ message: `declaration host '${url.hostname}' is not in this deployment's declaration trust policy`,
94
+ };
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ /**
100
+ * Project a normative §5 `SourceDeclaration` onto the internal snapshot shape.
101
+ *
102
+ * Returns the document unchanged when it is already in the internal shape, so
103
+ * existing private fixtures keep working — this is a compatibility adapter,
104
+ * not a redefinition of either format.
105
+ *
106
+ * The projection is deliberately narrow:
107
+ * - `source.id` / `source.kind` → `source_id` / `source_kind`
108
+ * - `declaration_version` → `version`
109
+ * - per stream, `schema.properties` keys → `fields`, and `schema.required`
110
+ * → `required_fields`
111
+ *
112
+ * Deriving fields from the JSON Schema is the substantive part. The internal
113
+ * model wants a flat allowlist because §6 field selection and §8 projection
114
+ * operate on top-level names; the normative model carries a real schema
115
+ * because a declaration must describe record shape, not just name it. Reading
116
+ * `properties` keys is the honest projection of one onto the other, and
117
+ * `required` is exactly the per-stream consent floor §6 already calls for.
118
+ *
119
+ * Nothing here validates the normative document beyond what the projection
120
+ * needs — the checks that follow do that, and they run on the projected form
121
+ * so one code path enforces both.
122
+ */
123
+ /**
124
+ * The source id a declaration document claims, in either shape.
125
+ *
126
+ * A caller that needs the id *before* parsing — a deployment loader keying
127
+ * retention by source, say — would otherwise have to reach into the document
128
+ * itself and pick a field. That is how the boot path came to accept only the
129
+ * internal flat `source_id` and silently skip every normative document, which
130
+ * left the AS unmounted with nothing but a warning to say why.
131
+ *
132
+ * Reading the shape is this module's job, so the question is answered here
133
+ * once. Returns null when neither shape carries a usable id; `parseDeclaration`
134
+ * remains the authority on whether the document is actually valid.
135
+ */
136
+ export function declaredSourceId(body) {
137
+ let doc;
138
+ try {
139
+ doc = JSON.parse(body);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ if (typeof doc !== "object" || doc === null)
145
+ return null;
146
+ // Normative §5: `source.id`. Internal/flat: `source_id`.
147
+ const nested = doc.source?.id;
148
+ const flat = doc.source_id;
149
+ const id = typeof nested === "string" && nested.length > 0 ? nested : flat;
150
+ return typeof id === "string" && id.length > 0 ? id : null;
151
+ }
152
+ function normalizeNormativeDeclaration(doc) {
153
+ const source = doc.source;
154
+ if (typeof source !== "object" || source === null)
155
+ return doc;
156
+ const { id, kind } = source;
157
+ const declarationVersion = doc
158
+ .declaration_version;
159
+ const streams = Array.isArray(doc.streams)
160
+ ? doc.streams.map((raw) => {
161
+ const stream = raw;
162
+ // Already internal-shaped: leave it alone rather than guessing.
163
+ if (Array.isArray(stream.fields))
164
+ return stream;
165
+ const schema = stream.schema;
166
+ if (!schema || typeof schema !== "object")
167
+ return stream;
168
+ return {
169
+ ...stream,
170
+ fields: Object.keys(schema.properties ?? {}),
171
+ required_fields: Array.isArray(schema.required)
172
+ ? schema.required
173
+ : [],
174
+ };
175
+ })
176
+ : doc.streams;
177
+ return {
178
+ ...doc,
179
+ ...(typeof id === "string" && { source_id: id }),
180
+ ...(typeof kind === "string" && { source_kind: kind }),
181
+ ...(typeof declarationVersion === "string" && {
182
+ version: declarationVersion,
183
+ }),
184
+ streams,
185
+ };
186
+ }
187
+ /** The dialect §5 fixes for every embedded stream schema. */
188
+ const SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
189
+ /**
190
+ * Meta-validate one embedded stream schema (§5).
191
+ *
192
+ * "If `$schema` is present, it MUST equal
193
+ * `https://json-schema.org/draft/2020-12/schema`. [...] The AS MUST
194
+ * meta-validate each embedded stream schema before accepting the declaration.
195
+ * Embedded `$ref` and `$dynamicRef` values MUST be local fragment references.
196
+ * A declaration MUST NOT make consent interpretation depend on a mutable
197
+ * remote schema."
198
+ *
199
+ * That last sentence is what the reference check is for, and it is the reason
200
+ * a remote `$ref` is a refusal rather than a warning: a schema the AS never
201
+ * fetched and never froze can be changed by a third party after the owner has
202
+ * consented, which rewrites what the consent covers without the declaration
203
+ * changing at all.
204
+ *
205
+ * The walk is recursive because a `$ref` under `properties`, `items`, `$defs`
206
+ * or a combinator is exactly as remote as one at the root; a top-level-only
207
+ * check would be a formality rather than a fence.
208
+ *
209
+ * Returns a human-readable reason, or null when the schema is acceptable.
210
+ */
211
+ function metaValidateSchema(schema) {
212
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
213
+ return "schema must be a JSON Schema object";
214
+ }
215
+ const dialect = schema.$schema;
216
+ if (dialect !== undefined && dialect !== SCHEMA_DIALECT) {
217
+ return `schema declares $schema '${String(dialect)}', but §5 fixes the dialect at ${SCHEMA_DIALECT}`;
218
+ }
219
+ return findRemoteReference(schema);
220
+ }
221
+ /**
222
+ * The first non-local `$ref`/`$dynamicRef` anywhere in `node`, as a reason.
223
+ *
224
+ * "Local fragment reference" means a value beginning with `#` — a pointer into
225
+ * this same document. Anything else names a document the AS does not hold.
226
+ */
227
+ function findRemoteReference(node) {
228
+ if (Array.isArray(node)) {
229
+ for (const entry of node) {
230
+ const problem = findRemoteReference(entry);
231
+ if (problem)
232
+ return problem;
233
+ }
234
+ return null;
235
+ }
236
+ if (typeof node !== "object" || node === null)
237
+ return null;
238
+ for (const keyword of ["$ref", "$dynamicRef"]) {
239
+ const value = node[keyword];
240
+ if (typeof value === "string" && !value.startsWith("#")) {
241
+ return `schema resolves ${keyword} '${value}', which is not a local fragment reference`;
242
+ }
243
+ }
244
+ for (const value of Object.values(node)) {
245
+ const problem = findRemoteReference(value);
246
+ if (problem)
247
+ return problem;
248
+ }
249
+ return null;
250
+ }
251
+ /**
252
+ * The registered IANA top-level types. A closed set, unlike the subtypes.
253
+ */
254
+ const TOP_LEVEL_MEDIA_TYPES = new Set([
255
+ "application",
256
+ "audio",
257
+ "example",
258
+ "font",
259
+ "haptics",
260
+ "image",
261
+ "message",
262
+ "model",
263
+ "multipart",
264
+ "text",
265
+ "video",
266
+ ]);
267
+ /** RFC 6838 restricted-name grammar for a subtype (or a tree-prefixed one). */
268
+ const SUBTYPE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}$/;
269
+ /**
270
+ * Is `value` a valid IANA media type (§4.8)?
271
+ *
272
+ * Checked against the GRAMMAR, not a registry snapshot. IANA adds subtypes
273
+ * continuously and without a spec revision, so refusing a well-formed subtype
274
+ * this server has not heard of would reject conforming declarations and turn
275
+ * the check into a registry-freshness test rather than a validity one. The
276
+ * top-level types are a closed set, so that half IS an exact check.
277
+ */
278
+ function isValidMediaType(value) {
279
+ // Parameters (`; charset=utf-8`) are part of a well-formed value; the
280
+ // type/subtype pair is what the clause constrains.
281
+ const [essence] = value.split(";");
282
+ const parts = essence.trim().split("/");
283
+ if (parts.length !== 2)
284
+ return false;
285
+ const [type, subtype] = parts;
286
+ return TOP_LEVEL_MEDIA_TYPES.has(type.toLowerCase()) && SUBTYPE.test(subtype);
287
+ }
288
+ /**
289
+ * The first invalid declared `blob_ref` media type in a stream, as a reason.
290
+ *
291
+ * §4 shows `mime_type` inside a RECORD's `blob_ref`. A declaration describes
292
+ * record shape, so it can pin one in two places and both are checked rather
293
+ * than one being privileged:
294
+ *
295
+ * - a `blob_fields` member listing blob-bearing fields and their types;
296
+ * - a `const`/`enum` on a `blob_ref.mime_type` property inside the embedded
297
+ * schema, which is how a declaration constrains any record field.
298
+ *
299
+ * Neither is mandatory: a stream declaring no blob field is untouched, which
300
+ * is the clause's own applicability condition.
301
+ */
302
+ function findInvalidBlobMediaType(stream) {
303
+ const blobFields = stream.blob_fields;
304
+ if (Array.isArray(blobFields)) {
305
+ for (const raw of blobFields) {
306
+ if (typeof raw !== "object" || raw === null)
307
+ continue;
308
+ const { name, mime_type: mimeType } = raw;
309
+ if (typeof mimeType !== "string" || !isValidMediaType(mimeType)) {
310
+ return `declares blob_ref field '${String(name)}' with mime_type '${String(mimeType)}', which is not a valid IANA media type`;
311
+ }
312
+ }
313
+ }
314
+ return findInvalidSchemaMediaType(stream.schema);
315
+ }
316
+ /**
317
+ * A `const`/`enum` media type pinned on a `mime_type` property anywhere in the
318
+ * embedded schema, checked wherever it appears.
319
+ */
320
+ function findInvalidSchemaMediaType(node) {
321
+ if (Array.isArray(node)) {
322
+ for (const entry of node) {
323
+ const problem = findInvalidSchemaMediaType(entry);
324
+ if (problem)
325
+ return problem;
326
+ }
327
+ return null;
328
+ }
329
+ if (typeof node !== "object" || node === null)
330
+ return null;
331
+ const properties = node.properties;
332
+ if (typeof properties === "object" && properties !== null) {
333
+ const pinned = properties.mime_type;
334
+ if (typeof pinned === "object" && pinned !== null) {
335
+ const candidates = [
336
+ ...(typeof pinned.const === "string"
337
+ ? [pinned.const]
338
+ : []),
339
+ ...(Array.isArray(pinned.enum)
340
+ ? pinned.enum.filter((v) => typeof v === "string")
341
+ : []),
342
+ ];
343
+ const bad = candidates.find((c) => !isValidMediaType(c));
344
+ if (bad !== undefined) {
345
+ return `pins mime_type '${bad}', which is not a valid IANA media type`;
346
+ }
347
+ }
348
+ }
349
+ for (const value of Object.values(node)) {
350
+ const problem = findInvalidSchemaMediaType(value);
351
+ if (problem)
352
+ return problem;
353
+ }
354
+ return null;
355
+ }
356
+ export function computeDeclarationDigest(body) {
357
+ return createHash("sha256").update(body, "utf8").digest("hex");
358
+ }
359
+ /**
360
+ * Read a stream's §5 `display` object, keeping only string members.
361
+ *
362
+ * Returns `{ display }` or undefined, so the caller can spread it and let an
363
+ * absent or empty object stay absent. `{ label: "" }` is treated as no label:
364
+ * §5's fallback chain is label → description → stream name, and an empty
365
+ * string would win that race and render nothing where a name belongs.
366
+ */
367
+ function readDeclaredDisplay(value) {
368
+ if (typeof value !== "object" || value === null)
369
+ return undefined;
370
+ const { label, detail } = value;
371
+ const display = {
372
+ ...(typeof label === "string" && label.length > 0 && { label }),
373
+ ...(typeof detail === "string" && detail.length > 0 && { detail }),
374
+ };
375
+ return Object.keys(display).length > 0 ? { display } : undefined;
376
+ }
377
+ /**
378
+ * Parse and structurally validate a declaration document.
379
+ *
380
+ * Validates the embedded stream schemas enough that resolution can rely on
381
+ * them: every stream needs a name, a field list, and a primary key, and its
382
+ * `required_fields` must be a subset of its declared fields — otherwise the
383
+ * per-stream consent floor would name fields the schema does not have.
384
+ */
385
+ export function parseDeclaration(body, expectedSourceId, expectedDigest) {
386
+ const digest = computeDeclarationDigest(body);
387
+ if (expectedDigest !== undefined && digest !== expectedDigest) {
388
+ return {
389
+ ok: false,
390
+ failure: {
391
+ code: "digest_mismatch",
392
+ message: "declaration digest does not match the expected value",
393
+ },
394
+ };
395
+ }
396
+ let doc;
397
+ try {
398
+ doc = JSON.parse(body);
399
+ }
400
+ catch {
401
+ return {
402
+ ok: false,
403
+ failure: {
404
+ code: "invalid_document",
405
+ message: "declaration is not valid JSON",
406
+ },
407
+ };
408
+ }
409
+ // Accept the NORMATIVE §5 SourceDeclaration and project it onto this
410
+ // module's internal snapshot shape.
411
+ //
412
+ // PS historically parsed a private shape (`source_id`, `source_kind`,
413
+ // `version`, flat `fields[]`/`required_fields[]`). That shape is not the
414
+ // protocol: the published schema
415
+ // (`https://pdpp.dev/schemas/source-declaration/0.1.0`, `reference-contract`)
416
+ // requires `protocol_version`, nested `source {kind,id}`,
417
+ // `declaration_version`, `publisher`, `display`, and a real JSON-Schema
418
+ // `schema` per stream — and sets `additionalProperties: false`, so the
419
+ // private shape is affirmatively INVALID, not merely different.
420
+ //
421
+ // Normalizing here rather than loosening the checks below keeps the
422
+ // protocol boundary honest: a normative document is validated as normative,
423
+ // and the internal snapshot stays an internal detail. The raw bytes and
424
+ // their digest are untouched — `digest` above is computed over `body`
425
+ // exactly as retrieved, which is the property that proves a producer read
426
+ // the same bytes the owner consented against.
427
+ doc = normalizeNormativeDeclaration(doc);
428
+ if (typeof doc.source_id !== "string" || doc.source_id.length === 0) {
429
+ return {
430
+ ok: false,
431
+ failure: {
432
+ code: "invalid_document",
433
+ message: "declaration is missing source_id",
434
+ },
435
+ };
436
+ }
437
+ // The declaration must claim the source we asked about. Without this check a
438
+ // trusted host could serve an authority for a source it does not own.
439
+ if (doc.source_id !== expectedSourceId) {
440
+ return {
441
+ ok: false,
442
+ failure: {
443
+ code: "source_id_mismatch",
444
+ message: `declaration declares source_id '${doc.source_id}' but was retrieved for '${expectedSourceId}'`,
445
+ },
446
+ };
447
+ }
448
+ if (doc.source_kind !== "connector" &&
449
+ doc.source_kind !== "provider_native") {
450
+ return {
451
+ ok: false,
452
+ failure: {
453
+ code: "invalid_document",
454
+ message: "declaration source_kind must be connector or provider_native",
455
+ },
456
+ };
457
+ }
458
+ if (typeof doc.version !== "string" || doc.version.length === 0) {
459
+ return {
460
+ ok: false,
461
+ failure: {
462
+ code: "invalid_document",
463
+ message: "declaration is missing version",
464
+ },
465
+ };
466
+ }
467
+ if (!Array.isArray(doc.streams) || doc.streams.length === 0) {
468
+ return {
469
+ ok: false,
470
+ failure: {
471
+ code: "invalid_document",
472
+ message: "declaration declares no streams",
473
+ },
474
+ };
475
+ }
476
+ const streams = [];
477
+ const seenStreams = new Set();
478
+ for (const raw of doc.streams) {
479
+ const s = raw;
480
+ if (typeof s.name !== "string" || s.name.length === 0) {
481
+ return {
482
+ ok: false,
483
+ failure: {
484
+ code: "invalid_document",
485
+ message: "a declared stream is missing name",
486
+ },
487
+ };
488
+ }
489
+ if (seenStreams.has(s.name)) {
490
+ return {
491
+ ok: false,
492
+ failure: {
493
+ code: "invalid_document",
494
+ message: `stream '${s.name}' is declared more than once`,
495
+ },
496
+ };
497
+ }
498
+ seenStreams.add(s.name);
499
+ if (!Array.isArray(s.fields) || s.fields.length === 0) {
500
+ return {
501
+ ok: false,
502
+ failure: {
503
+ code: "invalid_document",
504
+ message: `stream '${s.name}' declares no fields`,
505
+ },
506
+ };
507
+ }
508
+ const fields = s.fields;
509
+ const requiredFields = Array.isArray(s.required_fields)
510
+ ? s.required_fields
511
+ : [];
512
+ const missing = requiredFields.filter((f) => !fields.includes(f));
513
+ if (missing.length > 0) {
514
+ return {
515
+ ok: false,
516
+ failure: {
517
+ code: "invalid_document",
518
+ message: `stream '${s.name}' marks fields required that its schema does not declare: ${missing.join(", ")}`,
519
+ },
520
+ };
521
+ }
522
+ if (!Array.isArray(s.primary_key) || s.primary_key.length === 0) {
523
+ return {
524
+ ok: false,
525
+ failure: {
526
+ code: "invalid_document",
527
+ message: `stream '${s.name}' declares no primary_key`,
528
+ },
529
+ };
530
+ }
531
+ // §5 `streams[].schema`: "`primary_key` and `cursor_field` MUST reference
532
+ // fields declared here."
533
+ //
534
+ // Checked against `fields` rather than against `schema.properties`
535
+ // directly, because `fields` IS that projection for a normative document
536
+ // and is what the internal flat shape carries — so one check covers both
537
+ // shapes. Without it a declaration can promise record ordering over a
538
+ // field that does not exist; nothing catches the contradiction until read
539
+ // time, long after the owner consented to the document.
540
+ const undeclaredKey = s.primary_key.filter((f) => !fields.includes(f));
541
+ if (undeclaredKey.length > 0) {
542
+ return {
543
+ ok: false,
544
+ failure: {
545
+ code: "invalid_document",
546
+ message: `stream '${s.name}' names primary_key fields its schema does not declare: ${undeclaredKey.join(", ")}`,
547
+ },
548
+ };
549
+ }
550
+ if (typeof s.cursor_field === "string" &&
551
+ !fields.includes(s.cursor_field)) {
552
+ return {
553
+ ok: false,
554
+ failure: {
555
+ code: "invalid_document",
556
+ message: `stream '${s.name}' names cursor_field '${s.cursor_field}', which its schema does not declare`,
557
+ },
558
+ };
559
+ }
560
+ // §5 `streams[].consent_time_field`: "MUST reference a field declared in
561
+ // the schema."
562
+ //
563
+ // Its own check, not folded into the one above, because this field is not
564
+ // read ordering — §6 makes its PRESENCE the authoritative signal that a
565
+ // stream is time-range-capable, and it is the field a `time_range` grant
566
+ // is evaluated against. An undeclared one therefore advertises a consent
567
+ // boundary with no column behind it: an owner who approves "the last six
568
+ // months" has that bound applied to nothing.
569
+ //
570
+ // Absence stays legal — §5: "Streams that cannot define a stable
571
+ // `consent_time_field` simply omit it."
572
+ if (typeof s.consent_time_field === "string" &&
573
+ !fields.includes(s.consent_time_field)) {
574
+ return {
575
+ ok: false,
576
+ failure: {
577
+ code: "invalid_document",
578
+ message: `stream '${s.name}' names consent_time_field '${s.consent_time_field}', which its schema does not declare`,
579
+ },
580
+ };
581
+ }
582
+ // §5: meta-validate the embedded schema before accepting the declaration.
583
+ // Only when one is present — the internal flat shape carries a field list
584
+ // and no schema, and §5's obligation is about the schema a normative
585
+ // document embeds.
586
+ if (s.schema !== undefined) {
587
+ const schemaProblem = metaValidateSchema(s.schema);
588
+ if (schemaProblem) {
589
+ return {
590
+ ok: false,
591
+ failure: {
592
+ code: "invalid_document",
593
+ message: `stream '${s.name}': ${schemaProblem}`,
594
+ },
595
+ };
596
+ }
597
+ }
598
+ // §4.8: "`mime_type` MUST be a valid IANA media type." The value is what
599
+ // every consumer uses to decide how to interpret fetched bytes, so one no
600
+ // registry defines means each client guesses differently — and the guess
601
+ // is frozen into the document the owner's consent is written against.
602
+ const mediaTypeProblem = findInvalidBlobMediaType(s);
603
+ if (mediaTypeProblem) {
604
+ return {
605
+ ok: false,
606
+ failure: {
607
+ code: "invalid_document",
608
+ message: `stream '${s.name}' ${mediaTypeProblem}`,
609
+ },
610
+ };
611
+ }
612
+ // §5 stream semantics. An unrecognized value is a rejection rather than a
613
+ // silent fallback: coercing an unknown semantics to `mutable_state` would
614
+ // upsert records a declaration may have meant to be immutable.
615
+ if (s.semantics !== undefined &&
616
+ s.semantics !== "mutable_state" &&
617
+ s.semantics !== "append_only") {
618
+ return {
619
+ ok: false,
620
+ failure: {
621
+ code: "invalid_document",
622
+ message: `stream '${s.name}' declares unsupported semantics '${String(s.semantics)}'`,
623
+ },
624
+ };
625
+ }
626
+ streams.push({
627
+ name: s.name,
628
+ fields,
629
+ required_fields: requiredFields,
630
+ // §5 consent-surface copy. Carried verbatim and kept as two distinct
631
+ // fields: `display` is the consent-surface metadata the AS renders,
632
+ // `description` is explicitly not. Dropping either left the consent
633
+ // surface with nothing but the raw stream name to show the owner.
634
+ //
635
+ // Absence stays absence — an empty `display: {}` would make a renderer
636
+ // print a blank label where it must fall back to the stream name.
637
+ ...(typeof s.description === "string" &&
638
+ s.description.length > 0 && { description: s.description }),
639
+ ...(readDeclaredDisplay(s.display) ?? {}),
640
+ ...(typeof s.semantics === "string" && {
641
+ semantics: s.semantics,
642
+ }),
643
+ ...(typeof s.consent_time_field === "string" && {
644
+ consent_time_field: s.consent_time_field,
645
+ }),
646
+ primary_key: s.primary_key,
647
+ // Carried verbatim from the normative declaration so §8 metadata can
648
+ // report record shape and selection capability without a second lookup.
649
+ ...(typeof s.schema === "object" && s.schema !== null
650
+ ? { schema: s.schema }
651
+ : {}),
652
+ ...(typeof s.selection === "object" && s.selection !== null
653
+ ? { selection: s.selection }
654
+ : {}),
655
+ });
656
+ }
657
+ // §6: a preset naming the same stream twice makes the declaration invalid.
658
+ // That is a declaration-acceptance failure, not deferred to issuance.
659
+ const presets = Array.isArray(doc.selection_presets)
660
+ ? doc.selection_presets
661
+ : undefined;
662
+ if (presets) {
663
+ for (const preset of presets) {
664
+ const seen = new Set();
665
+ for (const stream of preset.streams ?? []) {
666
+ if (seen.has(stream.name)) {
667
+ return {
668
+ ok: false,
669
+ failure: {
670
+ code: "invalid_document",
671
+ message: `selection preset '${preset.name}' names stream '${stream.name}' more than once`,
672
+ },
673
+ };
674
+ }
675
+ seen.add(stream.name);
676
+ }
677
+ }
678
+ }
679
+ const sourceDisplayName = doc.display
680
+ ?.name;
681
+ return {
682
+ ok: true,
683
+ snapshot: {
684
+ source_id: doc.source_id,
685
+ source_kind: doc.source_kind,
686
+ version: doc.version,
687
+ digest,
688
+ streams,
689
+ // §5 top-level `display.name`. Only carried when it is a non-empty
690
+ // string: a blank name would outrank the source id in the renderer's
691
+ // fallback chain and put an empty heading on the consent surface.
692
+ ...(typeof sourceDisplayName === "string" &&
693
+ sourceDisplayName.length > 0 && {
694
+ display: { name: sourceDisplayName },
695
+ }),
696
+ ...(Array.isArray(doc.views) && {
697
+ views: doc.views,
698
+ }),
699
+ ...(presets && {
700
+ selection_presets: presets,
701
+ }),
702
+ },
703
+ };
704
+ }
705
+ /**
706
+ * Read a response body, giving up as soon as it exceeds `limit` bytes.
707
+ *
708
+ * Returns null when the cap is passed. The stream is cancelled at that point,
709
+ * so a hostile or misconfigured host cannot make us hold more than the cap in
710
+ * memory however it frames the response. Falls back to a buffered read only
711
+ * when the runtime gives no readable stream, and still checks the size.
712
+ */
713
+ async function readCapped(response, limit) {
714
+ const stream = response.body;
715
+ if (!stream) {
716
+ const buffered = await response.text();
717
+ return Buffer.byteLength(buffered, "utf8") > limit ? null : buffered;
718
+ }
719
+ const reader = stream.getReader();
720
+ const chunks = [];
721
+ let total = 0;
722
+ try {
723
+ for (;;) {
724
+ const { done, value } = await reader.read();
725
+ if (done)
726
+ break;
727
+ if (!value)
728
+ continue;
729
+ total += value.byteLength;
730
+ if (total > limit) {
731
+ await reader.cancel();
732
+ return null;
733
+ }
734
+ chunks.push(value);
735
+ }
736
+ }
737
+ finally {
738
+ reader.releaseLock();
739
+ }
740
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
741
+ }
742
+ /**
743
+ * Retrieve one declaration under the bounded rules, then parse and digest it.
744
+ *
745
+ * Redirects are followed manually so each hop can be re-validated. This is the
746
+ * difference between a fence and a formality: `fetch`'s automatic redirect
747
+ * following would validate only the URL we started with.
748
+ */
749
+ export async function retrieveDeclaration(url, expectedSourceId, policy, options = {}) {
750
+ const fetcher = options.fetcher ?? ((u, init) => fetch(u, init));
751
+ let current = url;
752
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
753
+ const problem = checkDeclarationUrl(current, policy);
754
+ if (problem)
755
+ return { ok: false, failure: problem };
756
+ const controller = new AbortController();
757
+ const timer = setTimeout(() => controller.abort(), DECLARATION_FETCH_TIMEOUT_MS);
758
+ let response;
759
+ try {
760
+ response = await fetcher(current, {
761
+ redirect: "manual",
762
+ signal: controller.signal,
763
+ });
764
+ }
765
+ catch (err) {
766
+ return {
767
+ ok: false,
768
+ failure: {
769
+ code: "fetch_failed",
770
+ message: `declaration retrieval failed: ${err.message}`,
771
+ },
772
+ };
773
+ }
774
+ finally {
775
+ clearTimeout(timer);
776
+ }
777
+ if (response.status >= 300 && response.status < 400) {
778
+ const location = response.headers.get("location");
779
+ if (!location) {
780
+ return {
781
+ ok: false,
782
+ failure: {
783
+ code: "fetch_failed",
784
+ message: "redirect response carried no Location header",
785
+ },
786
+ };
787
+ }
788
+ // Resolve relative redirects against the current URL, then re-check.
789
+ current = new URL(location, current).toString();
790
+ continue;
791
+ }
792
+ if (!response.ok) {
793
+ return {
794
+ ok: false,
795
+ failure: {
796
+ code: "fetch_failed",
797
+ message: `declaration retrieval returned HTTP ${response.status}`,
798
+ },
799
+ };
800
+ }
801
+ const declaredLength = response.headers.get("content-length");
802
+ if (declaredLength && Number(declaredLength) > MAX_DECLARATION_BYTES) {
803
+ return {
804
+ ok: false,
805
+ failure: {
806
+ code: "too_large",
807
+ message: `declaration exceeds ${MAX_DECLARATION_BYTES} bytes`,
808
+ },
809
+ };
810
+ }
811
+ // Read incrementally and abort the moment the cap is passed. `text()`
812
+ // would buffer the whole response first, so a host that omits or lies
813
+ // about Content-Length could make us allocate an unbounded body and only
814
+ // then be told it was too large — the cap has to bound the allocation, not
815
+ // just the outcome.
816
+ const body = await readCapped(response, MAX_DECLARATION_BYTES);
817
+ if (body === null) {
818
+ return {
819
+ ok: false,
820
+ failure: {
821
+ code: "too_large",
822
+ message: `declaration exceeds ${MAX_DECLARATION_BYTES} bytes`,
823
+ },
824
+ };
825
+ }
826
+ return parseDeclaration(body, expectedSourceId, options.expectedDigest);
827
+ }
828
+ return {
829
+ ok: false,
830
+ failure: {
831
+ code: "too_many_redirects",
832
+ message: `declaration retrieval exceeded ${MAX_REDIRECTS} redirects`,
833
+ },
834
+ };
835
+ }
836
+ //# sourceMappingURL=declaration.js.map