reploid 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +534 -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 +83 -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,277 @@
|
|
|
1
|
+
import { createPeerIdFromPublicJwk, importSigningKey, importVerificationKey, getIdentitySignAlgorithm,
|
|
2
|
+
toBase64Url, fromBase64Url } from '../../artifacts/identity.js';
|
|
3
|
+
import { assertPartition as assert, canonicalPartitionJson, validatePartitionIdentity,
|
|
4
|
+
samePartitionIdentity, partitionActions, partitionIdentityKeys, canonicalRouteAttemptBinding, sameRouteAttemptIdentity } from './partition-contract.js';
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'reploid.partition-grant/v2';
|
|
7
|
+
const CANONICAL_SCHEMA = 'reploid.partition-grant/v3';
|
|
8
|
+
const ORDERED_SCHEMA = 'reploid.partition-grant/v4';
|
|
9
|
+
const bytes = value => new TextEncoder().encode(canonicalPartitionJson(value));
|
|
10
|
+
const bounds = ['maxTokens', 'maxPromptTokens', 'maxActivationBytes', 'maxOutputCharacters'];
|
|
11
|
+
|
|
12
|
+
/** Explicit disclosure closure for the ordinary stage-zero coordinator path. */
|
|
13
|
+
export function orderedPartitionDisclosure(identity) {
|
|
14
|
+
const binding = canonicalRouteAttemptBinding(identity), stages = binding.route.stages;
|
|
15
|
+
const coordinator = stages[0].participantId, unique = values => [...new Set(values)];
|
|
16
|
+
return { kind: 'coordinator-mediated-activations-and-tokens',
|
|
17
|
+
activationPaths: stages.slice(1).map((stage, index) => unique([stages[index].participantId, coordinator, stage.participantId])),
|
|
18
|
+
tokenRecipients: stages.map(stage => stage.participantId),
|
|
19
|
+
outputRecipients: unique([stages.at(-1).participantId, coordinator, binding.requesterId]) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The host calls issue only after disclosure approval. B separately consents by preparing its resident. */
|
|
23
|
+
export function createPartitionGrantAuthority({ identity, meshId, maxGrants, maxTtlMs, maxClockSkewMs = 0, now = Date.now, canonicalRuntime }) {
|
|
24
|
+
assert(identity?.privateJwk && identity?.publicJwk && typeof meshId === 'string' && meshId
|
|
25
|
+
&& Number.isSafeInteger(maxGrants) && maxGrants > 0 && Number.isSafeInteger(maxTtlMs) && maxTtlMs > 0
|
|
26
|
+
&& Number.isSafeInteger(maxClockSkewMs) && maxClockSkewMs >= 0,
|
|
27
|
+
'Explicit partition signing identity, mesh and grant budgets required');
|
|
28
|
+
const signer = structuredClone(identity);
|
|
29
|
+
const issued = new Map();
|
|
30
|
+
let closed = false;
|
|
31
|
+
// Signatures already verified, keyed by signature and bound to the exact canonical grant record.
|
|
32
|
+
// Later checks of the same grant skip only the key derivation, import and signature verification;
|
|
33
|
+
// binding, scope, expiry and revocation are evaluated on every call. Bounded like issuance.
|
|
34
|
+
const verified = new Map();
|
|
35
|
+
const OK = Object.freeze({ ok: true, reason: null });
|
|
36
|
+
const refused = reason => ({ ok: false, reason });
|
|
37
|
+
const DECLINED = Object.freeze(refused('declined'));
|
|
38
|
+
async function signatureValid(record, signerId, signed) {
|
|
39
|
+
const json = canonicalPartitionJson(record);
|
|
40
|
+
const known = verified.get(record.signature);
|
|
41
|
+
if (known?.json === json && known.signerId === signerId) return true;
|
|
42
|
+
if (await createPeerIdFromPublicJwk(record.publicJwk) !== signerId) return false;
|
|
43
|
+
const key = await importVerificationKey(record.publicJwk);
|
|
44
|
+
if (!await crypto.subtle.verify(getIdentitySignAlgorithm(record.publicJwk), key, fromBase64Url(record.signature), signed)) return false;
|
|
45
|
+
const at = now();
|
|
46
|
+
for (const [signature, entry] of verified) if (entry.expiresAt <= at) verified.delete(signature);
|
|
47
|
+
while (verified.size >= maxGrants) verified.delete(verified.keys().next().value);
|
|
48
|
+
verified.set(record.signature, { json, signerId, expiresAt: record.claim.expiresAt });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Expiry and revocation are re-evaluated after the asynchronous signature check.
|
|
52
|
+
const current = (claim, settlement) => closed ? DECLINED : settlement ? OK
|
|
53
|
+
: issued.get(claim.id)?.revoked === true ? refused('revoked') : claim.expiresAt <= now() ? refused('expired') : OK;
|
|
54
|
+
const canonicalCapability = () => assert(['metadataDigest', 'canonicalGenerationIdentity', 'validateResidentAttemptIdentity', 'residentAttemptDigest']
|
|
55
|
+
.every(method => typeof canonicalRuntime?.[method] === 'function'), 'Canonical Doppler grant capability required');
|
|
56
|
+
const exact = (value, keys) => assert(value && typeof value === 'object' && !Array.isArray(value)
|
|
57
|
+
&& Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key)), 'Invalid canonical grant fields');
|
|
58
|
+
const canonicalDigest = value => canonicalRuntime.metadataDigest(CANONICAL_SCHEMA, value);
|
|
59
|
+
const canonicalBytes = value => new TextEncoder().encode(canonicalDigest(value));
|
|
60
|
+
const canonicalScope = ({ identity: binding, limits, disclosure, generationId }) => {
|
|
61
|
+
canonicalCapability(); validatePartitionIdentity(binding);
|
|
62
|
+
exact(binding, [...partitionIdentityKeys, 'requesterId', 'placementGeneration']);
|
|
63
|
+
assert(typeof binding.requesterId === 'string' && bounds.every(key => Number.isSafeInteger(limits?.[key]) && limits[key] > 0)
|
|
64
|
+
&& disclosure === 'partition-activations-and-tokens' && /^sha256:[a-f0-9]{64}$/.test(generationId),
|
|
65
|
+
'Explicit canonical route, disclosure and allocation required');
|
|
66
|
+
return canonicalRuntime.metadataDigest('reploid.partition-authorization/v1', { meshId, identity: binding,
|
|
67
|
+
limits: Object.fromEntries(bounds.map(key => [key, limits[key]])), disclosure, generationId });
|
|
68
|
+
};
|
|
69
|
+
const canonicalClaim = claim => {
|
|
70
|
+
canonicalCapability();
|
|
71
|
+
canonicalDigest(claim);
|
|
72
|
+
exact(claim, ['schema', 'id', 'meshId', 'identity', 'canonicalAttempt', 'generation', 'vocabSize',
|
|
73
|
+
'disclosure', 'issuedAt', 'expiresAt', 'limits']);
|
|
74
|
+
exact(claim.limits, bounds); exact(claim.generation, ['policy', 'executionIdentity', 'generationId']);
|
|
75
|
+
assert(claim.schema === CANONICAL_SCHEMA && claim.meshId === meshId, 'Canonical grant scope mismatch');
|
|
76
|
+
const generation = canonicalRuntime.canonicalGenerationIdentity(claim.generation.policy,
|
|
77
|
+
claim.generation.executionIdentity, claim.vocabSize);
|
|
78
|
+
assert(canonicalDigest(generation) === canonicalDigest(claim.generation), 'Canonical generation identity mismatch');
|
|
79
|
+
const attempt = canonicalRuntime.validateResidentAttemptIdentity(claim.canonicalAttempt);
|
|
80
|
+
assert(attempt.attemptId === claim.identity.attemptId && attempt.requesterId === claim.identity.requesterId
|
|
81
|
+
&& attempt.generationId === generation.generationId && attempt.routeKind === 'two-stage-layer'
|
|
82
|
+
&& attempt.stages[0].participantId === claim.identity.participantA
|
|
83
|
+
&& attempt.stages[1].participantId === claim.identity.participantB
|
|
84
|
+
&& attempt.stages.every(stage => stage.executionId === generation.executionIdentity), 'Canonical attempt route binding mismatch');
|
|
85
|
+
assert(attempt.authorizationScope === canonicalScope({ ...claim, generationId: generation.generationId }),
|
|
86
|
+
'Canonical authorization scope mismatch');
|
|
87
|
+
assert(generation.policy.maxGeneratedTokens <= claim.limits.maxTokens
|
|
88
|
+
&& generation.policy.maxRawOutputUtf16 <= claim.limits.maxOutputCharacters
|
|
89
|
+
&& generation.policy.maxVisibleOutputUtf16 <= claim.limits.maxOutputCharacters,
|
|
90
|
+
'Canonical generation exceeds grant allocation');
|
|
91
|
+
assert(typeof claim.id === 'string' && claim.id.length > 0 && claim.id.length <= 128
|
|
92
|
+
&& Number.isSafeInteger(claim.issuedAt) && claim.issuedAt >= 0 && Number.isSafeInteger(claim.expiresAt)
|
|
93
|
+
&& claim.issuedAt <= now() + maxClockSkewMs && claim.expiresAt > claim.issuedAt
|
|
94
|
+
&& claim.expiresAt - claim.issuedAt <= maxTtlMs, 'Invalid canonical grant lifetime');
|
|
95
|
+
return { generation, attempt };
|
|
96
|
+
};
|
|
97
|
+
async function checkCanonical(grant, request, settlement) {
|
|
98
|
+
try {
|
|
99
|
+
if (closed) return DECLINED;
|
|
100
|
+
canonicalCapability(); canonicalDigest({ grant, request });
|
|
101
|
+
const record = structuredClone(grant), input = structuredClone(request), claim = record.claim;
|
|
102
|
+
exact(record, ['claim', 'publicJwk', 'signature']);
|
|
103
|
+
const { generation, attempt } = canonicalClaim(claim);
|
|
104
|
+
const requested = canonicalRuntime.validateResidentAttemptIdentity(input.canonicalAttempt);
|
|
105
|
+
if (!samePartitionIdentity(claim.identity, input.identity)
|
|
106
|
+
|| canonicalRuntime.residentAttemptDigest(attempt) !== canonicalRuntime.residentAttemptDigest(requested)) return DECLINED;
|
|
107
|
+
if (!settlement) {
|
|
108
|
+
if (input.generationId !== generation.generationId || !partitionActions.includes(input.action)
|
|
109
|
+
|| !['admit', 'step'].includes(input.phase) || !Number.isSafeInteger(input.step) || input.step < 0
|
|
110
|
+
|| !Number.isSafeInteger(input.inputTokenCount) || input.inputTokenCount < 1
|
|
111
|
+
|| input.inputTokenCount > (input.step === 0 ? Math.min(claim.limits.maxPromptTokens,
|
|
112
|
+
generation.policy.materializedInputCapacity, generation.policy.totalAcceptedTokenLimit) : 1)
|
|
113
|
+
|| input.phase === 'admit' && (input.step !== 0 || input.activationBytes != null && input.activationBytes !== 0)
|
|
114
|
+
|| input.phase === 'step' && input.step >= generation.policy.maxGeneratedTokens
|
|
115
|
+
|| input.activationBytes != null && (!Number.isSafeInteger(input.activationBytes)
|
|
116
|
+
|| input.activationBytes < 0 || input.activationBytes > claim.limits.maxActivationBytes)) return DECLINED;
|
|
117
|
+
}
|
|
118
|
+
if (!await signatureValid(record, claim.identity.participantA, canonicalBytes(claim))) return DECLINED;
|
|
119
|
+
return current(claim, settlement);
|
|
120
|
+
} catch { return DECLINED; }
|
|
121
|
+
}
|
|
122
|
+
const orderedClaim = claim => {
|
|
123
|
+
exact(claim, ['schema', 'id', 'meshId', 'identity', 'coordinatorId', 'disclosure', 'generationDigest', 'issuedAt', 'expiresAt', 'limits']);
|
|
124
|
+
const binding = canonicalRouteAttemptBinding(claim.identity);
|
|
125
|
+
exact(claim.limits, bounds);
|
|
126
|
+
assert(claim.schema === ORDERED_SCHEMA && claim.meshId === meshId
|
|
127
|
+
&& claim.coordinatorId === binding.route.stages[0].participantId
|
|
128
|
+
&& canonicalPartitionJson(claim.disclosure) === canonicalPartitionJson(orderedPartitionDisclosure(binding)),
|
|
129
|
+
'Explicit ordered route disclosure mismatch');
|
|
130
|
+
assert(typeof claim.id === 'string' && claim.id.length > 0 && claim.id.length <= 128
|
|
131
|
+
&& /^sha256:[a-f0-9]{64}$/.test(claim.generationDigest)
|
|
132
|
+
&& bounds.every(key => Number.isSafeInteger(claim.limits[key]) && claim.limits[key] > 0)
|
|
133
|
+
&& Number.isSafeInteger(claim.issuedAt) && claim.issuedAt >= 0 && Number.isSafeInteger(claim.expiresAt)
|
|
134
|
+
&& claim.issuedAt <= now() + maxClockSkewMs && claim.expiresAt > claim.issuedAt
|
|
135
|
+
&& claim.expiresAt - claim.issuedAt <= maxTtlMs, 'Invalid ordered grant allocation or lifetime');
|
|
136
|
+
return binding;
|
|
137
|
+
};
|
|
138
|
+
async function checkOrdered(grant, request, settlement) {
|
|
139
|
+
try {
|
|
140
|
+
if (closed) return DECLINED;
|
|
141
|
+
const record = structuredClone(grant), claim = record.claim;
|
|
142
|
+
// Host authorization calls also carry borrowed AbortSignals/ports. Snapshot
|
|
143
|
+
// only the signed protocol inputs, before any asynchronous verification.
|
|
144
|
+
const input = structuredClone(Object.fromEntries(['identity', 'coordinatorId', 'action', 'partitionIndex',
|
|
145
|
+
'fromStage', 'toStage', 'recipients', 'step', 'inputTokenCount', 'activationBytes', 'generationDigest']
|
|
146
|
+
.filter(key => Object.hasOwn(request, key)).map(key => [key, request[key]])));
|
|
147
|
+
exact(record, ['claim', 'publicJwk', 'signature']);
|
|
148
|
+
const binding = orderedClaim(claim), stages = binding.route.stages;
|
|
149
|
+
if (!sameRouteAttemptIdentity(binding, input.identity) || input.coordinatorId !== claim.coordinatorId) return DECLINED;
|
|
150
|
+
const validStage = Number.isSafeInteger(input.partitionIndex) && input.partitionIndex >= 0 && input.partitionIndex < stages.length;
|
|
151
|
+
if (settlement) {
|
|
152
|
+
if (input.action !== 'mesh.execute_partition_stage' || !validStage) return DECLINED;
|
|
153
|
+
} else {
|
|
154
|
+
if (input.generationDigest !== claim.generationDigest || !Number.isSafeInteger(input.step)
|
|
155
|
+
|| input.step < 0 || input.step >= claim.limits.maxTokens || !Number.isSafeInteger(input.inputTokenCount)
|
|
156
|
+
|| input.inputTokenCount < 1 || input.inputTokenCount > (input.step === 0 ? claim.limits.maxPromptTokens : 1)
|
|
157
|
+
|| input.activationBytes != null && (!Number.isSafeInteger(input.activationBytes)
|
|
158
|
+
|| input.activationBytes < 0 || input.activationBytes > claim.limits.maxActivationBytes)) return DECLINED;
|
|
159
|
+
let recipients;
|
|
160
|
+
if (input.action === 'mesh.execute_partition_stage') { if (!validStage) return DECLINED; }
|
|
161
|
+
else if (input.action === 'mesh.transfer_token_context') {
|
|
162
|
+
if (!validStage) return DECLINED;
|
|
163
|
+
recipients = [...new Set([claim.coordinatorId, stages[input.partitionIndex].participantId])];
|
|
164
|
+
} else if (input.action === 'mesh.transfer_intermediate_activation') {
|
|
165
|
+
if (!Number.isSafeInteger(input.fromStage) || input.fromStage < 0 || input.toStage !== input.fromStage + 1
|
|
166
|
+
|| input.toStage >= stages.length) return DECLINED;
|
|
167
|
+
recipients = claim.disclosure.activationPaths[input.fromStage];
|
|
168
|
+
} else if (input.action === 'mesh.transfer_partition_output') {
|
|
169
|
+
if (input.partitionIndex !== stages.length - 1) return DECLINED;
|
|
170
|
+
recipients = claim.disclosure.outputRecipients;
|
|
171
|
+
} else return DECLINED;
|
|
172
|
+
if (recipients && canonicalPartitionJson(input.recipients) !== canonicalPartitionJson(recipients)) return DECLINED;
|
|
173
|
+
}
|
|
174
|
+
if (!await signatureValid(record, claim.coordinatorId, bytes(claim))) return DECLINED;
|
|
175
|
+
return current(claim, settlement);
|
|
176
|
+
} catch { return DECLINED; }
|
|
177
|
+
}
|
|
178
|
+
/** `{ ok, reason }`: reason is 'expired', 'revoked' or 'declined' (any other mismatch) when refused. */
|
|
179
|
+
async function check(grant, request, { settlement = false } = {}) {
|
|
180
|
+
if (grant?.claim?.schema === ORDERED_SCHEMA) return checkOrdered(grant, request, settlement);
|
|
181
|
+
if (grant?.claim?.schema === CANONICAL_SCHEMA) return checkCanonical(grant, request, settlement);
|
|
182
|
+
try {
|
|
183
|
+
if (closed) return DECLINED;
|
|
184
|
+
validatePartitionIdentity(request.identity);
|
|
185
|
+
const record = structuredClone(grant), claim = record.claim;
|
|
186
|
+
if (claim?.schema !== SCHEMA || claim.meshId !== meshId || !samePartitionIdentity(claim.identity, request.identity)
|
|
187
|
+
|| !['partition-activations', 'partition-activations-and-tokens'].includes(claim.disclosure)
|
|
188
|
+
|| !/^sha256:[a-f0-9]{64}$/.test(claim.generationDigest)
|
|
189
|
+
|| !Number.isSafeInteger(claim.issuedAt) || !Number.isSafeInteger(claim.expiresAt)
|
|
190
|
+
|| claim.issuedAt > now() + maxClockSkewMs || claim.expiresAt <= claim.issuedAt || claim.expiresAt - claim.issuedAt > maxTtlMs
|
|
191
|
+
|| typeof claim.id !== 'string' || claim.id.length > 128
|
|
192
|
+
|| bounds.some(key => !Number.isSafeInteger(claim.limits?.[key]) || claim.limits[key] <= 0)) return DECLINED;
|
|
193
|
+
if (!settlement && (!partitionActions.includes(request.action) || !Number.isSafeInteger(request.step)
|
|
194
|
+
|| request.generationDigest !== claim.generationDigest
|
|
195
|
+
|| request.action === 'mesh.transfer_token_context' && claim.disclosure !== 'partition-activations-and-tokens'
|
|
196
|
+
|| request.step < 0 || request.step >= claim.limits.maxTokens
|
|
197
|
+
|| !Number.isSafeInteger(request.inputTokenCount) || request.inputTokenCount < 1
|
|
198
|
+
|| request.inputTokenCount > (request.step === 0 ? claim.limits.maxPromptTokens : 1)
|
|
199
|
+
|| request.activationBytes != null && (!Number.isSafeInteger(request.activationBytes)
|
|
200
|
+
|| request.activationBytes < 0 || request.activationBytes > claim.limits.maxActivationBytes))) return DECLINED;
|
|
201
|
+
if (!await signatureValid(record, request.identity.participantA, bytes(claim))) return DECLINED;
|
|
202
|
+
return current(claim, settlement);
|
|
203
|
+
} catch { return DECLINED; }
|
|
204
|
+
}
|
|
205
|
+
const verify = async (grant, request, options) => (await check(grant, request, options)).ok;
|
|
206
|
+
return Object.freeze({
|
|
207
|
+
participantId: signer.peerId, meshId, verify, check,
|
|
208
|
+
/** The declared clock allowance between participants that check this authority's grants. */
|
|
209
|
+
maxClockSkewMs,
|
|
210
|
+
canonicalScope,
|
|
211
|
+
async issueOrdered(identity, limits, { approved, ttlMs, disclosure, generationDigest, expiresAt }) {
|
|
212
|
+
assert(!closed && approved === true, 'Explicit ordered partition disclosure approval required');
|
|
213
|
+
assert(Number.isSafeInteger(ttlMs) && ttlMs > 0 && ttlMs <= maxTtlMs, 'Invalid ordered grant lifetime');
|
|
214
|
+
const binding = canonicalRouteAttemptBinding(identity), issuedAt = now();
|
|
215
|
+
assert(expiresAt === undefined || Number.isSafeInteger(expiresAt) && expiresAt > issuedAt, 'Ordered disclosure deadline expired or invalid');
|
|
216
|
+
const claim = structuredClone({ schema: ORDERED_SCHEMA, id: crypto.randomUUID(), meshId, identity: binding,
|
|
217
|
+
coordinatorId: binding.route.stages[0].participantId, disclosure, generationDigest, issuedAt,
|
|
218
|
+
expiresAt: Math.min(issuedAt + ttlMs, expiresAt ?? Infinity), limits: Object.fromEntries(bounds.map(key => [key, limits[key]])) });
|
|
219
|
+
orderedClaim(claim);
|
|
220
|
+
assert(claim.coordinatorId === signer.peerId && await createPeerIdFromPublicJwk(signer.publicJwk) === signer.peerId,
|
|
221
|
+
'Only the authenticated stage-zero coordinator grants ordered execution');
|
|
222
|
+
assert(!closed && issued.size < maxGrants, 'Partition grant budget exhausted');
|
|
223
|
+
const entry = { revoked: false }; issued.set(claim.id, entry);
|
|
224
|
+
try {
|
|
225
|
+
const key = await importSigningKey(signer);
|
|
226
|
+
const signature = await crypto.subtle.sign(getIdentitySignAlgorithm(signer), key, bytes(claim));
|
|
227
|
+
assert(!closed && !entry.revoked, 'Partition authority closed');
|
|
228
|
+
return { claim, publicJwk: structuredClone(signer.publicJwk), signature: toBase64Url(signature) };
|
|
229
|
+
} catch (error) { entry.revoked = true; throw error; }
|
|
230
|
+
},
|
|
231
|
+
async issueCanonical(binding, limits, { approved, ttlMs, disclosure, generation, canonicalAttempt, vocabSize }) {
|
|
232
|
+
canonicalCapability();
|
|
233
|
+
assert(!closed && approved === true, 'Explicit canonical partition disclosure approval required');
|
|
234
|
+
assert(Number.isSafeInteger(ttlMs) && ttlMs > 0 && ttlMs <= maxTtlMs, 'Invalid canonical grant lifetime');
|
|
235
|
+
const issuedAt = now();
|
|
236
|
+
const claim = structuredClone({ schema: CANONICAL_SCHEMA, id: crypto.randomUUID(), meshId, identity: binding,
|
|
237
|
+
canonicalAttempt, generation, vocabSize, disclosure, issuedAt, expiresAt: issuedAt + ttlMs,
|
|
238
|
+
limits: Object.fromEntries(bounds.map(key => [key, limits[key]])) });
|
|
239
|
+
canonicalClaim(claim);
|
|
240
|
+
assert(claim.identity.participantA === signer.peerId && await createPeerIdFromPublicJwk(signer.publicJwk) === signer.peerId,
|
|
241
|
+
'Only the authenticated input owner grants canonical execution');
|
|
242
|
+
assert(!closed && issued.size < maxGrants, 'Partition grant budget exhausted');
|
|
243
|
+
const entry = { revoked: false }; issued.set(claim.id, entry);
|
|
244
|
+
try {
|
|
245
|
+
const key = await importSigningKey(signer);
|
|
246
|
+
const signature = await crypto.subtle.sign(getIdentitySignAlgorithm(signer), key, canonicalBytes(claim));
|
|
247
|
+
assert(!closed && !entry.revoked, 'Partition authority closed');
|
|
248
|
+
return { claim, publicJwk: structuredClone(signer.publicJwk), signature: toBase64Url(signature) };
|
|
249
|
+
} catch (error) { entry.revoked = true; throw error; }
|
|
250
|
+
},
|
|
251
|
+
async issue(binding, limits, { approved, ttlMs, disclosure, generationDigest }) {
|
|
252
|
+
binding = structuredClone(binding); limits = structuredClone(limits);
|
|
253
|
+
assert(!closed && approved === true, 'Explicit partition disclosure approval required');
|
|
254
|
+
validatePartitionIdentity(binding);
|
|
255
|
+
assert(binding.participantA === signer.peerId && await createPeerIdFromPublicJwk(signer.publicJwk) === signer.peerId,
|
|
256
|
+
'Only the authenticated input owner can grant partition execution');
|
|
257
|
+
assert(Number.isSafeInteger(ttlMs) && ttlMs > 0 && ttlMs <= maxTtlMs, 'Invalid partition grant lifetime');
|
|
258
|
+
assert(['partition-activations', 'partition-activations-and-tokens'].includes(disclosure)
|
|
259
|
+
&& /^sha256:[a-f0-9]{64}$/.test(generationDigest), 'Explicit partition disclosure and generation identity required');
|
|
260
|
+
assert(bounds.every(key => Number.isSafeInteger(limits?.[key]) && limits[key] > 0), 'Explicit partition grant allocations required');
|
|
261
|
+
assert(issued.size < maxGrants, 'Partition grant budget exhausted');
|
|
262
|
+
const issuedAt = now();
|
|
263
|
+
const claim = { schema: SCHEMA, id: crypto.randomUUID(), meshId, identity: binding,
|
|
264
|
+
disclosure, generationDigest, issuedAt, expiresAt: issuedAt + ttlMs,
|
|
265
|
+
limits: Object.fromEntries(bounds.map(key => [key, limits[key]])) };
|
|
266
|
+
const entry = { revoked: false }; issued.set(claim.id, entry);
|
|
267
|
+
try {
|
|
268
|
+
const key = await importSigningKey(signer);
|
|
269
|
+
const signature = await crypto.subtle.sign(getIdentitySignAlgorithm(signer), key, bytes(claim));
|
|
270
|
+
assert(!closed && !entry.revoked, 'Partition authority closed');
|
|
271
|
+
return { claim, publicJwk: structuredClone(signer.publicJwk), signature: toBase64Url(signature) };
|
|
272
|
+
} catch (error) { entry.revoked = true; throw error; }
|
|
273
|
+
},
|
|
274
|
+
revoke(grant) { const entry = issued.get(grant?.claim?.id); if (entry) entry.revoked = true; },
|
|
275
|
+
close() { closed = true; verified.clear(); for (const entry of issued.values()) entry.revoked = true; }
|
|
276
|
+
});
|
|
277
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createP2PTransport } from '../../transport/assignment.js';
|
|
2
|
+
import { createMeshPeerIdentity } from '../peer-identity.js';
|
|
3
|
+
import { assertPartition as assert } from './partition-contract.js';
|
|
4
|
+
|
|
5
|
+
/** Manual signaling adapter with certificate-bound identities. Reuses Poolday's
|
|
6
|
+
* transport and identity challenge; a signaling code grants no compute permission.
|
|
7
|
+
*/
|
|
8
|
+
export function createPartitionLink({ identity, roomId, kind, initiator, config, rtcConfig,
|
|
9
|
+
timeoutMs, createEndpoint, onChange = () => {} }) {
|
|
10
|
+
assert(['entry', 'stage'].includes(kind) && Number.isSafeInteger(timeoutMs) && timeoutMs > 0,
|
|
11
|
+
'Explicit partition link kind and timeout required');
|
|
12
|
+
let remoteId = null, receiver, endpoint = null, channel = null, disposed = false, codeSignals = [], connecting = null;
|
|
13
|
+
const handlers = new Map(), lifetime = new AbortController();
|
|
14
|
+
const signal = (type, payload) => { codeSignals.push({ type, payload }); };
|
|
15
|
+
const fingerprint = description => description?.sdp?.split(/\r?\n/)
|
|
16
|
+
.find(line => line.startsWith('a=fingerprint:sha-256 '))?.slice(14);
|
|
17
|
+
const transport = createP2PTransport({ config, initiator, rtcConfig,
|
|
18
|
+
signaling: { subscribe: fn => { receiver = fn; return () => { receiver = null; }; },
|
|
19
|
+
sendOffer: payload => signal('offer', payload), sendAnswer: payload => signal('answer', payload),
|
|
20
|
+
sendIceCandidate: payload => signal('ice-candidate', payload), sendClose() {} },
|
|
21
|
+
onMessage: message => handlers.get(message.type)?.(remoteId, message.payload),
|
|
22
|
+
onAuxiliaryChannel: accepted => {
|
|
23
|
+
if (initiator || channel || accepted.label !== 'reploid-partition-' + kind) return false;
|
|
24
|
+
channel = accepted; attach(channel).catch(failed); return true;
|
|
25
|
+
},
|
|
26
|
+
onStateChange: state => {
|
|
27
|
+
if (state === 'connected' && initiator) {
|
|
28
|
+
channel = transport.getPeerConnection().createDataChannel('reploid-partition-' + kind, { ordered: true });
|
|
29
|
+
attach(channel).catch(failed);
|
|
30
|
+
}
|
|
31
|
+
if (['failed', 'closed'].includes(state)) {
|
|
32
|
+
lifetime.abort(new Error('Partition link lost'));
|
|
33
|
+
Promise.resolve(endpoint?.close()).catch(failed);
|
|
34
|
+
}
|
|
35
|
+
onChange();
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const peerIdentity = createMeshPeerIdentity({ transport: {
|
|
39
|
+
_getPeerId: () => identity.peerId,
|
|
40
|
+
getPeerBinding: () => {
|
|
41
|
+
const pc = transport.getPeerConnection();
|
|
42
|
+
return transport.getState() === 'connected' ? { local: fingerprint(pc.localDescription), remote: fingerprint(pc.remoteDescription) } : null;
|
|
43
|
+
},
|
|
44
|
+
onMessage: (type, handler) => handlers.set(type, handler),
|
|
45
|
+
sendToPeer: (_peer, type, payload) => { transport.send({ type, payload }); return true; },
|
|
46
|
+
}, getIdentity: () => identity, roomId, timeoutMs, maxPending: 2 });
|
|
47
|
+
let error = null;
|
|
48
|
+
function failed(cause) { error = String(cause.message); lifetime.abort(cause); channel?.close(); onChange(); }
|
|
49
|
+
async function attach(accepted) {
|
|
50
|
+
const deadline = AbortSignal.any([lifetime.signal, AbortSignal.timeout(timeoutMs)]);
|
|
51
|
+
const wait = (event, predicate) => new Promise((resolve, reject) => {
|
|
52
|
+
const done = (error) => { accepted.removeEventListener(event, received); deadline.removeEventListener('abort', aborted); error ? reject(error) : resolve(); };
|
|
53
|
+
const received = input => { if (!predicate || predicate(input)) done(); };
|
|
54
|
+
const aborted = () => done(deadline.reason);
|
|
55
|
+
accepted.addEventListener(event, received); deadline.addEventListener('abort', aborted, { once: true });
|
|
56
|
+
if (deadline.aborted) aborted(); else if (event === 'open' && accepted.readyState === 'open') done();
|
|
57
|
+
});
|
|
58
|
+
const ready = initiator ? wait('message', e => e.data === 'partition-entry-ready') : wait('open');
|
|
59
|
+
ready.catch(() => {});
|
|
60
|
+
await wait('open');
|
|
61
|
+
const verified = await peerIdentity.verify(remoteId, deadline);
|
|
62
|
+
assert(verified === remoteId, 'Partition link signing identity differs from signaling code');
|
|
63
|
+
if (initiator) await ready;
|
|
64
|
+
deadline.throwIfAborted();
|
|
65
|
+
endpoint = await createEndpoint({ channel: accepted, localParticipantId: identity.peerId, remoteParticipantId: verified, kind, initiator });
|
|
66
|
+
if (!initiator) accepted.send('partition-entry-ready');
|
|
67
|
+
onChange();
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
get endpoint() { return endpoint; },
|
|
71
|
+
get remoteId() { return remoteId; },
|
|
72
|
+
getState: () => ({ state: transport.getState(), kind, remoteId, ready: !!endpoint && !lifetime.signal.aborted, error }),
|
|
73
|
+
async connect() {
|
|
74
|
+
if (connecting) return connecting;
|
|
75
|
+
connecting = transport.connect(); connecting.catch(failed);
|
|
76
|
+
assert(transport.getPeerConnection(), 'Transport did not create its connection');
|
|
77
|
+
},
|
|
78
|
+
async exportCode() {
|
|
79
|
+
const deadline = performance.now() + timeoutMs;
|
|
80
|
+
while (!codeSignals.some(s => s.type === (initiator ? 'offer' : 'answer'))
|
|
81
|
+
|| transport.getPeerConnection().iceGatheringState !== 'complete') {
|
|
82
|
+
lifetime.signal.throwIfAborted();
|
|
83
|
+
if (performance.now() >= deadline) throw new Error('Partition signaling timed out');
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
85
|
+
}
|
|
86
|
+
return btoa(JSON.stringify({ schema: 'reploid.partition-link/v1', peerId: identity.peerId, roomId, kind, signals: codeSignals }));
|
|
87
|
+
},
|
|
88
|
+
acceptCode(code) {
|
|
89
|
+
assert(typeof code === 'string' && code.length <= 90000, 'Invalid partition connection code');
|
|
90
|
+
const packet = JSON.parse(atob(code.trim()));
|
|
91
|
+
assert(packet.schema === 'reploid.partition-link/v1' && packet.roomId === roomId && packet.kind === kind
|
|
92
|
+
&& /^peer:[a-f0-9]{24}$/.test(packet.peerId) && packet.peerId !== identity.peerId
|
|
93
|
+
&& (!remoteId || remoteId === packet.peerId) && Array.isArray(packet.signals) && packet.signals.length <= 70,
|
|
94
|
+
'Partition signaling identity mismatch');
|
|
95
|
+
remoteId = packet.peerId;
|
|
96
|
+
for (const item of packet.signals) { assert(['offer', 'answer', 'ice-candidate'].includes(item.type), 'Invalid signal'); receiver(item); }
|
|
97
|
+
},
|
|
98
|
+
async close() {
|
|
99
|
+
if (disposed) return; disposed = true; lifetime.abort(); peerIdentity.close();
|
|
100
|
+
await endpoint?.close(); await transport.close();
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createRoutePartitionNetwork } from './partition-route-network.js';
|
|
2
|
+
import { assertPartition as assert } from './partition-contract.js';
|
|
3
|
+
|
|
4
|
+
const LABEL = 'reploid-partitions';
|
|
5
|
+
const READY = 'reploid.partition-channel-ready/v1';
|
|
6
|
+
|
|
7
|
+
/** Uses the existing mesh certificate/signature verifier; never trusts an advertised peer ID. */
|
|
8
|
+
export function createPartitionNetwork({ transport, verifyPeer, createEndpoint, maxPeers, timeoutMs, onPeer = () => {}, label = LABEL, ordered = null }) {
|
|
9
|
+
if (ordered !== null) return createRoutePartitionNetwork({ transport, verifyPeer, createEndpoint, maxPeers, timeoutMs, onPeer, label, ordered });
|
|
10
|
+
assert((/^reploid-partitions(?:-[a-z-]+)?$/.test(label) || label === 'reploid-training') && label.length <= 80, 'Invalid partition channel label');
|
|
11
|
+
assert(typeof transport?.onDataChannel === 'function' && typeof transport.getPeerBinding === 'function'
|
|
12
|
+
&& typeof verifyPeer === 'function' && typeof createEndpoint === 'function'
|
|
13
|
+
&& Number.isSafeInteger(maxPeers) && maxPeers > 0 && Number.isSafeInteger(timeoutMs) && timeoutMs > 0
|
|
14
|
+
&& timeoutMs <= 2147483647, 'Partition network ports and budgets required');
|
|
15
|
+
const entries = new Map(), retiring = new Set();
|
|
16
|
+
let closed = false, closing = null, settlementFailure = null;
|
|
17
|
+
const settleEntry = entry => {
|
|
18
|
+
if (!entry.settlement) {
|
|
19
|
+
entry.settlement = Promise.resolve().then(() => entry.endpoint?.close());
|
|
20
|
+
retiring.add(entry.settlement);
|
|
21
|
+
entry.settlement.then(() => retiring.delete(entry.settlement), error => {
|
|
22
|
+
retiring.delete(entry.settlement); settlementFailure ||= error;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return entry.settlement;
|
|
26
|
+
};
|
|
27
|
+
const wait = (entry, handshake) => new Promise((resolve, reject) => {
|
|
28
|
+
const finish = error => {
|
|
29
|
+
clearTimeout(timer); entry.channel.removeEventListener('open', opened);
|
|
30
|
+
entry.channel.removeEventListener('message', message); entry.channel.removeEventListener('close', lost);
|
|
31
|
+
entry.controller.signal.removeEventListener('abort', lost);
|
|
32
|
+
error ? reject(error) : resolve();
|
|
33
|
+
};
|
|
34
|
+
const opened = () => { if (!handshake) finish(); };
|
|
35
|
+
const message = event => finish(event.data === READY ? null : new Error('Invalid partition channel handshake'));
|
|
36
|
+
const lost = () => finish(new Error('Partition channel closed during authentication'));
|
|
37
|
+
const timer = setTimeout(() => finish(new Error('Partition channel authentication timed out')), timeoutMs);
|
|
38
|
+
entry.channel.addEventListener('open', opened);
|
|
39
|
+
if (handshake) entry.channel.addEventListener('message', message, { once: true });
|
|
40
|
+
entry.channel.addEventListener('close', lost, { once: true });
|
|
41
|
+
entry.controller.signal.addEventListener('abort', lost, { once: true });
|
|
42
|
+
if (entry.controller.signal.aborted || entry.channel.readyState === 'closed') lost();
|
|
43
|
+
else if (!handshake && entry.channel.readyState === 'open') finish();
|
|
44
|
+
});
|
|
45
|
+
const attach = (peerId, channel, incoming) => {
|
|
46
|
+
assert(!closed && !entries.has(peerId) && entries.size < maxPeers, 'Partition peer limit or duplicate connection');
|
|
47
|
+
const entry = { channel, controller: new AbortController(), endpoint: null, promise: null };
|
|
48
|
+
entries.set(peerId, entry);
|
|
49
|
+
channel.addEventListener('close', () => {
|
|
50
|
+
entry.controller.abort();
|
|
51
|
+
if (entries.get(peerId) === entry) entries.delete(peerId);
|
|
52
|
+
settleEntry(entry);
|
|
53
|
+
}, { once: true });
|
|
54
|
+
// Install the handshake listener before any asynchronous certificate verification.
|
|
55
|
+
const ready = wait(entry, !incoming);
|
|
56
|
+
ready.catch(() => {});
|
|
57
|
+
entry.promise = (async () => {
|
|
58
|
+
const timer = setTimeout(() => entry.controller.abort(), timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const before = transport.getPeerBinding(peerId);
|
|
61
|
+
assert(before, 'Authenticated WebRTC certificate binding required');
|
|
62
|
+
const verifiedId = await verifyPeer(peerId, entry.controller.signal);
|
|
63
|
+
entry.controller.signal.throwIfAborted();
|
|
64
|
+
const after = transport.getPeerBinding(peerId);
|
|
65
|
+
assert(/^peer:[a-f0-9]{24}$/.test(verifiedId) && before.local === after?.local && before.remote === after?.remote,
|
|
66
|
+
'Partition peer identity or certificate changed');
|
|
67
|
+
await ready;
|
|
68
|
+
entry.controller.signal.throwIfAborted();
|
|
69
|
+
entry.endpoint = createEndpoint({ channel, remoteParticipantId: verifiedId });
|
|
70
|
+
if (incoming) channel.send(READY);
|
|
71
|
+
onPeer(peerId, entry.endpoint);
|
|
72
|
+
return entry.endpoint;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
entry.controller.abort(); channel.close();
|
|
75
|
+
if (entries.get(peerId) === entry) entries.delete(peerId);
|
|
76
|
+
await settleEntry(entry).catch(() => {});
|
|
77
|
+
throw error;
|
|
78
|
+
} finally { clearTimeout(timer); }
|
|
79
|
+
})();
|
|
80
|
+
return entry.promise;
|
|
81
|
+
};
|
|
82
|
+
const unsubscribe = transport.onDataChannel(label, (peerId, channel) => {
|
|
83
|
+
try { attach(peerId, channel, true).catch(() => {}); } catch { channel.close(); }
|
|
84
|
+
});
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
connect(peerId) {
|
|
87
|
+
assert(!closed, 'Partition network closed');
|
|
88
|
+
if (entries.has(peerId)) return entries.get(peerId).promise;
|
|
89
|
+
assert(entries.size < maxPeers, 'Partition peer limit');
|
|
90
|
+
const channel = transport.openDataChannel(peerId, label);
|
|
91
|
+
try { return attach(peerId, channel, false); } catch (error) { channel.close(); throw error; }
|
|
92
|
+
},
|
|
93
|
+
close() {
|
|
94
|
+
if (closing) return closing;
|
|
95
|
+
closed = true; unsubscribe();
|
|
96
|
+
const pending = [...entries.values()];
|
|
97
|
+
closing = Promise.resolve().then(async () => {
|
|
98
|
+
for (const entry of pending) { entry.controller.abort(); entry.channel.close(); }
|
|
99
|
+
await Promise.allSettled(pending.map(entry => entry.promise));
|
|
100
|
+
const results = await Promise.allSettled([...retiring, ...pending.map(settleEntry)]);
|
|
101
|
+
entries.clear();
|
|
102
|
+
const failures = results.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
103
|
+
if (settlementFailure && !failures.includes(settlementFailure)) failures.push(settlementFailure);
|
|
104
|
+
if (failures.length) throw new AggregateError(failures, 'Partition network settlement failed');
|
|
105
|
+
});
|
|
106
|
+
return closing;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|