reploid 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/apps/poolday/config/chat-files.json +27 -0
  2. package/apps/poolday/config/partition-generation.js +21 -0
  3. package/apps/poolday/config/partition-policy.json +74 -0
  4. package/apps/poolday/config/partition-releases.json +3 -0
  5. package/apps/poolday/config/work-profile.json +122 -0
  6. package/apps/poolday/host/chat-view.js +47 -0
  7. package/apps/poolday/host/earning/client.js +199 -0
  8. package/apps/poolday/host/earning/contribution.js +168 -0
  9. package/apps/poolday/host/partition-release.js +33 -0
  10. package/apps/poolday/host/work-partition-artifact-source.js +37 -0
  11. package/apps/poolday/host/work-partition-capsule.js +39 -0
  12. package/apps/poolday/host/work-partition-plan.js +167 -0
  13. package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
  14. package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
  15. package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
  16. package/apps/poolday/pool/pack-operation.js +161 -0
  17. package/apps/poolday/pool/pack-release-policy.js +44 -0
  18. package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
  19. package/apps/poolday/providers/work-adapter.js +67 -0
  20. package/apps/poolday/providers/work-device.js +33 -0
  21. package/apps/poolday/providers/work-generation-completion.js +55 -0
  22. package/apps/poolday/providers/work-provider.js +209 -0
  23. package/apps/poolday/providers/work-resident-provider.js +113 -0
  24. package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
  25. package/docs/fleet-service.md +214 -0
  26. package/docs/native-participant.md +542 -0
  27. package/native-delivery.json +1174 -0
  28. package/package.json +29 -4
  29. package/packages/reploid/src/artifacts/binary.js +25 -0
  30. package/packages/reploid/src/artifacts/canonical-json.js +23 -0
  31. package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
  32. package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
  33. package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
  34. package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
  35. package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
  36. package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
  37. package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
  38. package/packages/reploid/src/artifacts/identity.js +91 -0
  39. package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
  40. package/packages/reploid/src/artifacts/receipt.js +131 -0
  41. package/packages/reploid/src/artifacts/signatures.js +94 -0
  42. package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
  43. package/packages/reploid/src/cancellation.js +44 -0
  44. package/packages/reploid/src/config/defaults.json +110 -0
  45. package/packages/reploid/src/config/index.js +209 -0
  46. package/packages/reploid/src/config/request-overrides.json +9 -0
  47. package/packages/reploid/src/config/transport-defaults.json +7 -0
  48. package/packages/reploid/src/hashing.js +30 -0
  49. package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
  50. package/packages/reploid/src/measurement/index.js +5 -0
  51. package/packages/reploid/src/measurement/readiness-records.js +310 -0
  52. package/packages/reploid/src/mesh/computer-identity.js +50 -0
  53. package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
  54. package/packages/reploid/src/mesh/contribution.js +141 -0
  55. package/packages/reploid/src/mesh/generation-recovery.js +58 -0
  56. package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
  57. package/packages/reploid/src/mesh/gpu-signature.js +136 -0
  58. package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
  59. package/packages/reploid/src/mesh/index.js +66 -0
  60. package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
  61. package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
  62. package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
  63. package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
  64. package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
  65. package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
  66. package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
  67. package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
  68. package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
  69. package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
  70. package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
  71. package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
  72. package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
  73. package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
  74. package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
  75. package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
  76. package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
  77. package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
  78. package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
  79. package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
  80. package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
  81. package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
  82. package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
  83. package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
  84. package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
  85. package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
  86. package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
  87. package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
  88. package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
  89. package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
  90. package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
  91. package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
  92. package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
  93. package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
  94. package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
  95. package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
  96. package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
  97. package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
  98. package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
  99. package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
  100. package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
  101. package/packages/reploid/src/mesh/peer-generation.js +896 -0
  102. package/packages/reploid/src/mesh/peer-identity.js +87 -0
  103. package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
  104. package/packages/reploid/src/mesh/preparation-progress.js +46 -0
  105. package/packages/reploid/src/mesh/provider-hardware.js +11 -0
  106. package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
  107. package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
  108. package/packages/reploid/src/mesh/web-access-network.js +112 -0
  109. package/packages/reploid/src/mesh/web-access-policy.json +17 -0
  110. package/packages/reploid/src/retrieval/citations.js +156 -0
  111. package/packages/reploid/src/retrieval/claim-support.js +113 -0
  112. package/packages/reploid/src/retrieval/costs.js +101 -0
  113. package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
  114. package/packages/reploid/src/retrieval/index.js +20 -0
  115. package/packages/reploid/src/retrieval/lexical-support.js +27 -0
  116. package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
  117. package/packages/reploid/src/retrieval/page-fetch.json +10 -0
  118. package/packages/reploid/src/retrieval/page-text.js +182 -0
  119. package/packages/reploid/src/retrieval/passage.js +37 -0
  120. package/packages/reploid/src/retrieval/policy.json +14 -0
  121. package/packages/reploid/src/retrieval/retrieval.js +23 -0
  122. package/packages/reploid/src/retrieval/review-queue.js +76 -0
  123. package/packages/reploid/src/retrieval/sources.js +263 -0
  124. package/packages/reploid/src/retrieval/support-review.js +155 -0
  125. package/packages/reploid/src/retrieval/text.js +21 -0
  126. package/packages/reploid/src/rules/contribution.rules.json +8 -0
  127. package/packages/reploid/src/rules/routing.rules.json +24 -0
  128. package/packages/reploid/src/transport/assignment.js +571 -0
  129. package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
  130. package/packages/reploid/src/transport/index.js +11 -0
  131. package/packages/reploid/src/transport/introduction-admission.js +24 -0
  132. package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
  133. package/packages/reploid/src/transport/introduction-policy.json +12 -0
  134. package/packages/reploid/src/transport/introduction-records.js +90 -0
  135. package/packages/reploid/src/transport/introductions.js +209 -0
  136. package/packages/reploid/src/transport/partition-data-channel.js +421 -0
  137. package/packages/reploid/src/transport/partition-errors.json +16 -0
  138. package/packages/reploid/src/transport/retry-policy.js +36 -0
  139. package/packages/reploid/src/transport/room.js +502 -0
  140. package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
  141. package/packages/reploid/src/transport/signaling.js +571 -0
  142. package/packages/reploid/src/transport/swarm.js +1247 -0
  143. package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
  144. package/server/browser-roots.js +42 -0
  145. package/server/browser-web-worker.js +94 -0
  146. package/server/chat-completions.js +45 -0
  147. package/server/fleet/browser-bridge.js +145 -0
  148. package/server/fleet/contract.js +42 -0
  149. package/server/fleet/credit-evidence.js +103 -0
  150. package/server/fleet/credit-recovery.js +175 -0
  151. package/server/fleet/credits.js +178 -0
  152. package/server/fleet/execution-admission.js +32 -0
  153. package/server/fleet/native-host.js +817 -0
  154. package/server/fleet/placement.js +132 -0
  155. package/server/fleet/requester.js +346 -0
  156. package/server/fleet/service.js +311 -0
  157. package/server/fleet/terminal-policy.js +83 -0
  158. package/server/fleet/text-api.js +94 -0
  159. package/server/hashing.js +35 -0
  160. package/server/http-request-owner.js +49 -0
  161. package/server/messages.js +57 -0
  162. package/server/native-canonical-epochs.js +90 -0
  163. package/server/native-change-log.js +40 -0
  164. package/server/native-earning.js +203 -0
  165. package/server/native-generation-journal.js +46 -0
  166. package/server/native-model-config.js +57 -0
  167. package/server/native-model-files.js +382 -0
  168. package/server/native-model-origin.js +54 -0
  169. package/server/native-model-origins.json +12 -0
  170. package/server/native-node.js +85 -0
  171. package/server/native-owner/controls.js +162 -0
  172. package/server/native-owner/monitor.js +251 -0
  173. package/server/native-owner/policy.js +273 -0
  174. package/server/native-owner/rate.js +92 -0
  175. package/server/native-participant.js +866 -0
  176. package/server/native-readiness-log.js +30 -0
  177. package/server/native-release-storage.js +64 -0
  178. package/server/native-shader-cache.js +369 -0
  179. package/server/native-supply-budget.js +66 -0
  180. package/server/native-whole-earning.js +67 -0
  181. package/server/native-whole-model-fill.js +182 -0
  182. package/server/native-whole-model.js +130 -0
  183. package/server/public-web-worker.js +66 -0
  184. package/server/responses.js +92 -0
  185. package/server/search/errors.js +47 -0
  186. package/server/search/page-fetch.js +185 -0
  187. package/server/text-api.js +83 -0
  188. package/server/text-recovery.js +45 -0
  189. package/server/text-request-options.js +23 -0
  190. package/server/text-requester.js +231 -0
  191. package/server/token-auth.js +9 -0
  192. package/server/whole-model-requester.js +99 -0
  193. package/shared/config/chat-models.json +4685 -0
  194. package/shared/config/doppler-execution-contracts.js +32 -0
  195. package/shared/config/doppler-execution-contracts.json +43 -0
  196. package/shared/config/doppler-generation-contract.js +223 -0
  197. package/shared/config/doppler-local-models.js +79 -0
  198. package/shared/config/doppler-package.json +7 -0
  199. package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
  200. package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
  201. package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
  202. package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
  203. package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
  204. package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
  205. package/shared/config/reploid-library.json +49 -0
  206. package/shared/config/swarm-bootstrap.json +21 -0
  207. package/shared/infrastructure/doppler-runtime-service.js +261 -0
  208. package/shared/pool/executable-pack.js +87 -0
  209. package/shared/pool/pack-operation-adapters.js +124 -0
  210. package/shared/pool/pack-operation-policy.js +75 -0
  211. package/shared/pool/pool-config.json +2280 -0
  212. package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
  213. package/shared/vendor/reploid/artifacts/binary.js +25 -0
  214. package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
  215. package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
  216. package/shared/vendor/reploid/artifacts/identity.js +91 -0
  217. package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
  218. package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
  219. package/shared/vendor/reploid/artifacts/signatures.js +94 -0
  220. package/shared/vendor/reploid/config/defaults.json +110 -0
  221. package/shared/vendor/reploid/config/index.js +209 -0
  222. package/shared/vendor/reploid/config/request-overrides.json +9 -0
  223. package/shared/vendor/reploid/hashing.js +30 -0
  224. package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
  225. package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
  226. package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
  227. package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
  228. package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
  229. package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
  230. package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
  231. package/shared/vendor/reploid/rules/routing.rules.json +24 -0
  232. package/shared/vendor/reploid/transport/partition-errors.json +16 -0
  233. package/README.md +0 -3
