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.
- package/apps/poolday/config/chat-files.json +27 -0
- package/apps/poolday/config/partition-generation.js +21 -0
- package/apps/poolday/config/partition-policy.json +74 -0
- package/apps/poolday/config/partition-releases.json +3 -0
- package/apps/poolday/config/work-profile.json +122 -0
- package/apps/poolday/host/chat-view.js +47 -0
- package/apps/poolday/host/earning/client.js +199 -0
- package/apps/poolday/host/earning/contribution.js +168 -0
- package/apps/poolday/host/partition-release.js +33 -0
- package/apps/poolday/host/work-partition-artifact-source.js +37 -0
- package/apps/poolday/host/work-partition-capsule.js +39 -0
- package/apps/poolday/host/work-partition-plan.js +167 -0
- package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
- package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
- package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
- package/apps/poolday/pool/pack-operation.js +161 -0
- package/apps/poolday/pool/pack-release-policy.js +44 -0
- package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
- package/apps/poolday/providers/work-adapter.js +67 -0
- package/apps/poolday/providers/work-device.js +33 -0
- package/apps/poolday/providers/work-generation-completion.js +55 -0
- package/apps/poolday/providers/work-provider.js +209 -0
- package/apps/poolday/providers/work-resident-provider.js +113 -0
- package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
- package/docs/fleet-service.md +214 -0
- package/docs/native-participant.md +542 -0
- package/native-delivery.json +1174 -0
- package/package.json +29 -4
- package/packages/reploid/src/artifacts/binary.js +25 -0
- package/packages/reploid/src/artifacts/canonical-json.js +23 -0
- package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
- package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
- package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
- package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
- package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
- package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
- package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
- package/packages/reploid/src/artifacts/identity.js +91 -0
- package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
- package/packages/reploid/src/artifacts/receipt.js +131 -0
- package/packages/reploid/src/artifacts/signatures.js +94 -0
- package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
- package/packages/reploid/src/cancellation.js +44 -0
- package/packages/reploid/src/config/defaults.json +110 -0
- package/packages/reploid/src/config/index.js +209 -0
- package/packages/reploid/src/config/request-overrides.json +9 -0
- package/packages/reploid/src/config/transport-defaults.json +7 -0
- package/packages/reploid/src/hashing.js +30 -0
- package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
- package/packages/reploid/src/measurement/index.js +5 -0
- package/packages/reploid/src/measurement/readiness-records.js +310 -0
- package/packages/reploid/src/mesh/computer-identity.js +50 -0
- package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
- package/packages/reploid/src/mesh/contribution.js +141 -0
- package/packages/reploid/src/mesh/generation-recovery.js +58 -0
- package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
- package/packages/reploid/src/mesh/gpu-signature.js +136 -0
- package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
- package/packages/reploid/src/mesh/index.js +66 -0
- package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
- package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
- package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
- package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
- package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
- package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
- package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
- package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
- package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
- package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
- package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
- package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
- package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
- package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
- package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
- package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
- package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
- package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
- package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
- package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
- package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
- package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
- package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
- package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
- package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
- package/packages/reploid/src/mesh/peer-generation.js +896 -0
- package/packages/reploid/src/mesh/peer-identity.js +87 -0
- package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
- package/packages/reploid/src/mesh/preparation-progress.js +46 -0
- package/packages/reploid/src/mesh/provider-hardware.js +11 -0
- package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
- package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
- package/packages/reploid/src/mesh/web-access-network.js +112 -0
- package/packages/reploid/src/mesh/web-access-policy.json +17 -0
- package/packages/reploid/src/retrieval/citations.js +156 -0
- package/packages/reploid/src/retrieval/claim-support.js +113 -0
- package/packages/reploid/src/retrieval/costs.js +101 -0
- package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
- package/packages/reploid/src/retrieval/index.js +20 -0
- package/packages/reploid/src/retrieval/lexical-support.js +27 -0
- package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
- package/packages/reploid/src/retrieval/page-fetch.json +10 -0
- package/packages/reploid/src/retrieval/page-text.js +182 -0
- package/packages/reploid/src/retrieval/passage.js +37 -0
- package/packages/reploid/src/retrieval/policy.json +14 -0
- package/packages/reploid/src/retrieval/retrieval.js +23 -0
- package/packages/reploid/src/retrieval/review-queue.js +76 -0
- package/packages/reploid/src/retrieval/sources.js +263 -0
- package/packages/reploid/src/retrieval/support-review.js +155 -0
- package/packages/reploid/src/retrieval/text.js +21 -0
- package/packages/reploid/src/rules/contribution.rules.json +8 -0
- package/packages/reploid/src/rules/routing.rules.json +24 -0
- package/packages/reploid/src/transport/assignment.js +571 -0
- package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
- package/packages/reploid/src/transport/index.js +11 -0
- package/packages/reploid/src/transport/introduction-admission.js +24 -0
- package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
- package/packages/reploid/src/transport/introduction-policy.json +12 -0
- package/packages/reploid/src/transport/introduction-records.js +90 -0
- package/packages/reploid/src/transport/introductions.js +209 -0
- package/packages/reploid/src/transport/partition-data-channel.js +421 -0
- package/packages/reploid/src/transport/partition-errors.json +16 -0
- package/packages/reploid/src/transport/retry-policy.js +36 -0
- package/packages/reploid/src/transport/room.js +502 -0
- package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
- package/packages/reploid/src/transport/signaling.js +571 -0
- package/packages/reploid/src/transport/swarm.js +1247 -0
- package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
- package/server/browser-roots.js +42 -0
- package/server/browser-web-worker.js +94 -0
- package/server/chat-completions.js +45 -0
- package/server/fleet/browser-bridge.js +145 -0
- package/server/fleet/contract.js +42 -0
- package/server/fleet/credit-evidence.js +103 -0
- package/server/fleet/credit-recovery.js +175 -0
- package/server/fleet/credits.js +178 -0
- package/server/fleet/execution-admission.js +32 -0
- package/server/fleet/native-host.js +817 -0
- package/server/fleet/placement.js +132 -0
- package/server/fleet/requester.js +346 -0
- package/server/fleet/service.js +311 -0
- package/server/fleet/terminal-policy.js +83 -0
- package/server/fleet/text-api.js +94 -0
- package/server/hashing.js +35 -0
- package/server/http-request-owner.js +49 -0
- package/server/messages.js +57 -0
- package/server/native-canonical-epochs.js +90 -0
- package/server/native-change-log.js +40 -0
- package/server/native-earning.js +203 -0
- package/server/native-generation-journal.js +46 -0
- package/server/native-model-config.js +57 -0
- package/server/native-model-files.js +382 -0
- package/server/native-model-origin.js +54 -0
- package/server/native-model-origins.json +12 -0
- package/server/native-node.js +85 -0
- package/server/native-owner/controls.js +162 -0
- package/server/native-owner/monitor.js +251 -0
- package/server/native-owner/policy.js +273 -0
- package/server/native-owner/rate.js +92 -0
- package/server/native-participant.js +866 -0
- package/server/native-readiness-log.js +30 -0
- package/server/native-release-storage.js +64 -0
- package/server/native-shader-cache.js +369 -0
- package/server/native-supply-budget.js +66 -0
- package/server/native-whole-earning.js +67 -0
- package/server/native-whole-model-fill.js +182 -0
- package/server/native-whole-model.js +130 -0
- package/server/public-web-worker.js +66 -0
- package/server/responses.js +92 -0
- package/server/search/errors.js +47 -0
- package/server/search/page-fetch.js +185 -0
- package/server/text-api.js +83 -0
- package/server/text-recovery.js +45 -0
- package/server/text-request-options.js +23 -0
- package/server/text-requester.js +231 -0
- package/server/token-auth.js +9 -0
- package/server/whole-model-requester.js +99 -0
- package/shared/config/chat-models.json +4685 -0
- package/shared/config/doppler-execution-contracts.js +32 -0
- package/shared/config/doppler-execution-contracts.json +43 -0
- package/shared/config/doppler-generation-contract.js +223 -0
- package/shared/config/doppler-local-models.js +79 -0
- package/shared/config/doppler-package.json +7 -0
- package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
- package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
- package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
- package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
- package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
- package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
- package/shared/config/reploid-library.json +49 -0
- package/shared/config/swarm-bootstrap.json +21 -0
- package/shared/infrastructure/doppler-runtime-service.js +261 -0
- package/shared/pool/executable-pack.js +87 -0
- package/shared/pool/pack-operation-adapters.js +124 -0
- package/shared/pool/pack-operation-policy.js +75 -0
- package/shared/pool/pool-config.json +2280 -0
- package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
- package/shared/vendor/reploid/artifacts/binary.js +25 -0
- package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
- package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
- package/shared/vendor/reploid/artifacts/identity.js +91 -0
- package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
- package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
- package/shared/vendor/reploid/artifacts/signatures.js +94 -0
- package/shared/vendor/reploid/config/defaults.json +110 -0
- package/shared/vendor/reploid/config/index.js +209 -0
- package/shared/vendor/reploid/config/request-overrides.json +9 -0
- package/shared/vendor/reploid/hashing.js +30 -0
- package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
- package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
- package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
- package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
- package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
- package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
- package/shared/vendor/reploid/rules/routing.rules.json +24 -0
- package/shared/vendor/reploid/transport/partition-errors.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { hashJson } from '../artifacts/signatures.js';
|
|
2
|
+
import { computeManifestShardSetHash } from '../artifacts/manifest-identity.js';
|
|
3
|
+
|
|
4
|
+
// The one place application adapters call Doppler's public session surface:
|
|
5
|
+
// module loading, runtime exports, identity, generation, embedding, sequence
|
|
6
|
+
// encoding, operation streams, reset and cancellation. Doppler owns computation;
|
|
7
|
+
// hosts own module URLs, load policy, scopes, work queues, event verification
|
|
8
|
+
// policy and closing. Importing this module has no side effects.
|
|
9
|
+
|
|
10
|
+
const isObject = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
const hasIdentityValue = value => value !== undefined && value !== null && String(value).trim() !== '';
|
|
12
|
+
const normalizeHashIdentity = value => {
|
|
13
|
+
const text = String(value || '').trim();
|
|
14
|
+
if (!text) return '';
|
|
15
|
+
return text.startsWith('sha256:') ? text : `sha256:${text}`;
|
|
16
|
+
};
|
|
17
|
+
const identityValuesMatch = (field, expected, actual) => field === 'modelHash' || field === 'manifestHash'
|
|
18
|
+
? normalizeHashIdentity(expected) === normalizeHashIdentity(actual)
|
|
19
|
+
: expected === actual;
|
|
20
|
+
|
|
21
|
+
/** Doppler publishes one runtime object as `dr`, `doppler` and the default export. */
|
|
22
|
+
export const resolveDopplerRuntime = module => module?.dr || module?.doppler || module?.default || null;
|
|
23
|
+
|
|
24
|
+
/** Memoized module loading; a failed load is forgotten so the next call retries.
|
|
25
|
+
* `loadModule` is one host loader whose own error surfaces unchanged. Otherwise
|
|
26
|
+
* host-chosen `candidates` are tried in order: objects are already-loaded modules
|
|
27
|
+
* and `importModule` imports a specifier, so the host keeps its declared loader. */
|
|
28
|
+
export function createDopplerModuleLoader({ loadModule, candidates, importModule, prepare = () => {}, fallback = () => null,
|
|
29
|
+
injectedLabel = 'injected Doppler module', unavailableMessage = 'Unable to load public Doppler module.' }) {
|
|
30
|
+
const loadCandidates = async () => {
|
|
31
|
+
prepare();
|
|
32
|
+
const errors = [];
|
|
33
|
+
for (const candidate of candidates()) {
|
|
34
|
+
const label = typeof candidate === 'string' ? candidate : injectedLabel;
|
|
35
|
+
try {
|
|
36
|
+
return candidate && typeof candidate === 'object' ? candidate : await importModule(candidate);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
errors.push(`${label}: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const loaded = fallback();
|
|
42
|
+
if (loaded && typeof loaded === 'object') return loaded;
|
|
43
|
+
throw new Error(`${unavailableMessage} Attempts: ${errors.join('; ') || 'none'}`);
|
|
44
|
+
};
|
|
45
|
+
let pending = null;
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
load() {
|
|
48
|
+
pending ||= (loadModule ? Promise.resolve().then(() => loadModule()) : loadCandidates()).catch(error => {
|
|
49
|
+
pending = null;
|
|
50
|
+
throw error;
|
|
51
|
+
});
|
|
52
|
+
return pending;
|
|
53
|
+
},
|
|
54
|
+
reset() { pending = null; }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function dopplerGenerateMethod(session) {
|
|
59
|
+
if (!session) return null;
|
|
60
|
+
for (const name of ['generateWithEvidence', 'generate', 'generateText', 'run']) {
|
|
61
|
+
if (typeof session[name] === 'function') return name;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function dopplerEmbedMethod(session) {
|
|
67
|
+
if (!session) return null;
|
|
68
|
+
for (const name of ['embed', 'embedText', 'run']) {
|
|
69
|
+
if (typeof session[name] === 'function') return name;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Identity a loaded public session reports about itself; never the request's claim. */
|
|
75
|
+
export async function readDopplerSessionIdentity(session) {
|
|
76
|
+
const manifest = session?.manifest || session?.model?.manifest || null;
|
|
77
|
+
const artifactIdentity = manifest?.artifactIdentity || session?.artifactIdentity || session?.model?.artifactIdentity || null;
|
|
78
|
+
const modelHash = manifest?.modelHash
|
|
79
|
+
|| manifest?.artifactIdentity?.weightPackHash
|
|
80
|
+
|| manifest?.artifactIdentity?.shardSetHash
|
|
81
|
+
|| (manifest ? await computeManifestShardSetHash(manifest) : null)
|
|
82
|
+
|| session?.modelHash
|
|
83
|
+
|| session?.hash
|
|
84
|
+
|| session?.model?.modelHash
|
|
85
|
+
|| session?.model?.hash
|
|
86
|
+
|| (artifactIdentity ? await hashJson(artifactIdentity) : null);
|
|
87
|
+
const declaredManifestHash = manifest?.manifestHash || manifest?.hash || null;
|
|
88
|
+
const manifestHash = declaredManifestHash
|
|
89
|
+
|| session?.manifestHash
|
|
90
|
+
|| session?.model?.manifestHash
|
|
91
|
+
|| session?.model?.manifest?.hash
|
|
92
|
+
|| null;
|
|
93
|
+
return {
|
|
94
|
+
modelId: manifest?.modelId || manifest?.id || session?.modelId || session?.id
|
|
95
|
+
|| session?.model?.modelId || session?.model?.id || null,
|
|
96
|
+
modelHash,
|
|
97
|
+
manifestHash,
|
|
98
|
+
runtimeManifestHash: manifest ? await hashJson(manifest) : null,
|
|
99
|
+
declaredManifestHash,
|
|
100
|
+
artifactIdentity
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const artifactIdentityValueMatches = (expected, actual) => {
|
|
105
|
+
if (expected === undefined) return true;
|
|
106
|
+
if (Array.isArray(expected)) {
|
|
107
|
+
return Array.isArray(actual) && expected.length === actual.length
|
|
108
|
+
&& expected.every((value, index) => artifactIdentityValueMatches(value, actual[index]));
|
|
109
|
+
}
|
|
110
|
+
if (isObject(expected)) {
|
|
111
|
+
return isObject(actual) && Object.entries(expected).every(([key, value]) => artifactIdentityValueMatches(value, actual[key]));
|
|
112
|
+
}
|
|
113
|
+
return expected === actual;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Reject a session whose reported identity differs from the requested descriptor.
|
|
117
|
+
* A signed session passes `packIdentity` (its contract's session identity record),
|
|
118
|
+
* whose semantic root and envelope digest replace the manifest-derived hashes. */
|
|
119
|
+
export async function assertDopplerSessionIdentity(session, descriptor = {}, options = {}) {
|
|
120
|
+
const evidence = await readDopplerSessionIdentity(session);
|
|
121
|
+
if (Object.hasOwn(options, 'packIdentity')) {
|
|
122
|
+
evidence.modelHash = options.packIdentity.semanticRoot;
|
|
123
|
+
evidence.manifestHash = options.packIdentity.envelopeDigest;
|
|
124
|
+
evidence.declaredManifestHash = options.packIdentity.envelopeDigest;
|
|
125
|
+
}
|
|
126
|
+
const assertField = (field, expected, actual) => {
|
|
127
|
+
if (!hasIdentityValue(expected)) return;
|
|
128
|
+
if (!hasIdentityValue(actual)) throw new Error(`Loaded Doppler handle must expose ${field} before provider registration`);
|
|
129
|
+
if (!identityValuesMatch(field, expected, actual)) {
|
|
130
|
+
throw new Error(`Loaded Doppler handle ${field} does not match requested model identity`);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
assertField('modelId', descriptor?.modelId || descriptor?.id, evidence.modelId);
|
|
134
|
+
assertField('modelHash', descriptor?.modelHash || descriptor?.hash, evidence.modelHash);
|
|
135
|
+
if (hasIdentityValue(descriptor?.manifestHash)) {
|
|
136
|
+
if (hasIdentityValue(evidence.declaredManifestHash)) {
|
|
137
|
+
assertField('manifestHash', descriptor.manifestHash, evidence.declaredManifestHash);
|
|
138
|
+
} else {
|
|
139
|
+
const observed = [evidence.manifestHash, evidence.runtimeManifestHash].filter(hasIdentityValue);
|
|
140
|
+
if (observed.length === 0 && !evidence.artifactIdentity) {
|
|
141
|
+
throw new Error('Loaded Doppler handle must expose manifestHash or artifactIdentity before provider registration');
|
|
142
|
+
}
|
|
143
|
+
const canonicalHashMatches = observed.some(hash => identityValuesMatch('manifestHash', descriptor.manifestHash, hash));
|
|
144
|
+
// A preflight-verified byte hash may name the manifest Doppler later normalized.
|
|
145
|
+
const byteHashMatches = hasIdentityValue(descriptor.manifestByteHash) && hasIdentityValue(evidence.manifestHash)
|
|
146
|
+
&& identityValuesMatch('manifestHash', descriptor.manifestByteHash, evidence.manifestHash);
|
|
147
|
+
if (observed.length > 0 && !canonicalHashMatches && !byteHashMatches) {
|
|
148
|
+
throw new Error('Loaded Doppler handle manifestHash does not match requested model identity');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (descriptor?.artifactIdentity && !(isObject(evidence.artifactIdentity) && Object.entries(descriptor.artifactIdentity)
|
|
153
|
+
.every(([key, value]) => artifactIdentityValueMatches(value, evidence.artifactIdentity[key])))) {
|
|
154
|
+
throw new Error('Loaded Doppler handle artifactIdentity does not match requested model identity');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const positiveIntegerOrUndefined = value => {
|
|
159
|
+
const number = Number(value);
|
|
160
|
+
return Number.isInteger(number) && number > 0 ? number : undefined;
|
|
161
|
+
};
|
|
162
|
+
const finiteNumberOrUndefined = value => {
|
|
163
|
+
const number = Number(value);
|
|
164
|
+
return Number.isFinite(number) ? number : undefined;
|
|
165
|
+
};
|
|
166
|
+
const stopList = (value, mapItem) => {
|
|
167
|
+
if (!Array.isArray(value)) return undefined;
|
|
168
|
+
const list = value.map(mapItem).filter(item => item != null);
|
|
169
|
+
return list.length ? list : undefined;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** Translate a Reploid generation configuration into Doppler's public sampling
|
|
173
|
+
* options. Doppler validates the values and owns their meaning. */
|
|
174
|
+
export function toDopplerSamplingOptions(generationConfig = {}) {
|
|
175
|
+
const options = {
|
|
176
|
+
maxTokens: positiveIntegerOrUndefined(generationConfig.maxTokens ?? generationConfig.maxOutputTokens),
|
|
177
|
+
temperature: finiteNumberOrUndefined(generationConfig.temperature),
|
|
178
|
+
topP: finiteNumberOrUndefined(generationConfig.topP),
|
|
179
|
+
topK: positiveIntegerOrUndefined(generationConfig.topK),
|
|
180
|
+
useSpeculative: typeof generationConfig.useSpeculative === 'boolean' ? generationConfig.useSpeculative : false,
|
|
181
|
+
stopTokens: stopList(generationConfig.stopTokens, token => Number.isInteger(token) ? token : null),
|
|
182
|
+
stopSequences: stopList(generationConfig.stopSequences, sequence => typeof sequence === 'string' ? sequence : null)
|
|
183
|
+
};
|
|
184
|
+
if (generationConfig.mode === 'greedy') Object.assign(options, { temperature: 0, topK: 1, topP: 1 });
|
|
185
|
+
if (typeof generationConfig.useChatTemplate === 'boolean') options.useChatTemplate = generationConfig.useChatTemplate;
|
|
186
|
+
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const isAsyncIterable = value => value && typeof value[Symbol.asyncIterator] === 'function';
|
|
190
|
+
const isIterable = value => value && typeof value[Symbol.iterator] === 'function' && typeof value !== 'string';
|
|
191
|
+
const collected = (chunks, stream) => {
|
|
192
|
+
const outputText = chunks.join('');
|
|
193
|
+
return { outputText, tokenIds: [], transcript: { chunks, outputText, tokenIds: [], stream } };
|
|
194
|
+
};
|
|
195
|
+
async function collectGeneration(value) {
|
|
196
|
+
if (isAsyncIterable(value)) {
|
|
197
|
+
const chunks = [];
|
|
198
|
+
for await (const chunk of value) chunks.push(String(chunk ?? ''));
|
|
199
|
+
return collected(chunks, true);
|
|
200
|
+
}
|
|
201
|
+
if (isIterable(value)) return collected(Array.from(value, chunk => String(chunk ?? '')), false);
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function firstSuccessful(attempts) {
|
|
206
|
+
let firstError = null;
|
|
207
|
+
for (const attempt of attempts) {
|
|
208
|
+
try {
|
|
209
|
+
return await collectGeneration(await attempt());
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (!firstError) firstError = error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw firstError || new Error('Doppler generation failed without an error');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Generate through whichever public method the session exposes. Prompt-and-options
|
|
218
|
+
* calls precede the request-object form older handles accept. */
|
|
219
|
+
export async function generateWithDopplerSession(session, prompt, generationConfig, assignment) {
|
|
220
|
+
const samplingOptions = toDopplerSamplingOptions(generationConfig);
|
|
221
|
+
const requestOptions = toDopplerSamplingOptions(generationConfig);
|
|
222
|
+
const request = { prompt, assignment, generationConfig, samplingOptions: requestOptions, ...requestOptions };
|
|
223
|
+
if (typeof session.generateWithEvidence === 'function') {
|
|
224
|
+
return collectGeneration(await session.generateWithEvidence(prompt, samplingOptions));
|
|
225
|
+
}
|
|
226
|
+
for (const method of ['generateText', 'generate']) {
|
|
227
|
+
if (typeof session[method] === 'function') {
|
|
228
|
+
return firstSuccessful([() => session[method](prompt, samplingOptions), () => session[method](request)]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (typeof session.run === 'function') return collectGeneration(await session.run(request));
|
|
232
|
+
throw new Error('Doppler public handle does not expose generate, generateText, or run');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function embedWithDopplerSession(session, prompt, assignment, { workload } = {}) {
|
|
236
|
+
if (typeof session.embed === 'function') return session.embed(prompt, { assignment });
|
|
237
|
+
if (typeof session.embedText === 'function') return session.embedText(prompt, { assignment });
|
|
238
|
+
if (typeof session.run === 'function') return session.run({ prompt, input: prompt, assignment, workload });
|
|
239
|
+
throw new Error('Doppler public handle does not expose embed, embedText, or run');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const dopplerSequenceMethod = session => session && typeof session.encodeSequence === 'function' ? 'encodeSequence' : null;
|
|
243
|
+
|
|
244
|
+
/** Biological-sequence encoding; the host chooses the outputs it requests. */
|
|
245
|
+
export async function encodeSequenceWithDopplerSession(session, sequence, options) {
|
|
246
|
+
if (!dopplerSequenceMethod(session)) throw new Error('Doppler public handle does not expose encodeSequence');
|
|
247
|
+
return session.encodeSequence(sequence, options);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The one call into a session's operation event stream. Callers verify each event. */
|
|
251
|
+
export function dopplerOperationEvents(session, request, options) {
|
|
252
|
+
if (typeof session?.executeOperation !== 'function') throw new Error('Doppler session does not expose public executeOperation');
|
|
253
|
+
return session.executeOperation(request, options);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Doppler's own reconstruction of an incremental (operation request v2) stream. */
|
|
257
|
+
export function createDopplerStreamAccumulator(runtime, request) {
|
|
258
|
+
if (typeof runtime?.createCapsuleStreamAccumulator !== 'function') {
|
|
259
|
+
throw new Error('Installed Doppler runtime does not support operation v2');
|
|
260
|
+
}
|
|
261
|
+
return runtime.createCapsuleStreamAccumulator(request);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Text deltas from a scoped session stream; completion events carry no new text. */
|
|
265
|
+
export async function* streamDopplerText(session, input, options) {
|
|
266
|
+
for await (const event of session.stream(input, options)) {
|
|
267
|
+
if (event?.type === 'text-delta') yield event.text;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function readDopplerOutputText(result) {
|
|
272
|
+
if (typeof result === 'string') return result;
|
|
273
|
+
const direct = result?.outputText ?? result?.text ?? result?.content ?? result?.completion ?? result?.response;
|
|
274
|
+
if (typeof direct === 'string') return direct;
|
|
275
|
+
const first = result?.output?.[0]?.content?.[0]?.text ?? result?.choices?.[0]?.message?.content ?? result?.choices?.[0]?.text;
|
|
276
|
+
return typeof first === 'string' ? first : '';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function readDopplerTokenIds(result) {
|
|
280
|
+
const raw = [result?.tokenIds, result?.tokens, result?.generatedTokenIds, result?.outputTokenIds,
|
|
281
|
+
result?.receipt?.tokenIds, result?.receipt?.generatedTokenIds, result?.usage?.tokenIds].find(Array.isArray);
|
|
282
|
+
if (!raw) return [];
|
|
283
|
+
return raw.map(token => {
|
|
284
|
+
if (Number.isInteger(token)) return token;
|
|
285
|
+
if (Number.isInteger(token?.id)) return token.id;
|
|
286
|
+
if (Number.isInteger(token?.tokenId)) return token.tokenId;
|
|
287
|
+
return null;
|
|
288
|
+
}).filter(Number.isInteger);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function readDopplerTokenCounts(result, tokenIds) {
|
|
292
|
+
const counts = result?.tokenCounts || result?.usage || result?.receipt?.tokenCounts;
|
|
293
|
+
if (!counts) return { input: 0, output: tokenIds.length };
|
|
294
|
+
return {
|
|
295
|
+
input: Number(counts.input ?? counts.promptTokens ?? counts.prompt_tokens ?? 0),
|
|
296
|
+
output: Number(counts.output ?? counts.completionTokens ?? counts.completion_tokens ?? tokenIds.length)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Embedding values as float32-rounded numbers; finiteness is the caller's check. */
|
|
301
|
+
export function readDopplerEmbedding(result) {
|
|
302
|
+
const candidate = ArrayBuffer.isView(result) || Array.isArray(result) ? result
|
|
303
|
+
: result?.embedding || result?.vector || result?.values || result?.outputEmbedding
|
|
304
|
+
|| result?.result?.embedding || result?.receipt?.embedding;
|
|
305
|
+
if (!(ArrayBuffer.isView(candidate) || Array.isArray(candidate))) return [];
|
|
306
|
+
return Array.from(candidate, item => Math.fround(Number(item)));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const DOPPLER_GENERATION_EVIDENCE_SCHEMA = 'doppler_generation_evidence/v1';
|
|
310
|
+
const field = (name, expected, actual) => ({ field: name, expected, actual, matched: expected === actual });
|
|
311
|
+
|
|
312
|
+
/** Check Doppler's own generation evidence against the delivered output, the requested
|
|
313
|
+
* options and the loaded model. Null when the session supplied no evidence. */
|
|
314
|
+
export async function verifyDopplerGenerationEvidence({ result, generationConfig, modelId, manifestHash }) {
|
|
315
|
+
if (result?.schema !== DOPPLER_GENERATION_EVIDENCE_SCHEMA) return null;
|
|
316
|
+
if (!isObject(result.transcript) || !isObject(result.generationConfig)) {
|
|
317
|
+
throw new Error('Doppler generation evidence is missing transcript or generationConfig');
|
|
318
|
+
}
|
|
319
|
+
if (!isObject(result.runtimeProfile) || !isObject(result.backendIdentity)) {
|
|
320
|
+
throw new Error('Doppler generation evidence is missing runtimeProfile or backendIdentity');
|
|
321
|
+
}
|
|
322
|
+
const configFields = await Promise.all(Object.entries(toDopplerSamplingOptions(generationConfig))
|
|
323
|
+
.filter(([name]) => name !== 'stopTokens')
|
|
324
|
+
.map(async ([name, expected]) => field(`generationConfig.${name}`,
|
|
325
|
+
await hashJson(expected), await hashJson(result.generationConfig[name]))));
|
|
326
|
+
const fields = [
|
|
327
|
+
field('outputText', readDopplerOutputText(result), result.transcript.outputText),
|
|
328
|
+
field('tokenIdsHash', await hashJson(readDopplerTokenIds(result)), await hashJson(result.transcript.tokenIds)),
|
|
329
|
+
field('transcriptHash', await hashJson(result.transcript), result.transcriptHash),
|
|
330
|
+
field('generationConfigHash', await hashJson(result.generationConfig), result.generationConfigHash),
|
|
331
|
+
field('runtimeProfileHash', await hashJson(result.runtimeProfile), result.runtimeProfileHash),
|
|
332
|
+
field('backendIdentityHash', await hashJson(result.backendIdentity), result.backendIdentityHash),
|
|
333
|
+
field('modelId', modelId || null, result.runtimeProfile?.model?.modelId || null),
|
|
334
|
+
field('manifestHash', normalizeHashIdentity(manifestHash), normalizeHashIdentity(result.runtimeProfile?.model?.manifestHash)),
|
|
335
|
+
field('backend', 'webgpu', result.backendIdentity?.backend || null),
|
|
336
|
+
...configFields
|
|
337
|
+
];
|
|
338
|
+
const mismatch = fields.find(item => item.matched !== true);
|
|
339
|
+
if (mismatch) {
|
|
340
|
+
throw new Error(`Doppler generation evidence ${mismatch.field} mismatch: expected ${mismatch.expected}, got ${mismatch.actual}`);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
schema: 'reploid.doppler_generation_evidence_comparison/v1',
|
|
344
|
+
verified: true,
|
|
345
|
+
evidenceSchema: result.schema,
|
|
346
|
+
transcriptHash: result.transcriptHash,
|
|
347
|
+
generationConfigHash: result.generationConfigHash,
|
|
348
|
+
runtimeProfileHash: result.runtimeProfileHash,
|
|
349
|
+
backendIdentityHash: result.backendIdentityHash,
|
|
350
|
+
fields
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Clear decoder state between independent requests on a resident session. */
|
|
355
|
+
export async function resetDopplerSession(session) {
|
|
356
|
+
if (typeof session?.resetGenerationState === 'function') {
|
|
357
|
+
await session.resetGenerationState();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (typeof session?.resetToSeqLen === 'function') await session.resetToSeqLen(0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Ask the session to stop. A request is not a confirmation that GPU work ended. */
|
|
364
|
+
export async function cancelDopplerSession(session, reason) {
|
|
365
|
+
if (!session) return { requested: false, method: null };
|
|
366
|
+
for (const method of ['abort', 'cancel']) {
|
|
367
|
+
if (typeof session[method] !== 'function') continue;
|
|
368
|
+
await session[method]({ reason });
|
|
369
|
+
return { requested: true, method };
|
|
370
|
+
}
|
|
371
|
+
return { requested: false, method: null };
|
|
372
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Binary encodings shared by signed artifacts and host transport adapters. */
|
|
2
|
+
export function bytesToBase64(value) {
|
|
3
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
4
|
+
if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64');
|
|
5
|
+
let binary = '';
|
|
6
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
7
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function base64ToBytes(value) {
|
|
13
|
+
const text = String(value || '');
|
|
14
|
+
if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(text, 'base64'));
|
|
15
|
+
return Uint8Array.from(atob(text), character => character.charCodeAt(0));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toBase64Url(value) {
|
|
19
|
+
return bytesToBase64(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fromBase64Url(value) {
|
|
23
|
+
const text = String(value || '').replace(/-/g, '+').replace(/_/g, '/');
|
|
24
|
+
return base64ToBytes(text.padEnd(Math.ceil(text.length / 4) * 4, '='));
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Canonical JSON serialization shared across Poolday runtimes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Canonical JSON whose object keys follow `compare`; without one, keys sort by
|
|
7
|
+
* UTF-16 code units, as Array.prototype.sort does.
|
|
8
|
+
*/
|
|
9
|
+
export function canonicalizeWith(compare) {
|
|
10
|
+
const serialize = (value) => {
|
|
11
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return `[${value.map((item) => serialize(item)).join(',')}]`;
|
|
14
|
+
}
|
|
15
|
+
const keys = Object.keys(value).sort(compare);
|
|
16
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${serialize(value[key])}`).join(',')}}`;
|
|
17
|
+
};
|
|
18
|
+
return serialize;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const canonicalize = canonicalizeWith();
|
|
22
|
+
|
|
23
|
+
export default canonicalize;
|
|
@@ -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,91 @@
|
|
|
1
|
+
import { toBase64Url, fromBase64Url } from './binary.js';
|
|
2
|
+
import { canonicalize as stableJson } from './canonical-json.js';
|
|
3
|
+
import { sha256HexUnprefixed } from './signatures.js';
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const getCryptoApi = (cryptoApi) => {
|
|
8
|
+
const api = cryptoApi || globalThis.crypto;
|
|
9
|
+
if (!api?.subtle) {
|
|
10
|
+
throw new Error('WebCrypto unavailable');
|
|
11
|
+
}
|
|
12
|
+
return api;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function inferIdentityAlgorithm(bundleOrJwk = {}) {
|
|
17
|
+
const algorithm = String(bundleOrJwk?.algorithm || '').trim();
|
|
18
|
+
const crv = String(bundleOrJwk?.crv || bundleOrJwk?.publicJwk?.crv || '').trim();
|
|
19
|
+
if (algorithm === 'Ed25519' || crv === 'Ed25519') {
|
|
20
|
+
return 'Ed25519';
|
|
21
|
+
}
|
|
22
|
+
return 'ECDSA';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getIdentityImportAlgorithm(bundleOrJwk = {}) {
|
|
26
|
+
return inferIdentityAlgorithm(bundleOrJwk) === 'Ed25519'
|
|
27
|
+
? { name: 'Ed25519' }
|
|
28
|
+
: { name: 'ECDSA', namedCurve: 'P-256' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getIdentitySignAlgorithm(bundleOrJwk = {}) {
|
|
32
|
+
return inferIdentityAlgorithm(bundleOrJwk) === 'Ed25519'
|
|
33
|
+
? { name: 'Ed25519' }
|
|
34
|
+
: { name: 'ECDSA', hash: 'SHA-256' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function createPeerIdFromPublicJwk(publicJwk, cryptoApi = globalThis.crypto) {
|
|
38
|
+
const publicShape = publicJwk ? {
|
|
39
|
+
kty: publicJwk.kty || null,
|
|
40
|
+
crv: publicJwk.crv || null,
|
|
41
|
+
x: publicJwk.x || null,
|
|
42
|
+
y: publicJwk.y || null,
|
|
43
|
+
n: publicJwk.n || null,
|
|
44
|
+
e: publicJwk.e || null
|
|
45
|
+
} : {};
|
|
46
|
+
const digest = await sha256HexUnprefixed(stableJson(publicShape), getCryptoApi(cryptoApi));
|
|
47
|
+
return `peer:${digest.slice(0, 24)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function importSigningKey(bundle, cryptoApi = globalThis.crypto) {
|
|
51
|
+
if (!bundle?.privateJwk) {
|
|
52
|
+
throw new Error('Missing private JWK');
|
|
53
|
+
}
|
|
54
|
+
return getCryptoApi(cryptoApi).subtle.importKey(
|
|
55
|
+
'jwk',
|
|
56
|
+
bundle.privateJwk,
|
|
57
|
+
getIdentityImportAlgorithm(bundle),
|
|
58
|
+
false,
|
|
59
|
+
['sign']
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function importVerificationKey(bundleOrJwk, cryptoApi = globalThis.crypto) {
|
|
64
|
+
const publicJwk = bundleOrJwk?.publicJwk || bundleOrJwk;
|
|
65
|
+
if (!publicJwk) {
|
|
66
|
+
throw new Error('Missing public JWK');
|
|
67
|
+
}
|
|
68
|
+
return getCryptoApi(cryptoApi).subtle.importKey(
|
|
69
|
+
'jwk',
|
|
70
|
+
publicJwk,
|
|
71
|
+
getIdentityImportAlgorithm(publicJwk),
|
|
72
|
+
false,
|
|
73
|
+
['verify']
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function encodeBytes(value) {
|
|
78
|
+
return encoder.encode(String(value || ''));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { fromBase64Url, toBase64Url };
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
export async function createSigningIdentity({ algorithm, cryptoApi = globalThis.crypto }) {
|
|
85
|
+
if (!['Ed25519', 'ECDSA'].includes(algorithm)) throw new TypeError('Explicit supported signing algorithm required');
|
|
86
|
+
const params = algorithm === 'Ed25519' ? { name: algorithm } : { name: algorithm, namedCurve: 'P-256' };
|
|
87
|
+
const keys = await cryptoApi.subtle.generateKey(params, true, ['sign', 'verify']);
|
|
88
|
+
const publicJwk = await cryptoApi.subtle.exportKey('jwk', keys.publicKey);
|
|
89
|
+
const privateJwk = await cryptoApi.subtle.exportKey('jwk', keys.privateKey);
|
|
90
|
+
return { version: 1, algorithm, peerId: await createPeerIdFromPublicJwk(publicJwk, cryptoApi), publicJwk, privateJwk };
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sha256Hex } from './signatures.js';
|
|
2
|
+
|
|
3
|
+
export const computeManifestShardSetHash = async (manifest = {}) => {
|
|
4
|
+
const shards = Array.isArray(manifest?.shards) ? manifest.shards : [];
|
|
5
|
+
if (shards.length === 0) return null;
|
|
6
|
+
const shardHashInput = shards
|
|
7
|
+
.map((shard) => {
|
|
8
|
+
const filename = typeof shard?.filename === 'string' ? shard.filename : '';
|
|
9
|
+
const size = Number.isFinite(Number(shard?.size)) ? Number(shard.size) : '';
|
|
10
|
+
const hash = typeof shard?.hash === 'string'
|
|
11
|
+
? shard.hash
|
|
12
|
+
: (typeof shard?.blake3 === 'string' ? shard.blake3 : '');
|
|
13
|
+
return `${filename}:${size}:${hash}`;
|
|
14
|
+
})
|
|
15
|
+
.join('\n');
|
|
16
|
+
return sha256Hex(shardHashInput);
|
|
17
|
+
};
|