@volter/world-core 2.0.37 → 3.0.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/README.md +4 -5
- package/app-route.cjs +12 -6
- package/app-route.d.cts +1 -1
- package/dist/app-route.cjs +12 -6
- package/dist/app-route.d.cts +1 -1
- package/dist/generated/pack-facts.json +1410 -3069
- package/dist/inject.cjs +64 -9
- package/dist/pack-facts.cjs +44 -0
- package/dist/src/actions.d.ts +3 -3
- package/dist/src/actions.js +22 -16
- package/dist/src/ancestry.d.ts +14 -2
- package/dist/src/ancestry.js +92 -2
- package/dist/src/anthropic-wire.d.ts +39 -0
- package/dist/src/anthropic-wire.js +136 -0
- package/dist/src/bytes.d.ts +7 -0
- package/dist/src/bytes.js +35 -0
- package/dist/src/changeset.d.ts +1 -1
- package/dist/src/changeset.js +0 -0
- package/dist/src/clickhouse/index.d.ts +3 -0
- package/dist/src/clickhouse/index.js +6 -0
- package/dist/src/clickhouse/sql.d.ts +233 -0
- package/dist/src/clickhouse/sql.js +4329 -0
- package/dist/src/clickhouse/types.d.ts +18 -0
- package/dist/src/clickhouse/types.js +47 -0
- package/dist/src/clickhouse/values.d.ts +146 -0
- package/dist/src/clickhouse/values.js +858 -0
- package/dist/src/client-bundle.js +2 -3
- package/dist/src/cors.d.ts +15 -0
- package/dist/src/cors.js +31 -0
- package/dist/src/derived-core.d.ts +487 -24
- package/dist/src/derived-core.js +788 -144
- package/dist/src/derived-real.d.ts +13 -0
- package/dist/src/derived-real.js +518 -0
- package/dist/src/derived.d.ts +35 -1
- package/dist/src/derived.js +61 -9
- package/dist/src/emit.js +1 -2
- package/dist/src/events.d.ts +206 -0
- package/dist/src/events.js +341 -0
- package/dist/src/executor.d.ts +3 -0
- package/dist/src/executor.js +19 -2
- package/dist/src/file-response.d.ts +6 -0
- package/dist/src/file-response.js +30 -0
- package/dist/src/fork.js +3 -2
- package/dist/src/git/history.d.ts +7 -0
- package/dist/src/git/history.js +24 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/lfs.d.ts +28 -0
- package/dist/src/git/lfs.js +66 -0
- package/dist/src/git/objects.js +3 -8
- package/dist/src/git/smart-http.d.ts +3 -1
- package/dist/src/git/smart-http.js +67 -6
- package/dist/src/graphql-wire.d.ts +29 -0
- package/dist/src/graphql-wire.js +101 -0
- package/dist/src/grpc-wire.d.ts +67 -0
- package/dist/src/grpc-wire.js +170 -0
- package/dist/src/h2.d.ts +40 -0
- package/dist/src/h2.js +656 -0
- package/dist/src/head.d.ts +32 -3
- package/dist/src/head.js +161 -40
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +6 -6
- package/dist/src/hpack.json +1 -0
- package/dist/src/index.d.ts +64 -75
- package/dist/src/index.js +58 -101
- package/dist/src/log.js +28 -19
- package/dist/src/machines.d.ts +50 -0
- package/dist/src/machines.js +151 -0
- package/dist/src/managed-database.d.ts +86 -0
- package/dist/src/managed-database.js +283 -0
- package/dist/src/multipart.d.ts +11 -0
- package/dist/src/multipart.js +51 -0
- package/dist/src/observe.d.ts +15 -5
- package/dist/src/observe.js +23 -9
- package/dist/src/openai-wire.d.ts +108 -0
- package/dist/src/openai-wire.js +337 -0
- package/dist/src/pack-assets.d.ts +3 -4
- package/dist/src/pack-assets.js +15 -10
- package/dist/src/pack-fetch.d.ts +77 -0
- package/dist/src/pack-fetch.js +449 -0
- package/dist/src/pack-paths.d.ts +12 -0
- package/dist/src/pack-paths.js +86 -0
- package/dist/src/packRegistry.d.ts +69 -162
- package/dist/src/packRegistry.js +55 -20
- package/dist/src/people.d.ts +13 -0
- package/dist/src/people.js +18 -0
- package/dist/src/placeholder-image.d.ts +5 -0
- package/dist/src/placeholder-image.js +114 -0
- package/dist/src/protobuf.d.ts +28 -0
- package/dist/src/protobuf.js +332 -0
- package/dist/src/redis/engine.js +1 -1
- package/dist/src/request-scope.d.ts +1 -1
- package/dist/src/request-scope.js +6 -4
- package/dist/src/resource-blob.d.ts +5 -0
- package/dist/src/resource-blob.js +11 -0
- package/dist/src/runtime.d.ts +85 -0
- package/dist/src/runtime.js +104 -0
- package/dist/src/s3/wire.d.ts +60 -0
- package/dist/src/s3/wire.js +157 -0
- package/dist/src/scenario.d.ts +3 -0
- package/dist/src/scenario.js +2 -0
- package/dist/src/schema-sample.d.ts +1 -0
- package/dist/src/schema-sample.js +21 -0
- package/dist/src/sealed-box.d.ts +14 -0
- package/dist/src/sealed-box.js +225 -0
- package/dist/src/serve-http.d.ts +14 -0
- package/dist/src/serve-http.js +27 -3
- package/dist/src/serve.d.ts +6 -0
- package/dist/src/serve.js +69 -14
- package/dist/src/signing.d.ts +135 -0
- package/dist/src/signing.js +222 -0
- package/dist/src/sigv4.d.ts +48 -0
- package/dist/src/sigv4.js +167 -0
- package/dist/src/smtp.d.ts +16 -0
- package/dist/src/smtp.js +72 -0
- package/dist/src/sockets.d.ts +51 -0
- package/dist/src/sockets.js +90 -0
- package/dist/src/state-system.d.ts +1 -0
- package/dist/src/state-system.js +1 -1
- package/dist/src/storage.d.ts +1 -1
- package/dist/src/storage.js +3 -3
- package/dist/src/trace-context.js +1 -1
- package/dist/src/twin-fetch.d.ts +0 -7
- package/dist/src/twin-fetch.js +0 -14
- package/dist/src/vendor-call.d.ts +6 -0
- package/dist/src/vendor-call.js +41 -0
- package/dist/src/world-store.js +1 -1
- package/dist/vendor-hosts.cjs +36 -125
- package/dist/vendor-hosts.d.cts +8 -0
- package/generated/pack-facts.json +1410 -3069
- package/inject.cjs +64 -9
- package/pack-facts.cjs +44 -0
- package/package.json +17 -3
- package/src/actions.ts +23 -16
- package/src/ancestry.ts +74 -2
- package/src/anthropic-wire.ts +137 -0
- package/src/bytes.ts +42 -0
- package/src/changeset.ts +5 -5
- package/src/clickhouse/index.ts +6 -0
- package/src/clickhouse/sql.ts +3059 -0
- package/src/clickhouse/types.ts +44 -0
- package/src/clickhouse/values.ts +697 -0
- package/src/client-bundle.ts +2 -3
- package/src/cors.ts +34 -0
- package/src/derived-core.ts +1013 -146
- package/src/derived-real.ts +434 -0
- package/src/derived.ts +73 -3
- package/src/emit.ts +1 -2
- package/src/events.ts +449 -0
- package/src/executor.ts +24 -2
- package/src/file-response.ts +27 -0
- package/src/fork.ts +3 -2
- package/src/git/history.ts +19 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +67 -0
- package/src/git/objects.ts +3 -5
- package/src/git/smart-http.ts +56 -6
- package/src/graphql-wire.ts +106 -0
- package/src/grpc-wire.ts +159 -0
- package/src/h2.ts +627 -0
- package/src/head.ts +132 -41
- package/src/history.ts +6 -6
- package/src/hpack.json +1 -0
- package/src/index.ts +82 -329
- package/src/log.ts +27 -18
- package/src/machines.ts +151 -0
- package/src/managed-database.ts +299 -0
- package/src/multipart.ts +51 -0
- package/src/observe.ts +31 -15
- package/src/openai-wire.ts +371 -0
- package/src/pack-assets.ts +15 -11
- package/src/pack-fetch.ts +458 -0
- package/src/pack-paths.ts +72 -0
- package/src/packRegistry.ts +79 -167
- package/src/people.ts +31 -0
- package/src/placeholder-image.ts +88 -0
- package/src/protobuf.ts +251 -0
- package/src/redis/engine.ts +1 -1
- package/src/request-scope.ts +8 -4
- package/src/resource-blob.ts +13 -0
- package/src/runtime.ts +344 -0
- package/src/s3/wire.ts +172 -0
- package/src/scenario.ts +4 -0
- package/src/schema-sample.ts +24 -0
- package/src/sealed-box.ts +182 -0
- package/src/serve-http.ts +31 -3
- package/src/serve.ts +58 -14
- package/src/signing.ts +231 -0
- package/src/sigv4.ts +158 -0
- package/src/smtp.ts +76 -0
- package/src/sockets.ts +140 -0
- package/src/state-system.ts +2 -2
- package/src/storage.ts +3 -3
- package/src/trace-context.ts +1 -1
- package/src/twin-fetch.ts +0 -20
- package/src/vendor-call.ts +41 -0
- package/src/world-store.ts +1 -1
- package/vendor-hosts.cjs +36 -125
- package/vendor-hosts.d.cts +8 -0
- package/dist/src/mirror-shell.d.ts +0 -2
- package/dist/src/mirror-shell.js +0 -13
- package/dist/src/v1-removed.d.ts +0 -159
- package/dist/src/v1-removed.js +0 -124
- package/src/mirror-shell.ts +0 -15
- package/src/v1-removed.ts +0 -172
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @volter/world-core/runtime — the kernel itself and the vendor-independent runtime libraries: the log and the tree
|
|
2
|
+
// read and written directly, checkpoints, branches, landing and receipts, the head, the stores, the runtime's scopes and
|
|
3
|
+
// clocks, and the plugin contract's surfaces. It is the runtime's, the CLI's, the host's and the tooling's. A Protocol 3
|
|
4
|
+
// pack never imports it: its contract is the root, `@volter/world-core` (docs/contributing/architecture.md, "The pack
|
|
5
|
+
// contract: the kernel's root"). Removed v1 entry points fail explicitly. Conformance and validation tooling lives in
|
|
6
|
+
// @volter/world-tooling, a dev dependency.
|
|
7
|
+
// Pack registry — vendor twins self-describe (TwinPack) so tooling discovers them.
|
|
8
|
+
export { clearRegistry, getPack, hasPack, listPacks, packOf, registerPack, pullPosture, assertContinuousPullAllowed, pullOnSchedule, resolvePullVendor, DEFAULT_PULL_POSTURE, DEFAULT_PULL_TRIGGER, PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from "./packRegistry.js";
|
|
9
|
+
// The DELIVER verb (`emit`) — vendor-agnostic engine + CLI glue; packs declare a TwinEmitter
|
|
10
|
+
// (their event catalog, endpoints-from-state, and signed synthesis) on `TwinPack.emitter`.
|
|
11
|
+
export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
|
|
12
|
+
export { WORLD_CLOCK_ENV, worldNow } from "./world-clock.js";
|
|
13
|
+
export { WORLD_ENV_NAMES_ENV, worldEnvValue } from "./world-env.js";
|
|
14
|
+
export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, twinPublicBase, withRequestScopes } from "./twin-fetch.js";
|
|
15
|
+
export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from "./request-scope.js";
|
|
16
|
+
export { compileSurface, createDerivedFetch, matchOperation } from "./derived.js";
|
|
17
|
+
export { currentTraceparent, deliveryTraceHeaders, newTraceparent, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from "./trace-context.js";
|
|
18
|
+
export { authRefusal, bindSemantics, coreFor, crossCutting, flagOf, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from "./derived-core.js";
|
|
19
|
+
export { deliverEvents, fillEnvelope, signEvent, signEventAll, verifyEvent } from "./events.js";
|
|
20
|
+
export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtVerify, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from "./signing.js";
|
|
21
|
+
export { emitTwinEvent, eventSubscriptionMatches, listEmittable, runEmitCli } from "./emit.js";
|
|
22
|
+
// The vendor-agnostic CLIENT-SIDE RATE BUDGET — the fail-closed backstop a pack's guarded client
|
|
23
|
+
// routes every live vendor call through. The MECHANISM is here; the per-vendor ceiling/window/
|
|
24
|
+
// weights are DATA the pack declares (`TwinPack.rateBudget` / `declareRateBudget`). Exported so an
|
|
25
|
+
// operator can inspect spend (`snapshot`) and a caller can catch `RateBudgetError` by type; there
|
|
26
|
+
// is deliberately no export that disables the guard, and a vendor with no declaration falls back
|
|
27
|
+
// to `DEFAULT_RATE_BUDGET` rather than to no limit at all.
|
|
28
|
+
export { DEFAULT_RATE_BUDGET, MAX_RATE_BUDGET_CEILING, MAX_RATE_BUDGET_WINDOW_MS, MIN_RATE_BUDGET_WEIGHT, MIN_RATE_BUDGET_WINDOW_MS, RateBudget, RateBudgetError, declareRateBudget, hasRateBudgetDeclaration, listRateBudgets, priceCall, rateBudgetPath, rateBudgetPolicy, rateBudgetWeight, resetToSeconds, assertBudgetGuardIntact, } from "./rateBudget.js";
|
|
29
|
+
export { GenericWorldStateSchema, WorldActorSchema, WorldExternalRefSchema, WorldServiceEventSchema, WorldSubjectSchema, } from "./schemas.js";
|
|
30
|
+
// NOTE: source books (event→tracker-source mapping) moved OUT of the world — it was
|
|
31
|
+
// the last tracker coupling. It now lives at `@volter/tracker/world-source-books`,
|
|
32
|
+
// so `@volter/world-core` is a pure twin runtime.
|
|
33
|
+
export { loadWorldConfig, } from "./worldConfig.js";
|
|
34
|
+
export { DELTA_TYPE_SUFFIX, diffSubjectFields, hashFieldValue, subjectKey, deltaAfterFields } from "./hash.js";
|
|
35
|
+
export { appendDurable, appendEvent, createEvent, emptyGenericState, genericWorldReducer, listEvents, loadState, projectionLockPath, readJsonFile, rebuildGenericState, rebuildState,
|
|
36
|
+
// Scrub: delete pulled data at rest (TWIN-45) — the honest, plain-`rm` counterpart to
|
|
37
|
+
// sync pull's fold-into-the-log. See docs/concepts/data-and-keys.md for the full data-at-rest story.
|
|
38
|
+
scrubService, scrubWorld, stateDirName,
|
|
39
|
+
// Opt-in structured stderr logging (VOLTER_TWIN_LOG=1) for the audit trail. Public so a
|
|
40
|
+
// CONNECTOR can say on the same stream when the vendor handed it a truncated page — a
|
|
41
|
+
// silent shortfall is the one thing a pull must never pass off as the whole resource.
|
|
42
|
+
twinLog,
|
|
43
|
+
// The kernel's cross-process mutual-exclusion primitive (exclusive-create lockfile with
|
|
44
|
+
// stale-holder reclaim). Public so world-runtime can guard concurrent `upWorld` claims of
|
|
45
|
+
// one instance dir with the SAME lock semantics the event log uses (TWIN-36).
|
|
46
|
+
withFileLock, worldPaths, worldStateRoot,
|
|
47
|
+
// Where another pack's store is in a World (architecture A3), and the variable the runtime names its data directory in.
|
|
48
|
+
ownerStoreRoots, WORLD_DATA_ENV, } from "./storage.js";
|
|
49
|
+
// The pluggable persistence seam: the sync WorldStore interface, its fs (default) and
|
|
50
|
+
// in-memory implementations, the active-store injection point, and the async
|
|
51
|
+
// hydrate/flush boundary a serverless (Durable Object / KV / redis) entry uses.
|
|
52
|
+
export { FsWorldStore, MemoryWorldStore, getActiveWorldStore, setActiveWorldStore, withWorldStore, hydrateInto, flushFrom, } from "./world-store.js";
|
|
53
|
+
export { SqlWorldStore } from "./world-store-sql.js";
|
|
54
|
+
// The blob seam (runtime contract R11): byte storage behind byte-carrying handlers, so a
|
|
55
|
+
// serverless namespace puts bytes in object storage while local worlds keep today's layout.
|
|
56
|
+
export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, } from "./blob-store.js";
|
|
57
|
+
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, CALLER_HEADER, callerForHeader, callerFromHeader, journalTwinRequest, readTwinRequestJournalFrom, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, recordedInput, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
|
|
58
|
+
export { createTwinProxy } from "./proxy.js";
|
|
59
|
+
export { forkTwin, isFork, readForkMeta, } from "./fork.js";
|
|
60
|
+
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from "./actions.js";
|
|
61
|
+
// ── Operator control plane (R19/R20): remote refs, queue lifecycle, push ledger,
|
|
62
|
+
// apply leases, plan, status. (the twins architecture notes)
|
|
63
|
+
// The CHANGESET primitive + the ledger DIFF (docs/concepts/the-model.md v0) — "commit" and
|
|
64
|
+
// "diff" for operational reality: a cross-service base marker, the ledger delta since it, and
|
|
65
|
+
// the content-addressed, replayable changeset over that delta. State-level by construction
|
|
66
|
+
// (it reads the same per-service action ledgers `actions.ts` writes); `volter-world` supplies
|
|
67
|
+
// world discovery and thin verbs on top.
|
|
68
|
+
export { approveChangeset, assertMarkerBelongsTo, assertSafeChangesetName, assertValidVerifiers, buildChangeset, captureMarker, changesetContentHash, changesetHashMatches, changesetReadiness, approverOf, CHANGESET_KIND, diffLedgers, formatApplication, formatChangeset, formatRebaseReport, narrateActions, narrationDrift, rebaseChangeset, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, MARKER_KIND, normalizeChangeset, parseVerifierExpression, replayChangeset, runChangesetVerifiers, summarizeByVendor, twinWriteShape, withApplication, withVerification, worldBootMarker, WORLD_BOOT_MARKER_ID, } from "./changeset.js";
|
|
69
|
+
export { getActivePackAssets, setActivePackAssets, packAsset, assetContentType, confinedAssetPath, LocalPackAssets, BindingPackAssets } from "./pack-assets.js";
|
|
70
|
+
export { installedPackageDir, packRepository, isDerivedPack, vendorPackageDir, vendorOfPackModule } from "./pack-paths.js";
|
|
71
|
+
// The git plane (contract "The git plane is a kernel library"): objects, packs, refs, smart HTTP.
|
|
72
|
+
export * as git from "./git/index.js";
|
|
73
|
+
// S3's wire (contract "S3's wire is a kernel library too"): its XML, CORS, lifecycle, delete batches, ETags, body bytes.
|
|
74
|
+
export * as s3 from "./s3/wire.js";
|
|
75
|
+
// The push arm: the kernel's transaction over a pack's one `perform<Name>Action` (contract section of that name).
|
|
76
|
+
// The placeholder remote (contract section of that name): the seed is a pull, never a pending write.
|
|
77
|
+
export { PLACEHOLDER_REMOTE, beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, placeholderPullMarkerPath, withPlaceholderPull, placeholderEventsFor } from "./placeholder-remote.js";
|
|
78
|
+
export { aliasesFrom, ownFields, subjectHistory, rebaseBranch, cutCheckpoint, appendParentEntry, wholeLog, branchEntries, branchLogPath, branchMetaPath, CHECKPOINT_EVERY, dropCheckpoint, foldEntries, landedCopy, landedIds, parentEntries, parentLogPath, position, readBranchMeta, readTree, toEntry, toEvent, unpushedEntries, writeBranchMeta } from "./log.js";
|
|
79
|
+
export { sealCredential, openSealedCredential, MemoryCredentialStorage, keySealer, transitSealer } from "./credential.js";
|
|
80
|
+
export { validateRemoteOrigin, buildRemoteExecute } from "./executor.js";
|
|
81
|
+
export { clearRoot, authStrategyFor, clearStateSystems, NO_SECRETS_CHECK, readRoot, registerAuthStrategy, registerStateSystem, rootPath, runChecks, stateSystemFor, writeRoot } from "./state-system.js";
|
|
82
|
+
export { observeAppends } from "./actions.js";
|
|
83
|
+
export { appendActionIfAbsent, appendActionOccurrence } from "./actions.js";
|
|
84
|
+
// the head and the fold (company contract "The head", "Refresh is the kernel's fold")
|
|
85
|
+
export { answerVendorErrors, performEntries, performAtHead, deployableEntries, headOf, boundRoot, loadChecks, loadCheck, openRootCredential, resealRootCredential, destinationOf, openSealedRecord, sealsThroughVault, rootCustody, sealingKey, setCheckLoader, setSealingKeySource, setSealer, currentSealer, openSealedAt, userKek, userKekPath, vendorOf, worldRootOf, RefusedWriteError, VendorWriteError, HeadError } from "./head.js";
|
|
86
|
+
export { observeResource, observeResources, collectObservations, foldObservations } from "./observe.js";
|
|
87
|
+
export { readTreeMap, readParentTreeMap, treeStamp, treeChangesSince, originEntries, appendOriginEntry, originLogPath, isUrlParent, parentPosition, positionAt, splitsBatch, assertBatchBoundary } from "./log.js";
|
|
88
|
+
export { referenceField, registerReferences, packReferences, resolveReferences } from "./references.js";
|
|
89
|
+
// the HTTP server seam: the one place the serve path meets the runtime (Bun or Node)
|
|
90
|
+
export { serveHttp, serveStream, nodeBuiltin, WORLD_BOOT_PATH } from "./serve-http.js";
|
|
91
|
+
// two runtime-neutral helpers for a serve path: a file as a Response, a page's client bundle
|
|
92
|
+
export { fileResponse, contentTypeOf } from "./file-response.js";
|
|
93
|
+
export { bundleClient, filePathOf } from "./client-bundle.js";
|
|
94
|
+
// the brand's tokens and faces for a Volter page (the console, the site, the UI kit), fetched at build
|
|
95
|
+
export { brandTokensResponse } from "./brand-tokens.js";
|
|
96
|
+
export { multipartBoundary, multipartParts } from "./multipart.js";
|
|
97
|
+
export { corsHeaders, withCors } from "./cors.js";
|
|
98
|
+
export { vendorFetch, VendorUnreachableError } from "./vendor-call.js";
|
|
99
|
+
export { closeManagedDatabases, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver, WORLD_CLOCK_SETTING, worldClockSql } from "./managed-database.js";
|
|
100
|
+
export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobSize, resourceChain, writeResourceBlob } from "./resource-blob.js";
|
|
101
|
+
export { assertStateRemovable, checkParent, stateGeneration, withAncestryLock, withHistoryLock, withHistoryLocks, withStateLock, withStateRemoval } from "./ancestry.js";
|
|
102
|
+
export { captureHistory, captureParentHistory, historyChanges, historyAtInstant, historyDigest, historyEntries, historyLength, historyPrefix, inheritedHistory, originHead, publishOriginHistory, readHistoryView } from "./history.js";
|
|
103
|
+
export { foldHistory } from "./log.js";
|
|
104
|
+
export { volterHome } from "./volter-home.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export declare function esc(s: string | number): string;
|
|
2
|
+
/** An element and its children or text. */
|
|
3
|
+
export declare const x: (tag: string, inner: string | number | string[]) => string;
|
|
4
|
+
export declare const doc: (root: string, inner: string[]) => string;
|
|
5
|
+
export declare const xmlAnswer: (body: string, status?: number, headers?: Record<string, string>) => Response;
|
|
6
|
+
export declare function unesc(s: string): string;
|
|
7
|
+
/** Every element of a tag's text in an XML body, unescaped. */
|
|
8
|
+
export declare const allOf: (xml: string, tag: string) => string[];
|
|
9
|
+
/** The first. */
|
|
10
|
+
export declare const oneOf: (xml: string, tag: string) => string | undefined;
|
|
11
|
+
export type CorsRule = {
|
|
12
|
+
id?: string;
|
|
13
|
+
allowedOrigins: string[];
|
|
14
|
+
allowedMethods: string[];
|
|
15
|
+
allowedHeaders: string[];
|
|
16
|
+
exposeHeaders: string[];
|
|
17
|
+
maxAgeSeconds?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function parseCors(xml: string): CorsRule[];
|
|
20
|
+
export declare function corsXml(rules: CorsRule[]): string;
|
|
21
|
+
/** The first rule that allows the origin, the method and the headers a preflight asks for. */
|
|
22
|
+
export declare function corsRuleFor(rules: CorsRule[], origin: string, method: string, requested: string[]): CorsRule | undefined;
|
|
23
|
+
/** The Access-Control-* headers a matching rule gives an answer ("AllowedOrigins: Specifies the value for the
|
|
24
|
+
* Access-Control-Allow-Origin header", and likewise for the methods, headers, exposed headers and max age). */
|
|
25
|
+
export declare function corsHeaders(rule: CorsRule, origin: string, requested: string[], preflight: boolean): Record<string, string>;
|
|
26
|
+
export declare const requestedHeaders: (request: Request) => string[];
|
|
27
|
+
/** A rule's prefix is sent in its Filter or, in S3's older form, on the rule itself, or not at all (R2's lifecycle page
|
|
28
|
+
* sends a transition rule with neither, for the whole bucket); it is answered as it was sent. A
|
|
29
|
+
* transition moves an object to Infrequent Access (Object lifecycles: "transition objects to Infrequent Access storage
|
|
30
|
+
* after 30 days", its example `Transitions: [{ Days: 30, StorageClass: "STANDARD_IA" }]`). */
|
|
31
|
+
export type LifecycleRule = {
|
|
32
|
+
id: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
prefix: string;
|
|
35
|
+
prefixOnRule?: boolean;
|
|
36
|
+
noPrefix?: boolean;
|
|
37
|
+
expireDays?: number;
|
|
38
|
+
expireDate?: string;
|
|
39
|
+
abortMultipartDays?: number;
|
|
40
|
+
transitions?: Array<{
|
|
41
|
+
days?: number;
|
|
42
|
+
date?: string;
|
|
43
|
+
storageClass: string;
|
|
44
|
+
}>;
|
|
45
|
+
};
|
|
46
|
+
export declare function parseLifecycle(xml: string): LifecycleRule[];
|
|
47
|
+
export declare function lifecycleXml(rules: LifecycleRule[]): string;
|
|
48
|
+
export declare function parseDelete(xml: string): {
|
|
49
|
+
keys: string[];
|
|
50
|
+
quiet: boolean;
|
|
51
|
+
};
|
|
52
|
+
export declare const locationConstraintOf: (xml: string) => string | undefined;
|
|
53
|
+
export declare const md5Etag: (bytes: Uint8Array) => string;
|
|
54
|
+
export declare function withBytes(request: Request): Promise<Request>;
|
|
55
|
+
export declare const bytesOf: (request: Request) => Uint8Array;
|
|
56
|
+
/** Concatenate exact wire bytes; no text conversion is involved in multipart assembly. */
|
|
57
|
+
export declare function concatBytes(parts: readonly Uint8Array[]): Uint8Array;
|
|
58
|
+
export type ChecksumAlgorithm = 'MD5' | 'SHA1' | 'SHA256' | 'CRC32' | 'CRC32C' | 'CRC64NVME';
|
|
59
|
+
/** S3's base64 digest; CRC bytes are big-endian on the wire. No streaming framing is decoded here. */
|
|
60
|
+
export declare function checksum(algorithm: ChecksumAlgorithm, bytes: Uint8Array): string;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// S3'S WIRE — the XML bodies S3 answers and reads, a bucket's CORS and lifecycle documents, a delete batch, an object's
|
|
2
|
+
// ETag, and a request's body kept as its bytes (docs/contributing/architecture.md, "Other wires": S3's wire is a kernel
|
|
3
|
+
// library, as Redis's command core is). Every vendor that speaks S3's API speaks this wire: AWS S3 itself, Cloudflare
|
|
4
|
+
// R2, and the S3-compatible stores that follow. What differs between them (where a request's account and bucket are,
|
|
5
|
+
// what they refuse, what they implement) is each pack's own. Moved here from the cloudflare pack's R2 lane, which had it
|
|
6
|
+
// ported from the aws pack's S3 code; a pack may import neither (B4). Importing this module does nothing.
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
// ── XML ──────────────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
const DECL = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
10
|
+
const NS = 'http://s3.amazonaws.com/doc/2006-03-01/';
|
|
11
|
+
export function esc(s) {
|
|
12
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
13
|
+
}
|
|
14
|
+
/** An element and its children or text. */
|
|
15
|
+
export const x = (tag, inner) => `<${tag}>${Array.isArray(inner) ? inner.join('') : esc(inner)}</${tag}>`;
|
|
16
|
+
export const doc = (root, inner) => `${DECL}<${root} xmlns="${NS}">${inner.join('')}</${root}>`;
|
|
17
|
+
export const xmlAnswer = (body, status = 200, headers = {}) => new Response(body, { status, headers: { 'content-type': 'application/xml', ...headers } });
|
|
18
|
+
export function unesc(s) {
|
|
19
|
+
return s.replace(/&(?:lt|gt|quot|apos|amp|#\d+|#x[0-9a-fA-F]+);/g, decodeEntity);
|
|
20
|
+
}
|
|
21
|
+
/** One XML entity, named or numeric, as its character. */
|
|
22
|
+
function decodeEntity(m) {
|
|
23
|
+
const named = { '<': '<', '>': '>', '"': '"', ''': "'", '&': '&' };
|
|
24
|
+
if (named[m])
|
|
25
|
+
return named[m];
|
|
26
|
+
const body = m.slice(2, -1);
|
|
27
|
+
const code = body[0] === 'x' ? Number.parseInt(body.slice(1), 16) : Number(body);
|
|
28
|
+
return Number.isInteger(code) && code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : m;
|
|
29
|
+
}
|
|
30
|
+
/** Every element of a tag's text in an XML body, unescaped. */
|
|
31
|
+
export const allOf = (xml, tag) => [...xml.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'g'))].map((m) => unesc(m[1].trim()));
|
|
32
|
+
/** The first. */
|
|
33
|
+
export const oneOf = (xml, tag) => allOf(xml, tag)[0];
|
|
34
|
+
export function parseCors(xml) {
|
|
35
|
+
return [...xml.matchAll(/<CORSRule>([\s\S]*?)<\/CORSRule>/g)].map((m) => {
|
|
36
|
+
const b = m[1];
|
|
37
|
+
const id = oneOf(b, 'ID');
|
|
38
|
+
const age = oneOf(b, 'MaxAgeSeconds');
|
|
39
|
+
return { ...(id ? { id } : {}), allowedOrigins: allOf(b, 'AllowedOrigin'), allowedMethods: allOf(b, 'AllowedMethod'), allowedHeaders: allOf(b, 'AllowedHeader'), exposeHeaders: allOf(b, 'ExposeHeader'), ...(age ? { maxAgeSeconds: Number(age) } : {}) };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function corsXml(rules) {
|
|
43
|
+
return doc('CORSConfiguration', rules.map((r) => x('CORSRule', [
|
|
44
|
+
...(r.id ? [x('ID', r.id)] : []), ...r.allowedOrigins.map((o) => x('AllowedOrigin', o)), ...r.allowedMethods.map((m) => x('AllowedMethod', m)),
|
|
45
|
+
...r.allowedHeaders.map((h) => x('AllowedHeader', h)), ...r.exposeHeaders.map((h) => x('ExposeHeader', h)), ...(r.maxAgeSeconds !== undefined ? [x('MaxAgeSeconds', r.maxAgeSeconds)] : []),
|
|
46
|
+
])));
|
|
47
|
+
}
|
|
48
|
+
function wildMatch(pattern, value, fold) {
|
|
49
|
+
const p = fold ? pattern.toLowerCase() : pattern;
|
|
50
|
+
const v = fold ? value.toLowerCase() : value;
|
|
51
|
+
const star = p.indexOf('*');
|
|
52
|
+
if (star < 0)
|
|
53
|
+
return p === v;
|
|
54
|
+
return v.length >= p.length - 1 && v.startsWith(p.slice(0, star)) && v.endsWith(p.slice(star + 1));
|
|
55
|
+
}
|
|
56
|
+
/** The first rule that allows the origin, the method and the headers a preflight asks for. */
|
|
57
|
+
export function corsRuleFor(rules, origin, method, requested) {
|
|
58
|
+
return rules.find((r) => r.allowedOrigins.some((o) => wildMatch(o, origin, false))
|
|
59
|
+
&& r.allowedMethods.some((m) => m === '*' || m.toUpperCase() === method.toUpperCase())
|
|
60
|
+
&& requested.every((h) => r.allowedHeaders.some((a) => wildMatch(a, h, true))));
|
|
61
|
+
}
|
|
62
|
+
/** The Access-Control-* headers a matching rule gives an answer ("AllowedOrigins: Specifies the value for the
|
|
63
|
+
* Access-Control-Allow-Origin header", and likewise for the methods, headers, exposed headers and max age). */
|
|
64
|
+
export function corsHeaders(rule, origin, requested, preflight) {
|
|
65
|
+
return {
|
|
66
|
+
'access-control-allow-origin': origin,
|
|
67
|
+
'access-control-allow-methods': rule.allowedMethods.join(', '),
|
|
68
|
+
vary: 'Origin, Access-Control-Request-Headers, Access-Control-Request-Method',
|
|
69
|
+
...(preflight && requested.length ? { 'access-control-allow-headers': requested.join(', ') } : {}),
|
|
70
|
+
...(preflight && rule.maxAgeSeconds !== undefined ? { 'access-control-max-age': String(rule.maxAgeSeconds) } : {}),
|
|
71
|
+
...(rule.exposeHeaders.length ? { 'access-control-expose-headers': rule.exposeHeaders.join(', ') } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export const requestedHeaders = (request) => (request.headers.get('access-control-request-headers') ?? '').split(',').map((h) => h.trim()).filter(Boolean);
|
|
75
|
+
export function parseLifecycle(xml) {
|
|
76
|
+
return [...xml.matchAll(/<Rule>([\s\S]*?)<\/Rule>/g)].map((m) => {
|
|
77
|
+
const b = m[1];
|
|
78
|
+
const expiration = /<Expiration>([\s\S]*?)<\/Expiration>/.exec(b)?.[1];
|
|
79
|
+
const abort = /<AbortIncompleteMultipartUpload>([\s\S]*?)<\/AbortIncompleteMultipartUpload>/.exec(b)?.[1];
|
|
80
|
+
const days = expiration ? oneOf(expiration, 'Days') : undefined;
|
|
81
|
+
const date = expiration ? oneOf(expiration, 'Date') : undefined;
|
|
82
|
+
const after = abort ? oneOf(abort, 'DaysAfterInitiation') : undefined;
|
|
83
|
+
const transitions = [...b.matchAll(/<Transition>([\s\S]*?)<\/Transition>/g)].map((x) => {
|
|
84
|
+
const d = oneOf(x[1], 'Days');
|
|
85
|
+
const on = oneOf(x[1], 'Date');
|
|
86
|
+
return { ...(d !== undefined ? { days: Number(d) } : {}), ...(on !== undefined ? { date: on } : {}), storageClass: oneOf(x[1], 'StorageClass') ?? '' };
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
id: oneOf(b, 'ID') ?? '', enabled: (oneOf(b, 'Status') ?? 'Enabled') === 'Enabled', prefix: oneOf(b, 'Prefix') ?? '', ...(/<Filter>/.test(b) ? {} : /<Prefix>/.test(b) ? { prefixOnRule: true } : { noPrefix: true }),
|
|
90
|
+
...(days ? { expireDays: Number(days) } : {}), ...(date ? { expireDate: date } : {}), ...(after ? { abortMultipartDays: Number(after) } : {}), ...(transitions.length ? { transitions } : {}),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function lifecycleXml(rules) {
|
|
95
|
+
return doc('LifecycleConfiguration', rules.map((r) => x('Rule', [
|
|
96
|
+
x('ID', r.id), ...(r.noPrefix ? [] : [r.prefixOnRule ? x('Prefix', r.prefix) : x('Filter', [x('Prefix', r.prefix)])]), x('Status', r.enabled ? 'Enabled' : 'Disabled'),
|
|
97
|
+
...(r.transitions ?? []).map((tr) => x('Transition', [...(tr.date !== undefined ? [x('Date', tr.date)] : [x('Days', tr.days ?? 0)]), x('StorageClass', tr.storageClass)])),
|
|
98
|
+
...(r.expireDays !== undefined ? [x('Expiration', [x('Days', r.expireDays)])] : r.expireDate !== undefined ? [x('Expiration', [x('Date', r.expireDate)])] : []),
|
|
99
|
+
...(r.abortMultipartDays !== undefined ? [x('AbortIncompleteMultipartUpload', [x('DaysAfterInitiation', r.abortMultipartDays)])] : []),
|
|
100
|
+
])));
|
|
101
|
+
}
|
|
102
|
+
// ── bodies ───────────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
export function parseDelete(xml) {
|
|
104
|
+
return { keys: [...xml.matchAll(/<Object>([\s\S]*?)<\/Object>/g)].map((m) => oneOf(m[1], 'Key') ?? ''), quiet: /<Quiet>\s*true\s*<\/Quiet>/i.test(xml) };
|
|
105
|
+
}
|
|
106
|
+
export const locationConstraintOf = (xml) => oneOf(xml, 'LocationConstraint');
|
|
107
|
+
export const md5Etag = (bytes) => `"${createHash('md5').update(bytes).digest('hex')}"`;
|
|
108
|
+
/** A request's body as the bytes it carried. The derived dispatch reads a body as text to find its parameters, which no
|
|
109
|
+
* binary object survives, so the lane's fetch reads the bytes first and keeps them here for the handler. */
|
|
110
|
+
const BODIES = new WeakMap();
|
|
111
|
+
export async function withBytes(request) {
|
|
112
|
+
if (request.method === 'GET' || request.method === 'HEAD' || request.method === 'OPTIONS')
|
|
113
|
+
return request;
|
|
114
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
115
|
+
const kept = new Request(request.url, { method: request.method, headers: request.headers, body: bytes });
|
|
116
|
+
BODIES.set(kept, bytes);
|
|
117
|
+
return kept;
|
|
118
|
+
}
|
|
119
|
+
export const bytesOf = (request) => BODIES.get(request) ?? new Uint8Array(0);
|
|
120
|
+
/** Concatenate exact wire bytes; no text conversion is involved in multipart assembly. */
|
|
121
|
+
export function concatBytes(parts) {
|
|
122
|
+
const out = new Uint8Array(parts.reduce((size, part) => size + part.byteLength, 0));
|
|
123
|
+
let offset = 0;
|
|
124
|
+
for (const part of parts) {
|
|
125
|
+
out.set(part, offset);
|
|
126
|
+
offset += part.byteLength;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/** S3's base64 digest; CRC bytes are big-endian on the wire. No streaming framing is decoded here. */
|
|
131
|
+
export function checksum(algorithm, bytes) {
|
|
132
|
+
if (algorithm === 'MD5' || algorithm === 'SHA1' || algorithm === 'SHA256') {
|
|
133
|
+
return createHash(algorithm.toLowerCase()).update(bytes).digest('base64');
|
|
134
|
+
}
|
|
135
|
+
if (algorithm === 'CRC64NVME') {
|
|
136
|
+
// AWS's reflected NVME polynomial: aws-checksums/tests/crc64_test.c.
|
|
137
|
+
let crc = 0xffffffffffffffffn;
|
|
138
|
+
for (const byte of bytes) {
|
|
139
|
+
crc ^= BigInt(byte);
|
|
140
|
+
for (let bit = 0; bit < 8; bit++)
|
|
141
|
+
crc = (crc >> 1n) ^ (crc & 1n ? 0x9a6c9329ac4bc9b5n : 0n);
|
|
142
|
+
}
|
|
143
|
+
const out = new Uint8Array(8);
|
|
144
|
+
new DataView(out.buffer).setBigUint64(0, crc ^ 0xffffffffffffffffn, false);
|
|
145
|
+
return Buffer.from(out).toString('base64');
|
|
146
|
+
}
|
|
147
|
+
const polynomial = algorithm === 'CRC32C' ? 0x82f63b78 : 0xedb88320;
|
|
148
|
+
let crc = 0xffffffff;
|
|
149
|
+
for (const byte of bytes) {
|
|
150
|
+
crc ^= byte;
|
|
151
|
+
for (let bit = 0; bit < 8; bit++)
|
|
152
|
+
crc = (crc >>> 1) ^ (crc & 1 ? polynomial : 0);
|
|
153
|
+
}
|
|
154
|
+
const out = new Uint8Array(4);
|
|
155
|
+
new DataView(out.buffer).setUint32(0, (crc ^ 0xffffffff) >>> 0, false);
|
|
156
|
+
return Buffer.from(out).toString('base64');
|
|
157
|
+
}
|
package/dist/src/scenario.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export type PackScenarioAdapter<Req> = {
|
|
|
19
19
|
* This is ALSO where a pack refuses success-shaped handlers on stateful routes
|
|
20
20
|
* ("seed that through the vendor's API instead"). */
|
|
21
21
|
validateRespond?: (respond: unknown, handler: ScenarioHandler) => string | null;
|
|
22
|
+
/** Whether the request admits a handler's answer (OpenAI's `tool_choice: 'none'` admits no tool call): a handler whose
|
|
23
|
+
* answer it does not admit is passed over, and the next that matches answers, else the stub. Absent, every answer. */
|
|
24
|
+
admits?: (req: Req, respond: unknown) => boolean;
|
|
22
25
|
/** The request's text corpus for `textPattern` extractors (packs with text requests). */
|
|
23
26
|
text?: (req: Req) => string;
|
|
24
27
|
/** Pack-defined extractor KINDS beyond the builtins (feature, textPattern). A pack kind with
|
package/dist/src/scenario.js
CHANGED
|
@@ -231,6 +231,8 @@ export class ScenarioEngine {
|
|
|
231
231
|
continue;
|
|
232
232
|
if (!this.matches(r.handler, req, scope))
|
|
233
233
|
continue;
|
|
234
|
+
if ("respond" in r.handler && this.adapter.admits && !this.adapter.admits(req, r.handler.respond))
|
|
235
|
+
continue;
|
|
234
236
|
if (r.handler.once)
|
|
235
237
|
scope.onceFired.add(r.id);
|
|
236
238
|
if (r.handler.advancePhase !== undefined)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sampleFromSchema(schema: unknown, name?: string): unknown;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function sampleFromSchema(schema, name = 'value') {
|
|
2
|
+
const s = (schema && typeof schema === 'object' ? schema : {});
|
|
3
|
+
if (Array.isArray(s.enum) && s.enum.length)
|
|
4
|
+
return s.enum[0];
|
|
5
|
+
if (s.const !== undefined)
|
|
6
|
+
return s.const;
|
|
7
|
+
for (const k of ['anyOf', 'oneOf', 'allOf'])
|
|
8
|
+
if (Array.isArray(s[k]) && s[k].length)
|
|
9
|
+
return sampleFromSchema(s[k][0], name);
|
|
10
|
+
const raw = Array.isArray(s.type) ? s.type.find((t) => String(t).toLowerCase() !== 'null') : s.type;
|
|
11
|
+
const type = typeof raw === 'string' ? raw.toLowerCase() : s.properties ? 'object' : s.items ? 'array' : undefined;
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'object': return Object.fromEntries(Object.entries(s.properties ?? {}).map(([k, v]) => [k, sampleFromSchema(v, k)]));
|
|
14
|
+
case 'array': return [sampleFromSchema(s.items, name)];
|
|
15
|
+
case 'integer':
|
|
16
|
+
case 'number': return typeof s.minimum === 'number' ? s.minimum : 0;
|
|
17
|
+
case 'boolean': return false;
|
|
18
|
+
case 'null': return null;
|
|
19
|
+
default: return s.format === 'date-time' ? '2026-01-01T00:00:00Z' : s.format === 'email' ? 'twin@example.com' : s.format === 'uri' ? 'https://example.com/' : `twin-stub ${name}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A key pair from a seed: the secret key is SHA-256 of the seed (X25519 clamps it), the public key its X25519 point. */
|
|
2
|
+
export declare function sealedBoxKeyPair(seed: string): {
|
|
3
|
+
publicKey: Uint8Array;
|
|
4
|
+
secretKey: Uint8Array;
|
|
5
|
+
};
|
|
6
|
+
/** A sealed box opened with the recipient's key pair: the plaintext, or undefined when it is not one for this key (its
|
|
7
|
+
* MAC does not hold). */
|
|
8
|
+
export declare function sealedBoxOpen(sealed: Uint8Array, keyPair: {
|
|
9
|
+
publicKey: Uint8Array;
|
|
10
|
+
secretKey: Uint8Array;
|
|
11
|
+
}): Uint8Array | undefined;
|
|
12
|
+
/** A sealed box for a recipient's public key, as a client makes one (`gh secret set`, libsodium's `crypto_box_seal`): a
|
|
13
|
+
* fresh ephemeral key pair from `seed` (random when none is given), then `ephemeral_pk || mac || ciphertext`. */
|
|
14
|
+
export declare function sealedBoxSeal(message: Uint8Array, recipientPublicKey: Uint8Array, seed?: string): Uint8Array;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// SEALED BOXES — libsodium's `crypto_box_seal` opened, for a vendor whose API takes a secret encrypted to a public key it
|
|
2
|
+
// published (GitHub's Actions secrets: "Secrets must be encrypted using the libsodium sealed box", with the repository's
|
|
3
|
+
// public key; `gh secret set` does it). The twin holds the key pair, derived from a seed, so it can decrypt what its
|
|
4
|
+
// runner must receive, as the vendor does. Everything here is deterministic: X25519 from Node, and HSalsa20, XSalsa20,
|
|
5
|
+
// Poly1305 and BLAKE2b written from their specifications (the kernel takes no dependency for them: B1).
|
|
6
|
+
//
|
|
7
|
+
// A sealed box is `ephemeral_pk (32) || mac (16) || ciphertext`; its nonce is BLAKE2b-192(ephemeral_pk || recipient_pk),
|
|
8
|
+
// its key crypto_box_beforenm(ephemeral_pk, recipient_sk) = HSalsa20(X25519(sk, epk), 0), and its body XSalsa20-Poly1305
|
|
9
|
+
// (https://doc.libsodium.org/public-key_cryptography/sealed_boxes, "Algorithm details").
|
|
10
|
+
// a namespace, never named imports: signing.ts reaches this from the kernel a browser client bundles, and the browser's
|
|
11
|
+
// `node:crypto` polyfill has no X25519 (signing.ts, the same rule)
|
|
12
|
+
import * as nodeCrypto from 'node:crypto';
|
|
13
|
+
const PKCS8_X25519 = Buffer.from('302e020100300506032b656e04220420', 'hex');
|
|
14
|
+
const SPKI_X25519 = Buffer.from('302a300506032b656e032100', 'hex');
|
|
15
|
+
/** A key pair from a seed: the secret key is SHA-256 of the seed (X25519 clamps it), the public key its X25519 point. */
|
|
16
|
+
export function sealedBoxKeyPair(seed) {
|
|
17
|
+
const secretKey = new Uint8Array(nodeCrypto.createHash('sha256').update(`sealed-box:${seed}`).digest());
|
|
18
|
+
const priv = nodeCrypto.createPrivateKey({ key: Buffer.concat([PKCS8_X25519, secretKey]), format: 'der', type: 'pkcs8' });
|
|
19
|
+
const spki = nodeCrypto.createPublicKey(priv).export({ type: 'spki', format: 'der' });
|
|
20
|
+
return { publicKey: new Uint8Array(spki.subarray(spki.length - 32)), secretKey };
|
|
21
|
+
}
|
|
22
|
+
function x25519(secretKey, publicKey) {
|
|
23
|
+
const privateKey = nodeCrypto.createPrivateKey({ key: Buffer.concat([PKCS8_X25519, secretKey]), format: 'der', type: 'pkcs8' });
|
|
24
|
+
const pub = nodeCrypto.createPublicKey({ key: Buffer.concat([SPKI_X25519, publicKey]), format: 'der', type: 'spki' });
|
|
25
|
+
return new Uint8Array(nodeCrypto.diffieHellman({ privateKey, publicKey: pub }));
|
|
26
|
+
}
|
|
27
|
+
// ── Salsa20 (https://cr.yp.to/snuffle/spec.pdf) ─────────────────────────────────────────────
|
|
28
|
+
const rotl = (v, c) => ((v << c) | (v >>> (32 - c))) >>> 0;
|
|
29
|
+
const u32 = (b, i) => (b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)) >>> 0;
|
|
30
|
+
const SIGMA = new TextEncoder().encode('expand 32-byte k');
|
|
31
|
+
/** The Salsa20 state for a key, a 16-byte input (nonce and counter, or HSalsa20's nonce) and the constants. */
|
|
32
|
+
function state(key, input) {
|
|
33
|
+
const s = new Uint32Array(16);
|
|
34
|
+
s[0] = u32(SIGMA, 0);
|
|
35
|
+
s[5] = u32(SIGMA, 4);
|
|
36
|
+
s[10] = u32(SIGMA, 8);
|
|
37
|
+
s[15] = u32(SIGMA, 12);
|
|
38
|
+
for (let i = 0; i < 4; i++) {
|
|
39
|
+
s[1 + i] = u32(key, i * 4);
|
|
40
|
+
s[11 + i] = u32(key, 16 + i * 4);
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < 4; i++)
|
|
43
|
+
s[6 + i] = u32(input, i * 4);
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
/** Twenty rounds (ten double rounds) over a state, in place. */
|
|
47
|
+
function rounds(x) {
|
|
48
|
+
for (let i = 0; i < 10; i++) {
|
|
49
|
+
x[4] ^= rotl((x[0] + x[12]) >>> 0, 7);
|
|
50
|
+
x[8] ^= rotl((x[4] + x[0]) >>> 0, 9);
|
|
51
|
+
x[12] ^= rotl((x[8] + x[4]) >>> 0, 13);
|
|
52
|
+
x[0] ^= rotl((x[12] + x[8]) >>> 0, 18);
|
|
53
|
+
x[9] ^= rotl((x[5] + x[1]) >>> 0, 7);
|
|
54
|
+
x[13] ^= rotl((x[9] + x[5]) >>> 0, 9);
|
|
55
|
+
x[1] ^= rotl((x[13] + x[9]) >>> 0, 13);
|
|
56
|
+
x[5] ^= rotl((x[1] + x[13]) >>> 0, 18);
|
|
57
|
+
x[14] ^= rotl((x[10] + x[6]) >>> 0, 7);
|
|
58
|
+
x[2] ^= rotl((x[14] + x[10]) >>> 0, 9);
|
|
59
|
+
x[6] ^= rotl((x[2] + x[14]) >>> 0, 13);
|
|
60
|
+
x[10] ^= rotl((x[6] + x[2]) >>> 0, 18);
|
|
61
|
+
x[3] ^= rotl((x[15] + x[11]) >>> 0, 7);
|
|
62
|
+
x[7] ^= rotl((x[3] + x[15]) >>> 0, 9);
|
|
63
|
+
x[11] ^= rotl((x[7] + x[3]) >>> 0, 13);
|
|
64
|
+
x[15] ^= rotl((x[11] + x[7]) >>> 0, 18);
|
|
65
|
+
x[1] ^= rotl((x[0] + x[3]) >>> 0, 7);
|
|
66
|
+
x[2] ^= rotl((x[1] + x[0]) >>> 0, 9);
|
|
67
|
+
x[3] ^= rotl((x[2] + x[1]) >>> 0, 13);
|
|
68
|
+
x[0] ^= rotl((x[3] + x[2]) >>> 0, 18);
|
|
69
|
+
x[6] ^= rotl((x[5] + x[4]) >>> 0, 7);
|
|
70
|
+
x[7] ^= rotl((x[6] + x[5]) >>> 0, 9);
|
|
71
|
+
x[4] ^= rotl((x[7] + x[6]) >>> 0, 13);
|
|
72
|
+
x[5] ^= rotl((x[4] + x[7]) >>> 0, 18);
|
|
73
|
+
x[11] ^= rotl((x[10] + x[9]) >>> 0, 7);
|
|
74
|
+
x[8] ^= rotl((x[11] + x[10]) >>> 0, 9);
|
|
75
|
+
x[9] ^= rotl((x[8] + x[11]) >>> 0, 13);
|
|
76
|
+
x[10] ^= rotl((x[9] + x[8]) >>> 0, 18);
|
|
77
|
+
x[12] ^= rotl((x[15] + x[14]) >>> 0, 7);
|
|
78
|
+
x[13] ^= rotl((x[12] + x[15]) >>> 0, 9);
|
|
79
|
+
x[14] ^= rotl((x[13] + x[12]) >>> 0, 13);
|
|
80
|
+
x[15] ^= rotl((x[14] + x[13]) >>> 0, 18);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const put32 = (out, at, v) => { out[at] = v & 255; out[at + 1] = (v >>> 8) & 255; out[at + 2] = (v >>> 16) & 255; out[at + 3] = (v >>> 24) & 255; };
|
|
84
|
+
/** HSalsa20: a 32-byte subkey from a key and a 16-byte nonce (the rounds' words 0, 5, 10, 15, 6, 7, 8, 9, not added). */
|
|
85
|
+
function hsalsa20(key, nonce16) {
|
|
86
|
+
const x = state(key, nonce16);
|
|
87
|
+
rounds(x);
|
|
88
|
+
const out = new Uint8Array(32);
|
|
89
|
+
[0, 5, 10, 15, 6, 7, 8, 9].forEach((w, i) => put32(out, i * 4, x[w]));
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** The XSalsa20 keystream of `length` bytes for a key and a 24-byte nonce. */
|
|
93
|
+
function xsalsa20Stream(key, nonce24, length) {
|
|
94
|
+
const sub = hsalsa20(key, nonce24.subarray(0, 16));
|
|
95
|
+
const out = new Uint8Array(length);
|
|
96
|
+
const input = new Uint8Array(16);
|
|
97
|
+
input.set(nonce24.subarray(16, 24), 0);
|
|
98
|
+
for (let block = 0, at = 0; at < length; block++, at += 64) {
|
|
99
|
+
put32(input, 8, block >>> 0);
|
|
100
|
+
put32(input, 12, Math.floor(block / 0x100000000) >>> 0);
|
|
101
|
+
const s = state(sub, input);
|
|
102
|
+
const x = s.slice();
|
|
103
|
+
rounds(x);
|
|
104
|
+
const bytes = new Uint8Array(64);
|
|
105
|
+
for (let i = 0; i < 16; i++)
|
|
106
|
+
put32(bytes, i * 4, (x[i] + s[i]) >>> 0);
|
|
107
|
+
out.set(bytes.subarray(0, Math.min(64, length - at)), at);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
// ── Poly1305 (RFC 8439 section 2.5) ─────────────────────────────────────────────────────────
|
|
112
|
+
function poly1305(key, message) {
|
|
113
|
+
const le = (b) => b.reduceRight((acc, v) => (acc << 8n) | BigInt(v), 0n);
|
|
114
|
+
const r = le(key.subarray(0, 16)) & 0x0ffffffc0ffffffc0ffffffc0fffffffn;
|
|
115
|
+
const s = le(key.subarray(16, 32));
|
|
116
|
+
const p = (1n << 130n) - 5n;
|
|
117
|
+
let acc = 0n;
|
|
118
|
+
for (let i = 0; i < message.length; i += 16) {
|
|
119
|
+
const chunk = message.subarray(i, i + 16);
|
|
120
|
+
const n = le(chunk) + (1n << BigInt(8 * chunk.length));
|
|
121
|
+
acc = ((acc + n) * r) % p;
|
|
122
|
+
}
|
|
123
|
+
acc = (acc + s) & ((1n << 128n) - 1n);
|
|
124
|
+
const out = new Uint8Array(16);
|
|
125
|
+
for (let i = 0; i < 16; i++) {
|
|
126
|
+
out[i] = Number(acc & 255n);
|
|
127
|
+
acc >>= 8n;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
// ── BLAKE2b (RFC 7693), for the sealed box's 24-byte nonce ────────────────────────────────────
|
|
132
|
+
const IV = [0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n, 0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n];
|
|
133
|
+
const SIGMA_B = [
|
|
134
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
|
|
135
|
+
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
|
|
136
|
+
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
|
|
137
|
+
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
|
|
138
|
+
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
|
|
139
|
+
];
|
|
140
|
+
const M64 = (1n << 64n) - 1n;
|
|
141
|
+
const rotr64 = (v, c) => ((v >> c) | (v << (64n - c))) & M64;
|
|
142
|
+
function blake2b(input, outlen) {
|
|
143
|
+
const h = IV.slice();
|
|
144
|
+
h[0] ^= 0x01010000n ^ BigInt(outlen);
|
|
145
|
+
const blocks = Math.max(1, Math.ceil(input.length / 128));
|
|
146
|
+
for (let b = 0; b < blocks; b++) {
|
|
147
|
+
const chunk = new Uint8Array(128);
|
|
148
|
+
chunk.set(input.subarray(b * 128, b * 128 + 128));
|
|
149
|
+
const m = [];
|
|
150
|
+
for (let i = 0; i < 16; i++) {
|
|
151
|
+
let w = 0n;
|
|
152
|
+
for (let j = 7; j >= 0; j--)
|
|
153
|
+
w = (w << 8n) | BigInt(chunk[i * 8 + j]);
|
|
154
|
+
m.push(w);
|
|
155
|
+
}
|
|
156
|
+
const last = b === blocks - 1;
|
|
157
|
+
const t = BigInt(last ? input.length : (b + 1) * 128);
|
|
158
|
+
const v = [...h, ...IV];
|
|
159
|
+
v[12] ^= t & M64;
|
|
160
|
+
v[13] ^= t >> 64n;
|
|
161
|
+
if (last)
|
|
162
|
+
v[14] ^= M64;
|
|
163
|
+
const g = (a, bb, c, d, x, y) => {
|
|
164
|
+
v[a] = (v[a] + v[bb] + x) & M64;
|
|
165
|
+
v[d] = rotr64(v[d] ^ v[a], 32n);
|
|
166
|
+
v[c] = (v[c] + v[d]) & M64;
|
|
167
|
+
v[bb] = rotr64(v[bb] ^ v[c], 24n);
|
|
168
|
+
v[a] = (v[a] + v[bb] + y) & M64;
|
|
169
|
+
v[d] = rotr64(v[d] ^ v[a], 16n);
|
|
170
|
+
v[c] = (v[c] + v[d]) & M64;
|
|
171
|
+
v[bb] = rotr64(v[bb] ^ v[c], 63n);
|
|
172
|
+
};
|
|
173
|
+
for (let r = 0; r < 12; r++) {
|
|
174
|
+
const s = SIGMA_B[r % 10];
|
|
175
|
+
g(0, 4, 8, 12, m[s[0]], m[s[1]]);
|
|
176
|
+
g(1, 5, 9, 13, m[s[2]], m[s[3]]);
|
|
177
|
+
g(2, 6, 10, 14, m[s[4]], m[s[5]]);
|
|
178
|
+
g(3, 7, 11, 15, m[s[6]], m[s[7]]);
|
|
179
|
+
g(0, 5, 10, 15, m[s[8]], m[s[9]]);
|
|
180
|
+
g(1, 6, 11, 12, m[s[10]], m[s[11]]);
|
|
181
|
+
g(2, 7, 8, 13, m[s[12]], m[s[13]]);
|
|
182
|
+
g(3, 4, 9, 14, m[s[14]], m[s[15]]);
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < 8; i++)
|
|
185
|
+
h[i] = h[i] ^ v[i] ^ v[i + 8];
|
|
186
|
+
}
|
|
187
|
+
const out = new Uint8Array(outlen);
|
|
188
|
+
for (let i = 0; i < outlen; i++)
|
|
189
|
+
out[i] = Number((h[i >> 3] >> BigInt(8 * (i & 7))) & 255n);
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
/** A sealed box opened with the recipient's key pair: the plaintext, or undefined when it is not one for this key (its
|
|
193
|
+
* MAC does not hold). */
|
|
194
|
+
export function sealedBoxOpen(sealed, keyPair) {
|
|
195
|
+
if (sealed.length < 48)
|
|
196
|
+
return undefined;
|
|
197
|
+
const epk = sealed.subarray(0, 32);
|
|
198
|
+
const mac = sealed.subarray(32, 48);
|
|
199
|
+
const body = sealed.subarray(48);
|
|
200
|
+
const nonce = blake2b(new Uint8Array([...epk, ...keyPair.publicKey]), 24);
|
|
201
|
+
const key = hsalsa20(x25519(keyPair.secretKey, epk), new Uint8Array(16));
|
|
202
|
+
const stream = xsalsa20Stream(key, nonce, 32 + body.length);
|
|
203
|
+
const expected = poly1305(stream.subarray(0, 32), body);
|
|
204
|
+
let diff = 0;
|
|
205
|
+
for (let i = 0; i < 16; i++)
|
|
206
|
+
diff |= expected[i] ^ mac[i];
|
|
207
|
+
if (diff !== 0)
|
|
208
|
+
return undefined;
|
|
209
|
+
const out = new Uint8Array(body.length);
|
|
210
|
+
for (let i = 0; i < body.length; i++)
|
|
211
|
+
out[i] = body[i] ^ stream[32 + i];
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
/** A sealed box for a recipient's public key, as a client makes one (`gh secret set`, libsodium's `crypto_box_seal`): a
|
|
215
|
+
* fresh ephemeral key pair from `seed` (random when none is given), then `ephemeral_pk || mac || ciphertext`. */
|
|
216
|
+
export function sealedBoxSeal(message, recipientPublicKey, seed) {
|
|
217
|
+
const ephemeral = sealedBoxKeyPair(seed ?? nodeCrypto.randomBytes(32).toString('hex'));
|
|
218
|
+
const nonce = blake2b(new Uint8Array([...ephemeral.publicKey, ...recipientPublicKey]), 24);
|
|
219
|
+
const key = hsalsa20(x25519(ephemeral.secretKey, recipientPublicKey), new Uint8Array(16));
|
|
220
|
+
const stream = xsalsa20Stream(key, nonce, 32 + message.length);
|
|
221
|
+
const body = new Uint8Array(message.length);
|
|
222
|
+
for (let i = 0; i < message.length; i++)
|
|
223
|
+
body[i] = message[i] ^ stream[32 + i];
|
|
224
|
+
return new Uint8Array([...ephemeral.publicKey, ...poly1305(stream.subarray(0, 32), body), ...body]);
|
|
225
|
+
}
|