@@ -0,0 +1,404 @@
1
+ /** Reusable artifact custody behavior. The host supplies policy, protocol and execution ports. */
2
+ export { createCustodyExchange } from './exchange.js';
3
+ export function createCustodyContracts(ports) {
4
+ const { hashDopplerEvidence, validateExecutablePack, executablePacksMatch, sha256Hex, signCanonical, verifyCanonicalSignature } = ports;
5
+ for (const name of ["hashDopplerEvidence","validateExecutablePack","executablePacksMatch","sha256Hex","signCanonical","verifyCanonicalSignature"]) {
6
+ if (ports[name] === undefined) throw new TypeError('Missing createCustodyContracts port: ' + name);
7
+ }
8
+ /**
9
+ * Peer-only dependency transport for Doppler's artifactStore port.
10
+ * authorization is an authenticated requester/control-plane input, never a peer advert.
11
+ * Its indexDigest pins chunk commitments to the exact Pack closure. Doppler still
12
+ * owns Pack signature verification, final artifact verification, and verified caching.
13
+ */
14
+
15
+
16
+
17
+ const PREFIX = 'reploid.pool.pack-custody';
18
+ const digest = (value) => /^sha256:[0-9a-f]{64}$/.test(value);
19
+ const positive = (value) => Number.isSafeInteger(value) && value > 0;
20
+ const clone = (value) => structuredClone(value);
21
+ const assert = (condition, message) => { if (!condition) throw new Error(`Pack custody: ${message}`); };
22
+ const unsigned = ({ signature, ...body }) => body;
23
+ const sign = async (body, key) => ({ ...body, signature: await signCanonical(body, key, { domain: body.schema }) });
24
+ const verify = async (message, key, schema) => {
25
+ assert(message?.schema === schema, 'unexpected message schema');
26
+ assert(await verifyCanonicalSignature(unsigned(message), key, message.signature, { domain: schema }), 'invalid signature');
27
+ };
28
+
29
+ async function context(authorization, index) {
30
+ const grant = clone(authorization);
31
+ const manifest = clone(index);
32
+ const artifactSet = grant?.schema === `${PREFIX}-authorization/v2`;
33
+ assert(artifactSet || grant?.schema === `${PREFIX}-authorization/v1`, 'authorization is required');
34
+ if (artifactSet) {
35
+ assert(grant.artifactSet?.schema === 'reploid.pool.artifact-set/v1' && digest(grant.artifactSet.identity)
36
+ && Array.isArray(grant.artifactSet.artifacts) && grant.artifactSet.artifacts.length > 0
37
+ && grant.pack === undefined && grant.envelopeArtifact === undefined, 'exact artifact set required');
38
+ } else {
39
+ const validation = validateExecutablePack(grant.pack);
40
+ assert(validation.ok, validation.reasons.join('; '));
41
+ assert(await hashDopplerEvidence(grant.pack.artifacts) === grant.pack.artifactClosureDigest, 'closure mismatch');
42
+ }
43
+ assert(positive(grant.attempt) && typeof grant.transferId === 'string' && grant.transferId, 'transfer identity required');
44
+ assert(Number.isSafeInteger(grant.expiresAt), 'expiry required');
45
+ assert(grant.requester?.peerId && grant.requester.publicKey, 'requester identity required');
46
+ assert(Array.isArray(grant.suppliers) && grant.suppliers.length, 'authorized suppliers required');
47
+ const peers = new Map();
48
+ for (const peer of grant.suppliers) {
49
+ assert(peer.peerId && peer.publicKey && !peers.has(peer.peerId), 'invalid or duplicate supplier');
50
+ peers.set(peer.peerId, peer.publicKey);
51
+ }
52
+ for (const field of ['maxArtifactBytes', 'maxChunkBytes', 'maxTransferBytes', 'requestTimeoutMs']) {
53
+ assert(positive(grant.limits?.[field]), `explicit ${field} required`);
54
+ }
55
+ assert(grant.limits.requestTimeoutMs <= 2147483647, 'request timeout out of range');
56
+ assert(digest(grant.indexDigest) && await hashDopplerEvidence(manifest) === grant.indexDigest, 'unauthorized chunk index');
57
+ if (artifactSet) assert(manifest.schema === `${PREFIX}-index/v2`
58
+ && manifest.artifactSetIdentity === grant.artifactSet.identity, 'index artifact set mismatch');
59
+ else assert(manifest.schema === `${PREFIX}-index/v1`
60
+ && manifest.envelopeDigest === grant.pack.envelopeDigest
61
+ && manifest.artifactClosureDigest === grant.pack.artifactClosureDigest, 'index Pack mismatch');
62
+ const declaredArtifacts = [...(artifactSet ? grant.artifactSet.artifacts : grant.pack.artifacts)];
63
+ if (grant.envelopeArtifact !== undefined) {
64
+ const envelope = grant.envelopeArtifact;
65
+ assert(envelope?.role === 'pack-envelope' && typeof envelope.artifactId === 'string' && envelope.artifactId
66
+ && typeof envelope.path === 'string' && envelope.path && digest(envelope.hash) && positive(envelope.sizeBytes)
67
+ && !declaredArtifacts.some((artifact) => artifact.artifactId === envelope.artifactId), 'invalid authorized Pack envelope');
68
+ declaredArtifacts.push(envelope);
69
+ }
70
+ assert(Array.isArray(manifest.artifacts) && manifest.artifacts.length === declaredArtifacts.length, 'index does not close Pack');
71
+ const artifacts = new Map();
72
+ for (const artifact of declaredArtifacts) {
73
+ assert(typeof artifact.artifactId === 'string' && artifact.artifactId.length > 0 && !artifacts.has(artifact.artifactId)
74
+ && typeof artifact.path === 'string' && artifact.path.length > 0 && typeof artifact.role === 'string'
75
+ && digest(artifact.hash) && positive(artifact.sizeBytes), 'invalid or duplicate artifact identity');
76
+ const entry = manifest.artifacts.find((item) => item.artifactId === artifact.artifactId);
77
+ assert(entry && entry.hash === artifact.hash && entry.sizeBytes === artifact.sizeBytes, 'index artifact mismatch');
78
+ assert(artifact.sizeBytes <= grant.limits.maxArtifactBytes && Array.isArray(entry.chunks), 'artifact limit or chunks invalid');
79
+ let offset = 0;
80
+ for (const [index, chunk] of entry.chunks.entries()) {
81
+ assert(chunk.index === index && chunk.offset === offset && positive(chunk.sizeBytes)
82
+ && chunk.sizeBytes <= grant.limits.maxChunkBytes && digest(chunk.hash), 'invalid chunk geometry or commitment');
83
+ offset += chunk.sizeBytes;
84
+ assert(Number.isSafeInteger(offset) && offset <= artifact.sizeBytes, 'chunk range outside artifact');
85
+ }
86
+ assert(offset === artifact.sizeBytes, 'incomplete chunk index');
87
+ artifacts.set(artifact.artifactId, { artifact, chunks: entry.chunks });
88
+ }
89
+ return { grant, peers, artifacts, authorizationHash: await hashDopplerEvidence(grant) };
90
+ }
91
+
92
+ function checkInventory(inventory, ctx, now) {
93
+ assert(inventory.authorizationHash === ctx.authorizationHash && ctx.peers.has(inventory.peerId), 'unauthorized inventory');
94
+ assert(Number.isSafeInteger(inventory.expiresAt) && inventory.expiresAt > now
95
+ && inventory.expiresAt <= ctx.grant.expiresAt, 'expired inventory');
96
+ assert(positive(inventory.maxBytes) && inventory.maxBytes <= ctx.grant.limits.maxTransferBytes, 'inventory byte limit invalid');
97
+ assert(Array.isArray(inventory.artifacts), 'inventory artifacts required');
98
+ const ids = new Set();
99
+ for (const entry of inventory.artifacts) {
100
+ const declared = ctx.artifacts.get(entry.artifactId);
101
+ assert(declared && !ids.has(entry.artifactId) && Array.isArray(entry.chunkIndexes), 'inventory artifact invalid');
102
+ ids.add(entry.artifactId);
103
+ assert(new Set(entry.chunkIndexes).size === entry.chunkIndexes.length
104
+ && entry.chunkIndexes.every((index) => Number.isSafeInteger(index) && index >= 0 && declared.chunks[index]), 'inventory chunks invalid');
105
+ }
106
+ }
107
+
108
+ function checkRequest(request, ctx, inventory, now) {
109
+ checkInventory(inventory, ctx, now);
110
+ const grant = ctx.grant;
111
+ assert(request.authorizationHash === ctx.authorizationHash && request.transferId === grant.transferId
112
+ && request.attempt === grant.attempt && request.requesterPeerId === grant.requester.peerId
113
+ && request.supplierPeerId === inventory.peerId && typeof request.nonce === 'string' && request.nonce, 'request binding mismatch');
114
+ const chunk = ctx.artifacts.get(request.artifactId)?.chunks[request.chunkIndex];
115
+ assert(chunk && Number.isSafeInteger(request.chunkIndex) && chunk.offset === request.offset
116
+ && chunk.sizeBytes === request.sizeBytes, 'request range mismatch');
117
+ assert(inventory.artifacts.some((entry) => entry.artifactId === request.artifactId
118
+ && entry.chunkIndexes.includes(request.chunkIndex)), 'chunk not advertised');
119
+ return chunk;
120
+ }
121
+
122
+ async function createPeerPackSupplier({ authorization, index, peerId, privateKey, inventory, readChunk, now = Date.now }) {
123
+ const inventorySnapshot = clone(inventory);
124
+ const ctx = await context(authorization, index);
125
+ assert(typeof readChunk === 'function', 'readChunk port required');
126
+ const advert = { ...inventorySnapshot, schema: `${PREFIX}-inventory/v1`, authorizationHash: ctx.authorizationHash, peerId };
127
+ checkInventory(advert, ctx, now());
128
+ const signedInventory = await sign(advert, privateKey);
129
+ await verify(signedInventory, ctx.peers.get(peerId), advert.schema);
130
+ let reservedBytes = 0;
131
+ let closed = false;
132
+ return {
133
+ inventory: clone(signedInventory),
134
+ close() { closed = true; },
135
+ async serve(message) {
136
+ const request = clone(message);
137
+ assert(!closed, 'supplier closed');
138
+ const chunk = checkRequest(request, ctx, advert, now());
139
+ await verify(request, ctx.grant.requester.publicKey, `${PREFIX}-request/v1`);
140
+ assert(!closed, 'supplier closed');
141
+ checkRequest(request, ctx, advert, now());
142
+ assert(reservedBytes + chunk.sizeBytes <= advert.maxBytes, 'supplier byte budget exhausted');
143
+ reservedBytes += chunk.sizeBytes;
144
+ const source = await readChunk(request.artifactId, clone(chunk));
145
+ assert(source instanceof Uint8Array && source.byteLength === chunk.sizeBytes, 'supplier chunk size mismatch');
146
+ const bytes = Uint8Array.from(source);
147
+ const bytesHash = await sha256Hex(bytes);
148
+ assert(bytesHash === chunk.hash, 'supplier chunk integrity mismatch');
149
+ const body = await sign({ schema: `${PREFIX}-response/v1`, authorizationHash: ctx.authorizationHash,
150
+ peerId, requestHash: await hashDopplerEvidence(unsigned(request)), bytesHash }, privateKey);
151
+ assert(!closed, 'supplier closed');
152
+ checkRequest(request, ctx, advert, now());
153
+ return { message: body, bytes };
154
+ }
155
+ };
156
+ }
157
+
158
+ // A deadline is enforced here even when a transport ignores its AbortSignal.
159
+ async function boundedTransfer(requestChunk, peerId, request, signal, timeoutMs) {
160
+ const controller = new AbortController();
161
+ let timer;
162
+ let abort;
163
+ try {
164
+ return await new Promise((resolve, reject) => {
165
+ abort = () => { controller.abort(); reject(new Error('Pack custody: transfer cancelled')); };
166
+ signal.addEventListener('abort', abort, { once: true });
167
+ if (signal.aborted) return abort();
168
+ timer = setTimeout(() => { controller.abort(); reject(new Error('Pack custody: supplier timeout')); }, timeoutMs);
169
+ Promise.resolve().then(() => {
170
+ assert(!controller.signal.aborted, 'transfer cancelled');
171
+ // The transport must enforce this bound while receiving/framing bytes.
172
+ return requestChunk(peerId, request, { signal: controller.signal, maxBytes: request.sizeBytes });
173
+ }).then(resolve, reject);
174
+ });
175
+ } finally {
176
+ clearTimeout(timer);
177
+ signal.removeEventListener('abort', abort);
178
+ controller.abort();
179
+ }
180
+ }
181
+
182
+ async function createPeerPackArtifactStore({ authorization, index, inventories, requesterPrivateKey, requestChunk, now = Date.now, signal, checkpoints = null, maxConcurrentChunks = 1 }) {
183
+ const adverts = clone(inventories);
184
+ const ctx = await context(authorization, index);
185
+ assert(typeof requestChunk === 'function', 'peer transport required');
186
+ assert(positive(maxConcurrentChunks) && maxConcurrentChunks <= (ctx.grant.limits.maxConcurrentChunks ?? 1), 'chunk concurrency exceeds authorization');
187
+ if (checkpoints) assert(['getChunk', 'putChunk', 'deleteChunk'].every((name) => typeof checkpoints[name] === 'function'), 'checkpoint ports required');
188
+ assert(Array.isArray(adverts), 'inventories required');
189
+ const seen = new Set();
190
+ for (const advert of adverts) {
191
+ checkInventory(advert, ctx, now());
192
+ assert(!seen.has(advert.peerId), 'duplicate supplier inventory');
193
+ seen.add(advert.peerId);
194
+ await verify(advert, ctx.peers.get(advert.peerId), `${PREFIX}-inventory/v1`);
195
+ }
196
+ const controller = new AbortController();
197
+ const cancel = () => controller.abort();
198
+ signal?.addEventListener('abort', cancel, { once: true });
199
+ if (signal?.aborted) cancel();
200
+ const active = () => {
201
+ assert(!controller.signal.aborted, 'store closed or cancelled');
202
+ assert(ctx.grant.expiresAt > now(), 'authorization expired');
203
+ assert(receivedBytes <= ctx.grant.limits.maxTransferBytes, 'received byte budget exceeded');
204
+ };
205
+ const attempts = [];
206
+ const completed = [];
207
+ const peerBytes = new Map();
208
+ let reservedBytes = 0;
209
+ let receivedBytes = 0;
210
+ let verificationBytes = 0;
211
+ const startedAt = now();
212
+ let cacheBytes = 0;
213
+ let corruptCacheBytes = 0;
214
+ let persistedBytes = 0;
215
+ let evictedBytes = 0;
216
+ let duplicateBytes = 0;
217
+ let inFlightBytes = 0;
218
+ let peakInFlightBytes = 0;
219
+ let peakArtifactBytes = 0;
220
+ let firstArtifactAt = null;
221
+ const receivedHashes = new Set();
222
+ const pending = new Map();
223
+ // Only one artifact allocation at a time; its chunks use the authorized fanout.
224
+ let tail = Promise.resolve();
225
+ async function acquire(input) {
226
+ active();
227
+ const entry = ctx.artifacts.get(input?.artifactId);
228
+ assert(entry && executablePacksMatch(entry.artifact, input), 'artifact outside authorized closure');
229
+ const output = new Uint8Array(entry.artifact.sizeBytes);
230
+ peakArtifactBytes = Math.max(peakArtifactBytes, output.length);
231
+ const acquisition = new AbortController();
232
+ const cancelAcquisition = () => acquisition.abort();
233
+ controller.signal.addEventListener('abort', cancelAcquisition, { once: true });
234
+ const current = () => { active(); assert(!acquisition.signal.aborted, 'artifact acquisition cancelled'); };
235
+ async function acquireChunk(chunk) {
236
+ current();
237
+ if (checkpoints) {
238
+ const restored = await checkpoints.getChunk(clone(chunk), { signal: acquisition.signal });
239
+ current();
240
+ if (restored !== null) {
241
+ assert(restored instanceof Uint8Array, 'checkpoint returned invalid bytes');
242
+ const cached = restored.slice();
243
+ verificationBytes += cached.byteLength;
244
+ if (cached instanceof Uint8Array && cached.byteLength === chunk.sizeBytes && await sha256Hex(cached) === chunk.hash) {
245
+ current();
246
+ output.set(cached, chunk.offset);
247
+ cacheBytes += cached.byteLength;
248
+ return;
249
+ }
250
+ corruptCacheBytes += cached.byteLength;
251
+ await checkpoints.deleteChunk(clone(chunk), { signal: acquisition.signal });
252
+ current();
253
+ }
254
+ }
255
+ const suppliers = adverts.filter((advert) => advert.artifacts.some((item) => item.artifactId === input.artifactId
256
+ && item.chunkIndexes.includes(chunk.index)));
257
+ let received = false;
258
+ let tried = 0;
259
+ let undelivered = 0;
260
+ for (const advert of suppliers) {
261
+ current();
262
+ if (advert.expiresAt <= now() || (peerBytes.get(advert.peerId) || 0) + chunk.sizeBytes > advert.maxBytes) continue;
263
+ assert(reservedBytes + chunk.sizeBytes <= ctx.grant.limits.maxTransferBytes, 'transfer byte budget exhausted');
264
+ reservedBytes += chunk.sizeBytes;
265
+ peerBytes.set(advert.peerId, (peerBytes.get(advert.peerId) || 0) + chunk.sizeBytes);
266
+ const observation = { peerId: advert.peerId, artifactId: input.artifactId, chunkIndex: chunk.index,
267
+ reservedBytes: chunk.sizeBytes, receivedBytes: 0, status: 'pending', requestHash: null };
268
+ attempts.push(observation);
269
+ tried += 1;
270
+ let delivered = false;
271
+ try {
272
+ const request = await sign({ schema: `${PREFIX}-request/v1`, authorizationHash: ctx.authorizationHash,
273
+ transferId: ctx.grant.transferId, attempt: ctx.grant.attempt, requesterPeerId: ctx.grant.requester.peerId,
274
+ supplierPeerId: advert.peerId, artifactId: input.artifactId, chunkIndex: chunk.index,
275
+ offset: chunk.offset, sizeBytes: chunk.sizeBytes, nonce: crypto.randomUUID() }, requesterPrivateKey);
276
+ observation.requestHash = await hashDopplerEvidence(unsigned(request));
277
+ observation.request = clone(request);
278
+ active();
279
+ checkInventory(advert, ctx, now());
280
+ current();
281
+ inFlightBytes += chunk.sizeBytes;
282
+ peakInFlightBytes = Math.max(peakInFlightBytes, inFlightBytes);
283
+ let response;
284
+ try {
285
+ response = await boundedTransfer(requestChunk, advert.peerId, request, acquisition.signal,
286
+ Math.min(ctx.grant.limits.requestTimeoutMs, advert.expiresAt - now(), ctx.grant.expiresAt - now()));
287
+ } finally { inFlightBytes -= chunk.sizeBytes; }
288
+ delivered = true;
289
+ observation.receivedBytes = response?.bytes instanceof Uint8Array ? response.bytes.byteLength : 0;
290
+ receivedBytes += observation.receivedBytes;
291
+ assert(response?.bytes instanceof Uint8Array && response.bytes.byteLength === chunk.sizeBytes, 'response size mismatch');
292
+ const bytes = Uint8Array.from(response.bytes);
293
+ const message = clone(response.message);
294
+ observation.response = message;
295
+ active();
296
+ checkInventory(advert, ctx, now());
297
+ await verify(message, ctx.peers.get(advert.peerId), `${PREFIX}-response/v1`);
298
+ assert(message.authorizationHash === ctx.authorizationHash && message.peerId === advert.peerId
299
+ && message.requestHash === observation.requestHash && message.bytesHash === chunk.hash, 'response binding mismatch');
300
+ verificationBytes += bytes.byteLength;
301
+ observation.observedBytesHash = await sha256Hex(bytes);
302
+ assert(observation.observedBytesHash === chunk.hash, 'chunk integrity mismatch');
303
+ active();
304
+ checkInventory(advert, ctx, now());
305
+ current();
306
+ if (receivedHashes.has(chunk.hash)) duplicateBytes += bytes.length;
307
+ receivedHashes.add(chunk.hash);
308
+ if (checkpoints) {
309
+ // Storage failures are visible; verified output remains usable and does
310
+ // not trigger an unnecessary download from another supplier.
311
+ try {
312
+ const saved = await checkpoints.putChunk(clone(chunk), bytes.slice(), { signal: acquisition.signal });
313
+ persistedBytes += bytes.length;
314
+ evictedBytes += saved?.evictedBytes || 0;
315
+ } catch (error) { observation.checkpointError = String(error.message || error); }
316
+ current();
317
+ }
318
+ output.set(bytes, chunk.offset);
319
+ observation.status = 'accepted';
320
+ received = true;
321
+ break;
322
+ } catch (error) {
323
+ observation.status = 'rejected';
324
+ observation.error = String(error.message || error);
325
+ if (!delivered) undelivered += 1;
326
+ current();
327
+ }
328
+ }
329
+ if (!received) {
330
+ const error = new Error(`Pack custody: no authorized supplier completed ${input.artifactId} chunk ${chunk.index}`);
331
+ // No supplier delivered bytes (a stall, a disconnect or a refusal), so nothing invalid
332
+ // arrived and a later grant may resume from the verified checkpoints.
333
+ if (tried > 0 && undelivered === tried) error.code = 'CUSTODY_SUPPLIER_UNAVAILABLE';
334
+ throw error;
335
+ }
336
+ }
337
+ let nextChunk = 0;
338
+ let failure = null;
339
+ try {
340
+ await Promise.all(Array.from({ length: Math.min(maxConcurrentChunks, entry.chunks.length) }, async () => {
341
+ try {
342
+ while (nextChunk < entry.chunks.length) {
343
+ current();
344
+ const chunk = entry.chunks[nextChunk++];
345
+ await acquireChunk(chunk);
346
+ }
347
+ } catch (error) { failure ||= error; acquisition.abort(); }
348
+ }));
349
+ if (failure) throw failure;
350
+ current();
351
+ } finally {
352
+ controller.signal.removeEventListener('abort', cancelAcquisition);
353
+ acquisition.abort();
354
+ }
355
+ verificationBytes += output.byteLength;
356
+ assert(await sha256Hex(output) === entry.artifact.hash, 'reconstructed artifact integrity mismatch');
357
+ active();
358
+ firstArtifactAt ??= now();
359
+ if (!completed.some((item) => item.artifactId === input.artifactId)) {
360
+ completed.push({ artifactId: input.artifactId, hash: entry.artifact.hash, sizeBytes: output.byteLength });
361
+ }
362
+ return output;
363
+ }
364
+ return {
365
+ readEnvelope() {
366
+ assert(ctx.grant.envelopeArtifact, 'no Pack envelope authorized');
367
+ return this.readArtifact(ctx.grant.envelopeArtifact);
368
+ },
369
+ readArtifact(artifact) {
370
+ const snapshot = clone(artifact);
371
+ const key = JSON.stringify(snapshot);
372
+ if (pending.has(key)) return pending.get(key).then((bytes) => bytes.slice());
373
+ const task = tail.then(() => acquire(snapshot));
374
+ pending.set(key, task);
375
+ tail = task.catch(() => {}).finally(() => pending.delete(key));
376
+ return task.then((bytes) => bytes.slice());
377
+ },
378
+ close() { cancel(); signal?.removeEventListener('abort', cancel); },
379
+ getReceipt() {
380
+ return clone({ schema: `${PREFIX}-receipt/v1`, authorizationHash: ctx.authorizationHash,
381
+ transferId: ctx.grant.transferId, attempt: ctx.grant.attempt,
382
+ ...(ctx.grant.artifactSet ? { artifactSetIdentity: ctx.grant.artifactSet.identity }
383
+ : { envelopeDigest: ctx.grant.pack.envelopeDigest, artifactClosureDigest: ctx.grant.pack.artifactClosureDigest }),
384
+ indexDigest: ctx.grant.indexDigest,
385
+ source: cacheBytes > 0 ? (receivedBytes > 0 ? 'cache-and-peer' : 'cache') : 'peer',
386
+ reservedBytes, receivedBytes, verificationBytes, attempts, completed,
387
+ inventories: adverts,
388
+ cacheBytes, corruptCacheBytes, persistedBytes, evictedBytes, duplicateBytes,
389
+ maxConcurrentChunks, peakInFlightBytes, peakArtifactBytes,
390
+ firstArtifactMs: firstArtifactAt === null ? null : firstArtifactAt - startedAt,
391
+ elapsedMs: now() - startedAt,
392
+ // Retained payload accounting is not a measurement of the browser's heap.
393
+ browserHeapBytes: null,
394
+ // Transport framing, interrupted partial bytes, and relay traffic require
395
+ // measurements from the transport owner. Never report unknown costs as zero.
396
+ wireBytes: null, relayBytes: null, interruptedBytes: null });
397
+ }
398
+ };
399
+ }
400
+
401
+ return Object.freeze({ createPeerPackSupplier, createPeerPackArtifactStore });
402
+ }
403
+
404
+ export { createPieceAcquisition } from './piece-acquisition.js';
@@ -0,0 +1,19 @@
1
+ /** Shared record validation; hosts provide the atomic durable transaction. */
2
+ export function custodyBudgetError(code, message, cause) {
3
+ return Object.assign(new Error(message, { cause }), { code: 'CUSTODY_BUDGET_' + code });
4
+ }
5
+ export function reserveCustodyBudget(record, bytes, maxBytes, purpose = 'supply') {
6
+ if (!['supply', 'acquisition'].includes(purpose)) throw custodyBudgetError('INVALID', 'Unknown transfer allowance purpose');
7
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || !Number.isSafeInteger(bytes) || bytes < 0) {
8
+ throw custodyBudgetError('INVALID', 'Invalid file contribution allowance');
9
+ }
10
+ if (!record) throw custodyBudgetError('MISSING', 'File contribution ledger missing; previous allowance cannot be inferred');
11
+ if (record.schema !== `reploid.custody-${purpose}-budget/v1` || !Number.isSafeInteger(record.maxBytes)
12
+ || record.maxBytes <= 0 || !Number.isSafeInteger(record.reservedBytes)
13
+ || record.reservedBytes < 0 || record.reservedBytes > record.maxBytes) {
14
+ throw custodyBudgetError('CORRUPT', 'File contribution ledger is corrupt');
15
+ }
16
+ if (record.maxBytes !== maxBytes) throw custodyBudgetError('POLICY', 'File contribution allowance changed; renewal is not authorized');
17
+ if (bytes > maxBytes - record.reservedBytes) throw custodyBudgetError('LIMIT', 'File contribution allowance exhausted');
18
+ return { ...record, reservedBytes: record.reservedBytes + bytes };
19
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview Custody-key certificates.
3
+ *
4
+ * Custody signing keys are created fresh for each host process, so a
5
+ * delivery receipt or challenge answer signed with one names no device. The
6
+ * device's persistent participant identity signs a short-lived certificate
7
+ * over the process's custody public key and the custody peer id it uses in
8
+ * grants. A verifier holding the device registration can then attribute the
9
+ * key: certificate, then device, then account. Uncertified keys stay
10
+ * unattributable. WebCrypto only, so any host can sign.
11
+ */
12
+
13
+ import { importPublicKey } from './signatures.js';
14
+ import { participantIdFor, signAsParticipant, verifyParticipantSignature } from './participant-proofs.js';
15
+
16
+ export const CUSTODY_CERTIFICATE_SCHEMA = 'reploid.custody-key-certificate/v1';
17
+
18
+ /** The lifetime a native host signs; each verifier enforces its own explicit maximum. */
19
+ export const CUSTODY_CERTIFICATE_LIFETIME_MS = 60 * 60_000;
20
+
21
+ const BODY_KEYS = Object.freeze(['schema', 'certificateId', 'participantId', 'custodyPeerId', 'custodyPublicKey', 'issuedAt', 'expiresAt']);
22
+ const CERTIFICATE_KEYS = Object.freeze([...BODY_KEYS, 'signature'].sort());
23
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_.:@+-]{0,159}$/;
24
+ const toMs = (value) => (typeof value === 'string' ? Date.parse(value) : Number.NaN);
25
+
26
+ export const custodyCertificateBody = (certificate) => Object.fromEntries(BODY_KEYS.map((key) => [key, certificate[key]]));
27
+
28
+ /** Sign a certificate for `custodyPublicKey` (base64 SPKI) with the participant identity bundle. */
29
+ export async function createCustodyKeyCertificate({
30
+ identity, custodyPeerId, custodyPublicKey, issuedAtMs, lifetimeMs, certificateId = `cert_${crypto.randomUUID()}`
31
+ }) {
32
+ const participantId = await participantIdFor(identity?.publicJwk);
33
+ if (!identity?.privateJwk || identity.peerId !== participantId) throw new TypeError('A valid persistent participant identity is required');
34
+ if (!ID.test(String(custodyPeerId)) || typeof custodyPublicKey !== 'string') throw new TypeError('custodyPeerId and custodyPublicKey are required');
35
+ if (!Number.isSafeInteger(issuedAtMs) || !Number.isSafeInteger(lifetimeMs) || lifetimeMs <= 0) throw new TypeError('issuedAtMs and a positive lifetimeMs are required');
36
+ const body = {
37
+ schema: CUSTODY_CERTIFICATE_SCHEMA,
38
+ certificateId,
39
+ participantId,
40
+ custodyPeerId,
41
+ custodyPublicKey,
42
+ issuedAt: new Date(issuedAtMs).toISOString(),
43
+ expiresAt: new Date(issuedAtMs + lifetimeMs).toISOString()
44
+ };
45
+ return { ...body, signature: await signAsParticipant(identity, CUSTODY_CERTIFICATE_SCHEMA, body) };
46
+ }
47
+
48
+ /** Lifetime and validity-window problems at `atMs`; the signature is not checked. */
49
+ export function certificateWindowProblems({ issuedAt, expiresAt }, { atMs, maxLifetimeMs, maxClockSkewMs }) {
50
+ const issued = toMs(issuedAt);
51
+ const expires = toMs(expiresAt);
52
+ const problems = [];
53
+ if (!(expires - issued <= maxLifetimeMs)) problems.push('certificate_lifetime_exceeded');
54
+ if (!(atMs >= issued - maxClockSkewMs)) problems.push('certificate_not_yet_valid');
55
+ if (!(atMs < expires)) problems.push('certificate_expired');
56
+ return problems;
57
+ }
58
+
59
+ /**
60
+ * Problems with `certificate` as a statement by the participant key
61
+ * `publicJwk` covering time `atMs`; an empty list means valid. Malformed
62
+ * certificates return one problem without checking further.
63
+ */
64
+ export async function checkCustodyKeyCertificate(certificate, { publicJwk, atMs, maxLifetimeMs, maxClockSkewMs }) {
65
+ if (!certificate || typeof certificate !== 'object' || Array.isArray(certificate)
66
+ || Object.keys(certificate).sort().join('\u0000') !== CERTIFICATE_KEYS.join('\u0000')) return ['certificate_malformed'];
67
+ const { schema, certificateId, participantId, custodyPeerId, custodyPublicKey, issuedAt, expiresAt, signature } = certificate;
68
+ const issued = toMs(issuedAt);
69
+ const expires = toMs(expiresAt);
70
+ if (schema !== CUSTODY_CERTIFICATE_SCHEMA || !ID.test(String(certificateId)) || !ID.test(String(custodyPeerId))
71
+ || typeof participantId !== 'string' || typeof custodyPublicKey !== 'string' || custodyPublicKey.length > 512
72
+ || !Number.isFinite(issued) || !Number.isFinite(expires) || expires <= issued) return ['certificate_malformed'];
73
+ try {
74
+ await importPublicKey(custodyPublicKey);
75
+ } catch {
76
+ return ['certificate_malformed'];
77
+ }
78
+ const problems = certificateWindowProblems(certificate, { atMs, maxLifetimeMs, maxClockSkewMs });
79
+ if (participantId !== await participantIdFor(publicJwk)) {
80
+ problems.push('certificate_participant_mismatch');
81
+ } else if (!await verifyParticipantSignature(publicJwk, CUSTODY_CERTIFICATE_SCHEMA, custodyCertificateBody(certificate), signature)) {
82
+ problems.push('certificate_signature_invalid');
83
+ }
84
+ return problems;
85
+ }
86
+
87
+ /**
88
+ * Host side: certify the process's custody key with the persistent identity,
89
+ * reusing the current certificate until half its lifetime remains.
90
+ * `onIssue(certificate)` observes each newly signed certificate.
91
+ */
92
+ export function createCustodyKeyCertifier({
93
+ identity, lifetimeMs = CUSTODY_CERTIFICATE_LIFETIME_MS, now = () => Date.now(), onIssue = () => {}
94
+ }) {
95
+ let current = null;
96
+ let queue = Promise.resolve();
97
+ const fresh = ({ custodyPeerId, custodyPublicKey }) => current !== null
98
+ && current.custodyPeerId === custodyPeerId && current.custodyPublicKey === custodyPublicKey
99
+ && toMs(current.expiresAt) - now() > lifetimeMs / 2;
100
+ return Object.freeze({
101
+ certify(key) {
102
+ if (fresh(key)) return Promise.resolve(current);
103
+ const run = queue.then(async () => {
104
+ if (fresh(key)) return current;
105
+ current = await createCustodyKeyCertificate({ identity, ...key, issuedAtMs: now(), lifetimeMs });
106
+ onIssue(current);
107
+ return current;
108
+ });
109
+ queue = run.catch(() => {});
110
+ return run;
111
+ }
112
+ });
113
+ }
114
+
115
+ export default {
116
+ CUSTODY_CERTIFICATE_SCHEMA,
117
+ CUSTODY_CERTIFICATE_LIFETIME_MS,
118
+ custodyCertificateBody,
119
+ createCustodyKeyCertificate,
120
+ certificateWindowProblems,
121
+ checkCustodyKeyCertificate,
122
+ createCustodyKeyCertifier
123
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @fileoverview Custody delivery attestations.
3
+ *
4
+ * After a verified acquisition, the acquirer signs the exact custody receipt
5
+ * with the custody key its grant names. The statement says the bytes arrived
6
+ * and verified; supplier-held records cannot say that. A verifier attributes
7
+ * the custody key to a participant only through that participant's custody
8
+ * certificate, so an uncertified host's attestation names no one and hosts
9
+ * without an identity sign none. `hash` is the evidence digest the custody
10
+ * records use (canonical JSON, sha256), supplied by the caller so signer and
11
+ * verifier share one function. WebCrypto only.
12
+ */
13
+
14
+ import { sha256Hex, signCanonical } from './signatures.js';
15
+
16
+ export const DELIVERY_ATTESTATION_SCHEMA = 'reploid.custody-delivery-attestation/v1';
17
+
18
+ /** The statement an acquirer signs over one completed receipt. */
19
+ export async function deliveryAttestationBody({ receipt, attestedAt, hash }) {
20
+ return {
21
+ schema: DELIVERY_ATTESTATION_SCHEMA,
22
+ authorizationHash: receipt.authorizationHash,
23
+ transferId: receipt.transferId,
24
+ receiptDigest: await hash(receipt),
25
+ completed: receipt.completed,
26
+ attestedAt
27
+ };
28
+ }
29
+
30
+ /** Sign the attestation with the acquirer's custody private key (ECDSA P-256 CryptoKey). */
31
+ export async function signDeliveryAttestation({ receipt, attestedAt, privateKey, hash }) {
32
+ const body = await deliveryAttestationBody({ receipt, attestedAt, hash });
33
+ return { ...body, signature: await signCanonical(body, privateKey, { domain: DELIVERY_ATTESTATION_SCHEMA }) };
34
+ }
35
+
36
+ /** One identifier per delivered artifact of one grant; the supplier derives it from its own grant. */
37
+ export async function deliveryEvidenceId({ authorizationHash, artifactId }) {
38
+ return `dlv_${(await sha256Hex(`${authorizationHash}|${artifactId}`)).slice(7, 39)}`;
39
+ }
40
+
41
+ /**
42
+ * The delivery evidence a certified acquirer submits for one artifact of a
43
+ * requester record from the custody exchange; null without an attestation.
44
+ */
45
+ export function deliveryEvidenceFromRecord(record, artifactId = record?.authorization?.artifactSet?.artifacts?.[0]?.artifactId) {
46
+ if (record?.role !== 'requester' || !record.attestation || !record.certificates?.requester || typeof artifactId !== 'string') return null;
47
+ return structuredClone({
48
+ artifactId,
49
+ authorization: record.authorization,
50
+ index: record.index,
51
+ receipt: record.receipt,
52
+ attestation: record.attestation,
53
+ supplierCertificate: record.certificates.supplier,
54
+ acquirerCertificate: record.certificates.requester
55
+ });
56
+ }
57
+
58
+ export default {
59
+ DELIVERY_ATTESTATION_SCHEMA,
60
+ deliveryAttestationBody,
61
+ signDeliveryAttestation,
62
+ deliveryEvidenceId,
63
+ deliveryEvidenceFromRecord
64
+ };
@@ -0,0 +1,36 @@
1
+ /** Provider-signed cleanup evidence. It carries no invented token measurements. */
2
+ import { canonicalize } from './canonical-json.js';
3
+ import { createPeerIdFromPublicJwk, importSigningKey, importVerificationKey, getIdentitySignAlgorithm,
4
+ encodeBytes, toBase64Url, fromBase64Url } from './identity.js';
5
+
6
+ const digest = value => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
7
+ const text = value => typeof value === 'string' && value.length > 0 && value.length <= 512;
8
+ const assert = (ok, message) => { if (!ok) throw Error(message); };
9
+ function payload(value) {
10
+ assert(value?.schema === 'reploid.generation-terminal/v1' && ['cancelled', 'failed'].includes(value.status)
11
+ && value.cleanup === 'settled' && value.usage === null && ['requestId', 'provider', 'consumer', 'model'].every(key => text(value[key]))
12
+ && digest(value.modelIdentity) && digest(value.inputDigest) && Array.isArray(value.adapterIdentities)
13
+ && value.adapterIdentities.length <= 1 && value.adapterIdentities.every(digest)
14
+ && Number.isSafeInteger(value.timestamp) && value.timestamp > 0, 'Invalid generation terminal evidence');
15
+ return { schema: value.schema, requestId: value.requestId, provider: value.provider, consumer: value.consumer,
16
+ model: value.model, modelIdentity: value.modelIdentity, adapterIdentities: [...value.adapterIdentities],
17
+ inputDigest: value.inputDigest, status: value.status, cleanup: value.cleanup, usage: null, timestamp: value.timestamp };
18
+ }
19
+ const bytes = value => encodeBytes(canonicalize(payload(value)));
20
+ export async function signGenerationTerminal(binding, status, identity) {
21
+ const value = payload({ ...structuredClone(binding), schema: 'reploid.generation-terminal/v1', status,
22
+ cleanup: 'settled', usage: null, timestamp: Date.now() });
23
+ assert(value.provider === identity.peerId, 'Terminal signer differs from provider');
24
+ return { ...value, publicJwk: identity.publicJwk, signature: toBase64Url(await crypto.subtle.sign(
25
+ getIdentitySignAlgorithm(identity), await importSigningKey(identity), bytes(value))) };
26
+ }
27
+ export async function verifyGenerationTerminal(value, binding) {
28
+ try {
29
+ const canonical = payload(value);
30
+ if (await createPeerIdFromPublicJwk(value.publicJwk) !== canonical.provider
31
+ || ['requestId', 'provider', 'consumer', 'model', 'modelIdentity', 'adapterIdentities', 'inputDigest']
32
+ .some(key => canonicalize(canonical[key]) !== canonicalize(binding[key]))) return false;
33
+ return await crypto.subtle.verify(getIdentitySignAlgorithm(value.publicJwk), await importVerificationKey(value.publicJwk),
34
+ fromBase64Url(value.signature), bytes(canonical));
35
+ } catch { return false; }
36
+ }