@volter/world-core 3.0.37 → 3.0.39
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/app-route.cjs +26 -25
- package/app-route.d.cts +1 -0
- package/dist/app-route.cjs +26 -25
- package/dist/app-route.d.cts +1 -0
- package/dist/generated/pack-facts.json +409 -352
- package/dist/inject.cjs +474 -95
- package/dist/network-policy.cjs +108 -13
- package/dist/network-policy.d.cts +8 -3
- package/dist/src/actions.js +2 -1
- package/dist/src/anthropic-wire.d.ts +2 -0
- package/dist/src/anthropic-wire.js +5 -0
- package/dist/src/attestation.d.ts +6 -0
- package/dist/src/attestation.js +27 -0
- package/dist/src/bytes.d.ts +3 -0
- package/dist/src/bytes.js +17 -0
- package/dist/src/changeset.js +2 -1
- package/dist/src/clickhouse/sql.js +73 -14
- package/dist/src/derived-core.d.ts +210 -32
- package/dist/src/derived-core.js +510 -244
- package/dist/src/derived-real.d.ts +1 -1
- package/dist/src/derived-real.js +704 -93
- package/dist/src/derived.d.ts +18 -2
- package/dist/src/derived.js +21 -2
- package/dist/src/events.d.ts +28 -45
- package/dist/src/events.js +40 -58
- package/dist/src/exact-json.d.ts +10 -0
- package/dist/src/exact-json.js +98 -0
- package/dist/src/executor.d.ts +17 -0
- package/dist/src/executor.js +91 -30
- package/dist/src/file-response.js +1 -1
- package/dist/src/form.d.ts +8 -0
- package/dist/src/form.js +76 -0
- package/dist/src/git/format.d.ts +16 -0
- package/dist/src/git/format.js +110 -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 +1 -1
- package/dist/src/git/lfs.js +4 -3
- package/dist/src/graphql-wire.d.ts +6 -0
- package/dist/src/graphql-wire.js +6 -3
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/head.d.ts +1 -0
- package/dist/src/head.js +15 -4
- package/dist/src/held-reads.d.ts +6 -0
- package/dist/src/held-reads.js +60 -0
- package/dist/src/history.js +5 -4
- package/dist/src/host-port.d.ts +5 -0
- package/dist/src/host-port.js +14 -0
- package/dist/src/index.d.ts +11 -3
- package/dist/src/index.js +9 -4
- package/dist/src/log.js +12 -7
- package/dist/src/machines.d.ts +81 -5
- package/dist/src/machines.js +125 -9
- package/dist/src/managed-database.d.ts +15 -11
- package/dist/src/managed-database.js +38 -37
- package/dist/src/multipart.d.ts +21 -3
- package/dist/src/multipart.js +112 -38
- package/dist/src/observe.d.ts +3 -0
- package/dist/src/observe.js +6 -2
- package/dist/src/openai-wire.d.ts +16 -0
- package/dist/src/openai-wire.js +43 -12
- package/dist/src/pack-fetch.d.ts +4 -6
- package/dist/src/pack-fetch.js +189 -57
- package/dist/src/packRegistry.d.ts +7 -1
- package/dist/src/packRegistry.js +20 -3
- package/dist/src/private-transport.d.ts +1 -0
- package/dist/src/private-transport.js +11 -0
- package/dist/src/protobuf.js +10 -4
- package/dist/src/rateBudget.d.ts +8 -1
- package/dist/src/rateBudget.js +1 -0
- package/dist/src/redis/engine.d.ts +14 -5
- package/dist/src/redis/engine.js +271 -72
- package/dist/src/redis/frames.d.ts +17 -0
- package/dist/src/redis/frames.js +123 -0
- package/dist/src/redis/index.d.ts +6 -0
- package/dist/src/redis/index.js +6 -0
- package/dist/src/redis/json.d.ts +2 -0
- package/dist/src/redis/json.js +14 -0
- package/dist/src/redis/lua-libs.d.ts +4 -0
- package/dist/src/redis/lua-libs.js +173 -0
- package/dist/src/redis/lua.d.ts +1 -0
- package/dist/src/redis/lua.js +36 -0
- package/dist/src/redis/member-storage.d.ts +5 -0
- package/dist/src/redis/member-storage.js +75 -0
- package/dist/src/redis/resp2.d.ts +4 -0
- package/dist/src/redis/resp2.js +29 -0
- package/dist/src/redis/stream.d.ts +14 -0
- package/dist/src/redis/stream.js +335 -0
- package/dist/src/remote-execute.d.ts +2 -2
- package/dist/src/request-body.d.ts +11 -0
- package/dist/src/request-body.js +11 -0
- package/dist/src/runtime.d.ts +6 -5
- package/dist/src/runtime.js +5 -5
- package/dist/src/s3/wire.d.ts +4 -4
- package/dist/src/s3/wire.js +5 -3
- package/dist/src/scenario.d.ts +22 -0
- package/dist/src/scenario.js +35 -31
- package/dist/src/serve-http.d.ts +2 -1
- package/dist/src/serve-http.js +10 -1
- package/dist/src/serve.js +7 -4
- package/dist/src/signing.d.ts +23 -5
- package/dist/src/signing.js +74 -11
- package/dist/src/sigv4.d.ts +3 -1
- package/dist/src/sigv4.js +4 -2
- package/dist/src/smtp.js +2 -1
- package/dist/src/sockets.js +8 -3
- package/dist/src/state-system.d.ts +3 -0
- package/dist/src/storage.js +5 -2
- package/dist/src/twin-fetch.d.ts +3 -1
- package/dist/src/twin-fetch.js +3 -2
- package/dist/src/vendor-call.d.ts +10 -3
- package/dist/src/vendor-call.js +58 -14
- package/dist/test-fixtures/attestation.SOURCE.md +1 -0
- package/dist/test-fixtures/attestation.json +50 -0
- package/generated/pack-facts.json +409 -352
- package/inject.cjs +474 -95
- package/network-policy.cjs +108 -13
- package/network-policy.d.cts +8 -3
- package/package.json +11 -4
- package/src/actions.ts +2 -1
- package/src/anthropic-wire.ts +6 -0
- package/src/attestation.ts +23 -0
- package/src/bytes.ts +21 -0
- package/src/changeset.ts +2 -1
- package/src/clickhouse/sql.ts +50 -16
- package/src/derived-core.ts +573 -249
- package/src/derived-real.ts +552 -42
- package/src/derived.ts +23 -4
- package/src/events.ts +54 -85
- package/src/exact-json.ts +71 -0
- package/src/executor.ts +86 -27
- package/src/file-response.ts +1 -1
- package/src/form.ts +66 -0
- package/src/git/format.ts +89 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +5 -4
- package/src/graphql-wire.ts +8 -3
- package/src/grpc-wire.ts +2 -1
- package/src/head.ts +15 -5
- package/src/held-reads.ts +54 -0
- package/src/history.ts +5 -4
- package/src/host-port.ts +9 -0
- package/src/index.ts +10 -4
- package/src/log.ts +11 -7
- package/src/machines.ts +135 -13
- package/src/managed-database.ts +48 -40
- package/src/multipart.ts +110 -35
- package/src/observe.ts +6 -3
- package/src/openai-wire.ts +45 -14
- package/src/pack-fetch.ts +173 -50
- package/src/packRegistry.ts +27 -4
- package/src/private-transport.ts +8 -0
- package/src/protobuf.ts +8 -4
- package/src/rateBudget.ts +6 -1
- package/src/redis/engine.ts +168 -63
- package/src/redis/frames.ts +53 -0
- package/src/redis/index.ts +6 -0
- package/src/redis/json.ts +13 -0
- package/src/redis/lua-libs.ts +62 -0
- package/src/redis/lua.ts +20 -0
- package/src/redis/member-storage.ts +58 -0
- package/src/redis/resp2.ts +25 -0
- package/src/redis/stream.ts +102 -0
- package/src/remote-execute.ts +2 -2
- package/src/request-body.ts +12 -0
- package/src/runtime.ts +6 -5
- package/src/s3/wire.ts +8 -5
- package/src/scenario.ts +34 -13
- package/src/serve-http.ts +11 -2
- package/src/serve.ts +4 -3
- package/src/signing.ts +71 -17
- package/src/sigv4.ts +5 -3
- package/src/smtp.ts +3 -2
- package/src/sockets.ts +8 -3
- package/src/state-system.ts +3 -0
- package/src/storage.ts +5 -2
- package/src/twin-fetch.ts +4 -3
- package/src/vendor-call.ts +59 -12
- package/test-fixtures/attestation.json +50 -0
package/src/serve.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseExactJson } from './exact-json.ts';
|
|
1
2
|
// Twin serve layer (the twins architecture notes, increment 1): answer vendor-shaped
|
|
2
3
|
// READS from the local event-sourced state — the "local twin" read path. An app
|
|
3
4
|
// or agent points at this instead of the real vendor; it responds from local
|
|
@@ -126,7 +127,7 @@ export function readTwinRequestJournal(service: string, root?: string, options:
|
|
|
126
127
|
const parse = (file: string): TwinRequestJournalEntry[] => {
|
|
127
128
|
const text = getActiveWorldStore().read(file); if (!text) return [];
|
|
128
129
|
const rows: TwinRequestJournalEntry[] = [];
|
|
129
|
-
for (const line of text.split('\n')) { if (!line) continue; try { rows.push(
|
|
130
|
+
for (const line of text.split('\n')) { if (!line) continue; try { rows.push(parseExactJson(line) as TwinRequestJournalEntry); } catch { /* a torn line */ } }
|
|
130
131
|
return rows;
|
|
131
132
|
};
|
|
132
133
|
const kept: TwinRequestJournalEntry[][] = [parse(path)];
|
|
@@ -168,7 +169,7 @@ export function readTwinRequestJournalFrom(service: string, root: string | undef
|
|
|
168
169
|
const text = store.readRange ? store.readRange(file, start) : ((t) => (t === null ? null : Buffer.from(t, 'utf8').subarray(start).toString('utf8')))(store.read(file));
|
|
169
170
|
if (!text) return start;
|
|
170
171
|
const end = whole ? text.length : text.lastIndexOf('\n') + 1;
|
|
171
|
-
for (const line of text.slice(0, end).split('\n')) { if (!line) continue; try { entries.push(
|
|
172
|
+
for (const line of text.slice(0, end).split('\n')) { if (!line) continue; try { entries.push(parseExactJson(line) as TwinRequestJournalEntry); } catch { /* a torn line */ } }
|
|
172
173
|
return start + Buffer.byteLength(text.slice(0, end), 'utf8');
|
|
173
174
|
};
|
|
174
175
|
if (cursor === null) {
|
|
@@ -414,7 +415,7 @@ function journalingFetch(config: Record<string | symbol, unknown> & { fetch: (..
|
|
|
414
415
|
try {
|
|
415
416
|
const response = (await identitySlot().run(slot, () => inner.call(this, request, server))) as Response | undefined;
|
|
416
417
|
if (response instanceof Response) status = response.status;
|
|
417
|
-
else if (response === undefined) return response; //
|
|
418
|
+
else if (response === undefined) { status = 101; return response; } // accepted WebSocket upgrade
|
|
418
419
|
return response;
|
|
419
420
|
} finally {
|
|
420
421
|
// Only an OBSERVED identity is cached: it is the twin's own words. A fallback is
|
package/src/signing.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { parseExactJson, compareExactNumbers } from './exact-json.ts';
|
|
1
2
|
// SIGNING ON THE CONTEXT (docs/contributing/architecture.md, "What an author writes, and how": the tokens-and-keys row)
|
|
2
3
|
// — one deterministic implementation of what vendors sign with, given to handlers as `ctx.crypto`, so no pack reaches
|
|
3
|
-
// `node:crypto` itself: JWTs (RS256 with a pack's instance key, HS256 with a shared secret), their verification, a
|
|
4
|
+
// `node:crypto` itself: JWTs (RS256 or EdDSA with a pack's instance key, HS256 with a shared secret), their verification, a
|
|
4
5
|
// JWKS for a public key, an HMAC and a SHA-256. Nothing here is random: a key is the pack's data (a PEM it declares),
|
|
5
6
|
// and every value a signature carries comes from the call.
|
|
6
7
|
// A namespace, never named imports: a browser client bundles the kernel, and the browser's `node:crypto` polyfill has no
|
|
@@ -9,13 +10,14 @@ import * as nodeCrypto from 'node:crypto';
|
|
|
9
10
|
import { sealedBoxKeyPair, sealedBoxOpen } from './sealed-box.ts';
|
|
10
11
|
|
|
11
12
|
type Row = Record<string, unknown>;
|
|
12
|
-
export type Jwk = { kty: 'RSA'; use: 'sig'; alg: 'RS256'; kid: string; n: string; e: string };
|
|
13
|
+
export type Jwk = { kty: 'RSA'; use: 'sig'; alg: 'RS256'; kid: string; n: string; e: string } | { kty: 'OKP'; use: 'sig'; alg: 'EdDSA'; crv: 'Ed25519'; kid: string; x: string };
|
|
13
14
|
export type Jwks = { keys: Jwk[] };
|
|
14
|
-
/** A signing key: an RSA private key as a PEM (its public half, given or derived, is what a JWKS serves and
|
|
15
|
-
* `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs
|
|
16
|
-
|
|
15
|
+
/** A signing key: an RSA or Ed25519 private key as a PEM (its public half, given or derived, is what a JWKS serves and
|
|
16
|
+
* names by `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs
|
|
17
|
+
* none; Ed25519's is deterministic (RFC 8032), Better Auth's default (`EdDSA`). */
|
|
18
|
+
export type SigningKey = { alg: 'RS256' | 'RS384' | 'RS512' | 'EdDSA'; privatePem: string; publicPem?: string } | { alg: 'HS256' | 'HS384' | 'HS512'; secret: string };
|
|
17
19
|
/** The algorithms `jwtSign` signs with. */
|
|
18
|
-
export const SIGNING_ALGORITHMS: ReadonlyArray<SigningKey['alg']> = ['RS256', 'RS384', 'RS512', 'HS256', 'HS384', 'HS512'];
|
|
20
|
+
export const SIGNING_ALGORITHMS: ReadonlyArray<SigningKey['alg']> = ['RS256', 'RS384', 'RS512', 'EdDSA', 'HS256', 'HS384', 'HS512'];
|
|
19
21
|
const digestOf = (alg: string): string => `sha${alg.slice(2)}`;
|
|
20
22
|
/** Whether a token holds: its signature and its `exp`/`nbf` at `now` (seconds), with the payload when it does. */
|
|
21
23
|
export type JwtVerdict = { valid: boolean; payload?: Row; reason?: 'malformed' | 'unexpected_alg' | 'no_matching_key' | 'bad_signature' | 'expired' | 'not_yet_valid' };
|
|
@@ -31,14 +33,21 @@ export function keyId(publicPem: string, prefix = 'ins_'): string {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** A JWT: `iat` and `nbf` are `now` (seconds, the World's), `exp` `now` plus the lifetime (60 s when none is given),
|
|
34
|
-
* unless the claims name their own.
|
|
35
|
-
export function jwtSign(claims: Row, key: SigningKey, opts: { now: number; expiresInSeconds?: number; kidPrefix?: string }): string {
|
|
36
|
+
* unless the claims name their own. A token of a key pair carries the key's `kid`. */
|
|
37
|
+
export function jwtSign(claims: Row, key: SigningKey, opts: { now: number; expiresInSeconds?: number; kidPrefix?: string; typ?: string }): string {
|
|
38
|
+
// a key of another kind than its algorithm's is refused by name (an RSA key under EdDSA would fail inside the signer)
|
|
39
|
+
if ('privatePem' in key) {
|
|
40
|
+
const kind = nodeCrypto.createPrivateKey(key.privatePem).asymmetricKeyType;
|
|
41
|
+
const wants = key.alg === 'EdDSA' ? 'ed25519' : 'rsa';
|
|
42
|
+
if (kind !== wants) throw new Error(`jwtSign: ${key.alg} signs with an ${wants === 'rsa' ? 'RSA' : 'Ed25519'} key, and this key is ${kind ?? 'of no known type'}`);
|
|
43
|
+
}
|
|
36
44
|
const header = 'privatePem' in key
|
|
37
|
-
? { alg: key.alg, typ: 'JWT', kid: keyId(key.publicPem ?? nodeCrypto.createPublicKey(key.privatePem).export({ type: 'spki', format: 'pem' }).toString(), opts.kidPrefix) }
|
|
38
|
-
: { alg: key.alg, typ: 'JWT' };
|
|
45
|
+
? { alg: key.alg, typ: opts.typ ?? 'JWT', kid: keyId(key.publicPem ?? nodeCrypto.createPublicKey(key.privatePem).export({ type: 'spki', format: 'pem' }).toString(), opts.kidPrefix) }
|
|
46
|
+
: { alg: key.alg, typ: opts.typ ?? 'JWT' };
|
|
39
47
|
const payload = { iat: opts.now, exp: opts.now + (opts.expiresInSeconds ?? 60), nbf: opts.now, ...claims };
|
|
40
48
|
const input = `${jsonPart(header)}.${jsonPart(payload)}`;
|
|
41
49
|
if ('secret' in key) return `${input}.${base64url(nodeCrypto.createHmac(digestOf(key.alg), key.secret).update(input).digest())}`;
|
|
50
|
+
if (key.alg === 'EdDSA') return `${input}.${base64url(nodeCrypto.sign(null, Buffer.from(input), key.privatePem))}`;
|
|
42
51
|
const signer = nodeCrypto.createSign(`RSA-SHA${key.alg.slice(2)}`);
|
|
43
52
|
signer.update(input);
|
|
44
53
|
signer.end();
|
|
@@ -49,17 +58,17 @@ export function jwtSign(claims: Row, key: SigningKey, opts: { now: number; expir
|
|
|
49
58
|
export function jwtDecode(token: string): { header: Row; payload: Row } {
|
|
50
59
|
const parts = token.split('.');
|
|
51
60
|
if (parts.length !== 3) throw new Error('malformed jwt');
|
|
52
|
-
const part = (s: string): Row =>
|
|
61
|
+
const part = (s: string): Row => parseExactJson(fromBase64url(s).toString('utf8')) as Row;
|
|
53
62
|
return { header: part(parts[0]!), payload: part(parts[1]!) };
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
function timely(payload: Row, now: number): JwtVerdict {
|
|
57
|
-
if (typeof payload.exp === 'number' && now
|
|
58
|
-
if (typeof payload.nbf === 'number' && now
|
|
66
|
+
if ((typeof payload.exp === 'number' || typeof payload.exp === 'string' && /^-?\d+$/.test(payload.exp)) && compareExactNumbers(now, payload.exp) >= 0) return { valid: false, reason: 'expired' };
|
|
67
|
+
if ((typeof payload.nbf === 'number' || typeof payload.nbf === 'string' && /^-?\d+$/.test(payload.nbf)) && compareExactNumbers(now, payload.nbf) < 0) return { valid: false, reason: 'not_yet_valid' };
|
|
59
68
|
return { valid: true, payload };
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
/** A token checked: an RS256 one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
71
|
+
/** A token checked: an RS256 or EdDSA one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
63
72
|
* then its `exp`/`nbf` at `now` (seconds). */
|
|
64
73
|
export function jwtVerify(token: string, key: Jwks | { alg: 'HS256' | 'HS384' | 'HS512'; secret: string }, opts: { now: number }): JwtVerdict {
|
|
65
74
|
let decoded: { header: Row; payload: Row };
|
|
@@ -72,10 +81,16 @@ export function jwtVerify(token: string, key: Jwks | { alg: 'HS256' | 'HS384' |
|
|
|
72
81
|
if (given.length !== expected.length || !nodeCrypto.timingSafeEqual(given, expected)) return { valid: false, reason: 'bad_signature' };
|
|
73
82
|
return timely(decoded.payload, opts.now);
|
|
74
83
|
}
|
|
75
|
-
if (!['RS256', 'RS384', 'RS512'].includes(String(decoded.header.alg))) return { valid: false, reason: 'unexpected_alg' };
|
|
84
|
+
if (!['RS256', 'RS384', 'RS512', 'EdDSA'].includes(String(decoded.header.alg))) return { valid: false, reason: 'unexpected_alg' };
|
|
76
85
|
// a token naming a key the set does not hold matches none; one naming no key is tried against the set's only key
|
|
77
86
|
const jwk = decoded.header.kid !== undefined ? key.keys.find((k) => k.kid === decoded.header.kid) : key.keys.length === 1 ? key.keys[0] : undefined;
|
|
78
87
|
if (!jwk) return { valid: false, reason: 'no_matching_key' };
|
|
88
|
+
if ((jwk.kty === 'OKP') !== (decoded.header.alg === 'EdDSA')) return { valid: false, reason: 'unexpected_alg' };
|
|
89
|
+
if (jwk.kty === 'OKP') {
|
|
90
|
+
const edKey = nodeCrypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, format: 'jwk' });
|
|
91
|
+
if (!nodeCrypto.verify(null, Buffer.from(`${h}.${p}`), edKey, fromBase64url(s))) return { valid: false, reason: 'bad_signature' };
|
|
92
|
+
return timely(decoded.payload, opts.now);
|
|
93
|
+
}
|
|
79
94
|
const verifier = nodeCrypto.createVerify(`RSA-SHA${String(decoded.header.alg).slice(2)}`);
|
|
80
95
|
verifier.update(`${h}.${p}`);
|
|
81
96
|
verifier.end();
|
|
@@ -86,7 +101,8 @@ export function jwtVerify(token: string, key: Jwks | { alg: 'HS256' | 'HS384' |
|
|
|
86
101
|
|
|
87
102
|
/** The JWKS that verifies every token a public key's pair signs. */
|
|
88
103
|
export function jwks(publicPem: string, kidPrefix?: string): Jwks {
|
|
89
|
-
const jwk = nodeCrypto.createPublicKey(publicPem).export({ format: 'jwk' }) as { n?: string; e?: string };
|
|
104
|
+
const jwk = nodeCrypto.createPublicKey(publicPem).export({ format: 'jwk' }) as { kty?: string; n?: string; e?: string; x?: string };
|
|
105
|
+
if (jwk.kty === 'OKP') return { keys: [{ kty: 'OKP', use: 'sig', alg: 'EdDSA', crv: 'Ed25519', kid: keyId(publicPem, kidPrefix), x: String(jwk.x) }] };
|
|
90
106
|
return { keys: [{ kty: 'RSA', use: 'sig', alg: 'RS256', kid: keyId(publicPem, kidPrefix), n: String(jwk.n), e: String(jwk.e) }] };
|
|
91
107
|
}
|
|
92
108
|
|
|
@@ -168,6 +184,22 @@ export function lettersFrom(seed: string, count = 20): string {
|
|
|
168
184
|
return out;
|
|
169
185
|
}
|
|
170
186
|
|
|
187
|
+
/** `count` base62 characters (0-9A-Za-z) derived from `seed`, the same every run: the opaque tail of an id a vendor
|
|
188
|
+
* writes in base62 (OpenAI's `call_unLAR8MvFNptuiZK6K6HCy5k`, 24 after its prefix). */
|
|
189
|
+
export function base62From(seed: string, count = 24): string {
|
|
190
|
+
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
191
|
+
let out = '';
|
|
192
|
+
for (let round = 0; out.length < count; round += 1) {
|
|
193
|
+
// each byte below 248 (4 x 62) maps evenly onto the alphabet; the rest are skipped
|
|
194
|
+
for (const byte of sha256(`${seed}:${round}`).match(/../g)!) {
|
|
195
|
+
if (out.length === count) break;
|
|
196
|
+
const b = Number.parseInt(byte, 16);
|
|
197
|
+
if (b < 248) out += ALPHABET[b % 62];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
171
203
|
/** Two strings compared in constant time. */
|
|
172
204
|
export function equalSecrets(a: string, b: string): boolean {
|
|
173
205
|
const left = Buffer.from(a, 'utf8');
|
|
@@ -227,5 +259,27 @@ export function certificateOf(cert: string | Uint8Array): { der: Uint8Array; sub
|
|
|
227
259
|
}
|
|
228
260
|
|
|
229
261
|
/** What a handler signs and hashes with: `ctx.crypto`. */
|
|
230
|
-
export const handlerCrypto = { jwtSign, jwtVerify, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf } as const;
|
|
262
|
+
export const handlerCrypto = { jwtSign, jwtVerify, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, base62From, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf } as const;
|
|
231
263
|
export type HandlerCrypto = typeof handlerCrypto;
|
|
264
|
+
|
|
265
|
+
/** A [KSUID](https://github.com/segmentio/ksuid) as 27 base62 characters (0-9A-Za-z, most significant first): `seconds`
|
|
266
|
+
* past KSUID's epoch (1400000000) in four bytes, then `n` in four, then twelve bytes derived from `seed`, so KSUIDs
|
|
267
|
+
* made in one second sort by `n`, at `width` characters (27 unless a vendor's id is longer or shorter). Deterministic:
|
|
268
|
+
* the same arguments give the same id. */
|
|
269
|
+
export function ksuidFrom(seconds: number, n: number, seed: string, width = 27): string {
|
|
270
|
+
const bytes = new Uint8Array(20);
|
|
271
|
+
const view = new DataView(bytes.buffer);
|
|
272
|
+
view.setUint32(0, Math.max(0, seconds - 1_400_000_000) >>> 0);
|
|
273
|
+
view.setUint32(4, n >>> 0);
|
|
274
|
+
const tail = sha256(seed);
|
|
275
|
+
for (let i = 0; i < 12; i++) bytes[8 + i] = parseInt(tail.slice(i * 2, i * 2 + 2), 16);
|
|
276
|
+
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
277
|
+
let value = 0n;
|
|
278
|
+
for (const b of bytes) value = (value << 8n) | BigInt(b);
|
|
279
|
+
let out = '';
|
|
280
|
+
while (value > 0n) { out = ALPHABET[Number(value % 62n)]! + out; value /= 62n; }
|
|
281
|
+
const full = out.padStart(27, '0');
|
|
282
|
+
// another width than KSUID's 27 (a vendor's id of 24 or 29 characters): its first `width` characters, which keep the
|
|
283
|
+
// instant and the mint count (and so the order) from 11 on, or more base62 characters derived from the seed after them
|
|
284
|
+
return width <= 27 ? full.slice(0, width) : `${full}${base62From(`${seed}:ksuid`, width - 27)}`;
|
|
285
|
+
}
|
package/src/sigv4.ts
CHANGED
|
@@ -129,9 +129,10 @@ export function signSigV4(input: { method: string; url: string; headers?: Record
|
|
|
129
129
|
/**
|
|
130
130
|
* A presigned URL signed again with a key: its date, expiry, region, service and signed headers are the URL's own
|
|
131
131
|
* (X-Amz-Date, X-Amz-Expires, X-Amz-Credential's scope, X-Amz-SignedHeaders, `host` when it names none), its key and
|
|
132
|
-
* signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it.
|
|
132
|
+
* signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it. A
|
|
133
|
+
* signed header other than `host` (a Content-Type the URL binds) is signed with the value the request sends (`headers`).
|
|
133
134
|
*/
|
|
134
|
-
export function presignSigV4(input: { method: string; url: string; keyId: string; secret: string; host?: string }): string {
|
|
135
|
+
export function presignSigV4(input: { method: string; url: string; keyId: string; secret: string; host?: string; headers?: Record<string, string> }): string {
|
|
135
136
|
const url = new URL(input.url);
|
|
136
137
|
const q = url.searchParams;
|
|
137
138
|
const [, dateStamp = '', region = '', service = ''] = (q.get('X-Amz-Credential') ?? '').split('/');
|
|
@@ -148,7 +149,8 @@ export function presignSigV4(input: { method: string; url: string; keyId: string
|
|
|
148
149
|
.map(([k, v]) => `${k}=${v}`)
|
|
149
150
|
.join('&');
|
|
150
151
|
const names = (q.get('X-Amz-SignedHeaders') ?? 'host').toLowerCase().split(';').filter(Boolean);
|
|
151
|
-
const
|
|
152
|
+
const sent = new Headers(input.headers ?? {});
|
|
153
|
+
const canonicalHeaders = names.map((n) => `${n}:${(n === 'host' ? (input.host ?? url.host) : sent.get(n) ?? '').trim().replace(/\s+/g, ' ')}\n`).join('');
|
|
152
154
|
const canonicalRequest = [input.method.toUpperCase(), canonicalUri, canonicalQuery, canonicalHeaders, names.join(';'), 'UNSIGNED-PAYLOAD'].join('\n');
|
|
153
155
|
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, `${dateStamp}/${region}/${service}/aws4_request`, sha256Hex(canonicalRequest)].join('\n');
|
|
154
156
|
let key: Buffer = hmac(`AWS4${input.secret}`, dateStamp);
|
package/src/smtp.ts
CHANGED
|
@@ -45,9 +45,10 @@ const SMTP_TIMEOUT_MS = 10_000;
|
|
|
45
45
|
export async function sendMail(route: SmtpRoute, mail: Mail, headers: Record<string, string> = {}): Promise<void> {
|
|
46
46
|
const [{ connect }, { worldEgressRefusal }] = await Promise.all([
|
|
47
47
|
import('node:net'),
|
|
48
|
-
import('../network-policy.cjs') as Promise<{ worldEgressRefusal: (value: string) => string | null }>,
|
|
48
|
+
import('../network-policy.cjs') as Promise<{ worldEgressRefusal: (value: string, env?: NodeJS.ProcessEnv, method?: string) => string | null }>,
|
|
49
49
|
]);
|
|
50
|
-
|
|
50
|
+
// sending mail is a write: an external relay needs a `writes` grant
|
|
51
|
+
const refusal = worldEgressRefusal(`http://${route.host}:${route.port}/`, process.env, 'SMTP');
|
|
51
52
|
if (refusal !== null) throw new Error(refusal);
|
|
52
53
|
const socket = connect(route.port, route.host);
|
|
53
54
|
socket.setTimeout(SMTP_TIMEOUT_MS, () => socket.destroy(new Error(`SMTP ${route.host}:${route.port}: no answer in ${SMTP_TIMEOUT_MS} ms`)));
|
package/src/sockets.ts
CHANGED
|
@@ -50,7 +50,9 @@ export type SocketEngine<C> = {
|
|
|
50
50
|
|
|
51
51
|
type Open = { session: SocketSession; engine: SocketEngine<unknown>; context: () => Promise<unknown>; queue: Promise<void>; pending: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }> };
|
|
52
52
|
const sessions = new Map<string, Set<Open>>();
|
|
53
|
-
|
|
53
|
+
/** A World's next session number, per World (its service and root): a session's id is the World's, never the
|
|
54
|
+
* process's, so a life replayed on a fresh World numbers its sessions as it did the first time. */
|
|
55
|
+
const nextSession = new Map<string, number>();
|
|
54
56
|
const keyOf = (service: string, root: string | undefined): string => `${service}\0${root ?? ''}`;
|
|
55
57
|
|
|
56
58
|
/** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
|
|
@@ -101,7 +103,7 @@ export function socketUpgrade<C>(
|
|
|
101
103
|
const engine = engines[decl.id]! as SocketEngine<unknown>;
|
|
102
104
|
const pending: Open['pending'] = new Map();
|
|
103
105
|
const session: SocketSession = {
|
|
104
|
-
id: nextSession
|
|
106
|
+
id: (() => { const n = (nextSession.get(key) ?? 0) + 1; nextSession.set(key, n); return n; })(), socket: decl.id, state: {},
|
|
105
107
|
send: (frame) => { peer.send(typeof frame === 'string' || frame instanceof Uint8Array ? frame : JSON.stringify(frame)); },
|
|
106
108
|
close: (code, reason) => { peer.close(code, reason); },
|
|
107
109
|
expect: (key, ms) => new Promise((resolve, reject) => {
|
|
@@ -134,7 +136,10 @@ export function socketUpgrade<C>(
|
|
|
134
136
|
// what awaited this session's replies is told it closed
|
|
135
137
|
for (const [k, p] of o.pending) { clearTimeout(p.timer); p.reject(new Error('the session closed')); o.pending.delete(k); }
|
|
136
138
|
// the end is written after what the session is still doing, over its own context
|
|
137
|
-
|
|
139
|
+
// the session is gone, so a failure has no frame to go in: it is reported, never swallowed
|
|
140
|
+
if (o.engine.close) o.queue = o.queue.then(async () => o.engine.close!(o.session, await o.context())).catch((error) => {
|
|
141
|
+
console.error(`${service}: the ${o.session.socket} socket's end was not written: ${String((error as Error)?.message ?? error)}`);
|
|
142
|
+
});
|
|
138
143
|
},
|
|
139
144
|
};
|
|
140
145
|
}
|
package/src/state-system.ts
CHANGED
|
@@ -60,6 +60,9 @@ export type StateSystemAdapters = {
|
|
|
60
60
|
perform?: PerformAction;
|
|
61
61
|
refresh?: (execute: RemoteExecute, opts: { root?: string; origin?: string; credential?: string; scope?: string }) => Promise<unknown>;
|
|
62
62
|
ingest?: (request: Request, ctx: { root?: string; secret?: string }) => Response | Promise<Response>;
|
|
63
|
+
/** Why a recorded write is the World's own and sent nowhere, or undefined when perform sends it: a unit of the pack that
|
|
64
|
+
* declares `vendorBacked.none` beside lanes that are vendor-backed (PlanetScale's SQL wire beside its API lane). */
|
|
65
|
+
unsent?: (input: Record<string, unknown> | undefined) => string | undefined;
|
|
63
66
|
};
|
|
64
67
|
|
|
65
68
|
const adapters = new Map<string, StateSystemAdapters>();
|
package/src/storage.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseExactJson } from './exact-json.ts';
|
|
1
2
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
2
3
|
import { withStateLock, withStateRemoval } from './ancestry.ts';
|
|
3
4
|
import { appendParentEntry, toEntry } from './log.ts';
|
|
@@ -137,7 +138,7 @@ function readJsonl<T>(path: string): T[] {
|
|
|
137
138
|
for (const [index, line] of getActiveWorldStore().readLines(path).entries()) {
|
|
138
139
|
if (!line.trim()) continue;
|
|
139
140
|
try {
|
|
140
|
-
rows.push(
|
|
141
|
+
rows.push(parseExactJson(line) as T);
|
|
141
142
|
} catch (error) {
|
|
142
143
|
throw new Error(`${path}:${index + 1}: invalid JSONL row: ${(error as Error).message}`);
|
|
143
144
|
}
|
|
@@ -152,7 +153,7 @@ export function readJsonFile<T>(path: string): T {
|
|
|
152
153
|
try {
|
|
153
154
|
const raw = getActiveWorldStore().read(path);
|
|
154
155
|
if (raw === null) throw new Error('no such file');
|
|
155
|
-
return
|
|
156
|
+
return parseExactJson(raw) as T;
|
|
156
157
|
} catch (error) {
|
|
157
158
|
throw new Error(`${path}: invalid JSON: ${(error as Error).message}`);
|
|
158
159
|
}
|
|
@@ -350,6 +351,8 @@ export type ScrubResult = {
|
|
|
350
351
|
* one of them so scrubService never demands --force for an ordinary twin. */
|
|
351
352
|
const KNOWN_SERVICE_ENTRIES = new Set([
|
|
352
353
|
'ancestry.json', 'history-format.json', 'views', 'origin.jsonl', 'origin-head.json',
|
|
354
|
+
'answer-count',
|
|
355
|
+
'answer-count.lock',
|
|
353
356
|
'events.jsonl',
|
|
354
357
|
'events.jsonl.lock',
|
|
355
358
|
'event-queue.jsonl',
|
package/src/twin-fetch.ts
CHANGED
|
@@ -123,14 +123,15 @@ function readOnlyRefusal(error: ReadOnlyRequestError): Response {
|
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
125
|
* THE READ SCOPE for a pack that writes its own fetch (the derived packs, the byte-wire packs): a
|
|
126
|
-
* request carrying the read-only marker runs with the kernel's
|
|
126
|
+
* request carrying the read-only marker, or an instance mounted read-only, runs with the kernel's
|
|
127
|
+
* write seam refusing its writes, and a
|
|
127
128
|
* refused write answers `refuse` (the vendor's own read-only error; a generic 405 without one) —
|
|
128
129
|
* whatever the handler made of the refusal. `GET /twin` advertises `requestScopes`. Everything else
|
|
129
130
|
* passes through untouched; the wrapped fetch keeps its own properties (a derived fetch's `owners`).
|
|
130
131
|
*/
|
|
131
132
|
export function withRequestScopes<F extends (request: Request) => Promise<Response>>(
|
|
132
133
|
fetch: F,
|
|
133
|
-
opts: { refuse?: (request: Request, error: ReadOnlyRequestError) => Response | Promise<Response> } = {},
|
|
134
|
+
opts: { readOnly?: boolean; refuse?: (request: Request, error: ReadOnlyRequestError) => Response | Promise<Response> } = {},
|
|
134
135
|
): F {
|
|
135
136
|
const scoped = async (request: Request): Promise<Response> => {
|
|
136
137
|
const url = new URL(request.url);
|
|
@@ -143,7 +144,7 @@ export function withRequestScopes<F extends (request: Request) => Promise<Respon
|
|
|
143
144
|
// the request's W3C traceparent follows every entry the fetch writes, as through the kernel's own adapters
|
|
144
145
|
// (a custom fetch never entered it, so the timeline's trace filter missed its entries)
|
|
145
146
|
return runWithRequestTrace(request, async () => {
|
|
146
|
-
if (!isReadOnlyRequest(request)) return fetch(request);
|
|
147
|
+
if (!opts.readOnly && !isReadOnlyRequest(request)) return fetch(request);
|
|
147
148
|
const out = await runAsReadOnlyRequest(() => fetch(request));
|
|
148
149
|
if (!out.refused) return out.value;
|
|
149
150
|
return opts.refuse ? await opts.refuse(request, out.refused) : readOnlyRefusal(out.refused);
|
package/src/vendor-call.ts
CHANGED
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
// Google's token endpoint — and in a World that other vendor is the World's twin of it. The call is addressed as the
|
|
4
4
|
// vendor's own URL and reaches that twin by the same host rules the injector routes an application by (vendor-hosts.cjs),
|
|
5
5
|
// carrying the World's twins key; a vendor the World runs no twin of is refused, never reached: a twin makes no direct
|
|
6
|
-
// vendor call (simulated execution is deterministic and stays inside the World).
|
|
7
|
-
//
|
|
6
|
+
// vendor call (simulated execution is deterministic and stays inside the World). A twin this process answers itself (a
|
|
7
|
+
// journey walk's companions, `setInProcessTwins`) is reached first, in process; the instance's twins otherwise. This is
|
|
8
|
+
// the one network a handler has; importing this module does nothing.
|
|
8
9
|
import { twinOrigins, VENDOR_HOSTS } from '../vendor-hosts.cjs';
|
|
10
|
+
import type { ApplicationStandIn } from './events.ts';
|
|
9
11
|
|
|
10
12
|
/** A call to a vendor the World runs no twin of. */
|
|
11
13
|
export class VendorUnreachableError extends Error {
|
|
12
|
-
constructor(message: string) { super(message); this.name = 'VendorUnreachableError'; }
|
|
14
|
+
constructor(message: string, options?: ErrorOptions) { super(message, options); this.name = 'VendorUnreachableError'; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A twin answered in this process: its fetch, as a World's server hands it a request. */
|
|
18
|
+
export type InProcessTwin = (request: Request) => Response | Promise<Response>;
|
|
19
|
+
let inProcessTwins: Readonly<Record<string, InProcessTwin>> = {};
|
|
20
|
+
|
|
21
|
+
/** The twins this process answers itself, by routing key (a key of VENDOR_HOSTS: a journey walk's companions, over the
|
|
22
|
+
* walk's own World), consulted before the instance's; returns those set before, which the caller puts back. */
|
|
23
|
+
export function setInProcessTwins(twins: Readonly<Record<string, InProcessTwin>>): Readonly<Record<string, InProcessTwin>> {
|
|
24
|
+
const previous = inProcessTwins;
|
|
25
|
+
inProcessTwins = twins;
|
|
26
|
+
return previous;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
/** The World's twins by vendor: the instance's env (every twin's URL, whenever this process started) over this
|
|
@@ -27,15 +41,48 @@ async function worldTwins(): Promise<Record<string, string>> {
|
|
|
27
41
|
return { ...twinOrigins(env), ...twinOrigins(instanceEnv) };
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
/** One URL answered only by a World-held application or vendor twin: the fetch's application stand-in first (its answer
|
|
45
|
+
* is the application's; `'unreachable'` leaves no application to reach, the World's twins still reachable). */
|
|
46
|
+
async function worldResponse(target: URL, init: RequestInit, standIn?: ApplicationStandIn): Promise<Response> {
|
|
47
|
+
const scripted = await standIn?.(target.toString(), init);
|
|
48
|
+
if (scripted !== undefined && scripted !== 'unreachable') {
|
|
49
|
+
// A source may answer its headers then lose its body transport. Preserve that failure until the caller reads.
|
|
50
|
+
const body = scripted.bodyError === undefined ? scripted.body : new ReadableStream<Uint8Array>({
|
|
51
|
+
start(controller) { controller.error(new TypeError(scripted.bodyError)); },
|
|
52
|
+
});
|
|
53
|
+
return new Response([204, 205, 304].includes(scripted.status) || init.method === 'HEAD' ? null : body, { status: scripted.status, headers: scripted.headers });
|
|
54
|
+
}
|
|
55
|
+
const local = Object.keys(VENDOR_HOSTS).find(v => inProcessTwins[v] !== undefined && VENDOR_HOSTS[v]!(target.hostname, target.pathname));
|
|
56
|
+
if (local !== undefined) {
|
|
57
|
+
const headers = new Headers(init.headers);
|
|
58
|
+
headers.set('x-volter-twin-original-host', target.host);
|
|
59
|
+
return inProcessTwins[local]!(new Request(target, { ...init, headers, redirect: 'manual' }));
|
|
60
|
+
}
|
|
33
61
|
const twins = await worldTwins();
|
|
34
62
|
const vendor = Object.keys(VENDOR_HOSTS).find((v) => twins[v] !== undefined && VENDOR_HOSTS[v]!(target.hostname, target.pathname));
|
|
35
|
-
if (vendor
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
if (vendor !== undefined) {
|
|
64
|
+
const headers = new Headers(init.headers);
|
|
65
|
+
headers.set('x-volter-twin-original-host', target.host);
|
|
66
|
+
const key = process.env.VOLTER_TWINS_KEY;
|
|
67
|
+
if (key && !headers.has('x-twins-key')) headers.set('x-twins-key', key);
|
|
68
|
+
return fetch(`${twins[vendor]!.replace(/\/$/, '')}${target.pathname}${target.search}`, { ...init, headers, redirect: 'manual' });
|
|
69
|
+
}
|
|
70
|
+
const { appDestination, appFetch } = await import('../app-route.cjs') as { appDestination: (url: URL) => unknown; appFetch: (url: URL, init: RequestInit) => Promise<Response> };
|
|
71
|
+
if (scripted !== 'unreachable' && appDestination(target)) return appFetch(target, { ...init, redirect: 'manual' });
|
|
72
|
+
throw new VendorUnreachableError(`${target.host}${target.pathname}: this World holds no application or vendor twin at this URL`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A vendor URL (or the World's application), including redirects, always answered inside the World; `standIn` is the
|
|
76
|
+
* calling fetch's application stand-in (PackFetchOptions.application), consulted at every hop. */
|
|
77
|
+
export async function vendorFetch(url: string | URL, init: RequestInit = {}, standIn?: ApplicationStandIn): Promise<Response> {
|
|
78
|
+
const { followRedirects } = await import('../app-route.cjs') as {
|
|
79
|
+
followRedirects: (url: string | URL, init: RequestInit, send: (url: URL, init: RequestInit) => Promise<Response>, dropped?: string[]) => Promise<Response>;
|
|
80
|
+
};
|
|
81
|
+
try { return await followRedirects(url, init, async (target, request) => {
|
|
82
|
+
if (!['http:', 'https:'].includes(target.protocol)) throw new VendorUnreachableError(`A twin cannot fetch ${target.protocol} URLs`);
|
|
83
|
+
return worldResponse(target, request, standIn);
|
|
84
|
+
}, ['x-twins-key', 'x-volter-twin-original-host']); } catch (error) {
|
|
85
|
+
if (error instanceof VendorUnreachableError) throw error;
|
|
86
|
+
throw new VendorUnreachableError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
87
|
+
}
|
|
41
88
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bundle": {
|
|
3
|
+
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1",
|
|
4
|
+
"dsseEnvelope": {
|
|
5
|
+
"payloadType": "application/vnd.in-toto+json",
|
|
6
|
+
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9maXh0dXJlQDEuMC4wIiwiZGlnZXN0Ijp7InNoYTUxMiI6IjZhNzkzMjU0ZjQ0YjMwODY1MDUxZTZjNWQ0ZjRiYmFlMzI3OTc0MzIwZjdhYzYwZjg0ZDEzMWY0N2I3YjQ1MGJlOGRhMmRkNGQ4YzkyNThkNWVjMWJiOWNjNDliY2QzZDlkY2NiNTIyNmY0YjM4ODkyNzQyNWZjODVkNjU0MWRlIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MSIsInByZWRpY2F0ZSI6e319",
|
|
7
|
+
"signatures": [
|
|
8
|
+
{
|
|
9
|
+
"keyid": "",
|
|
10
|
+
"sig": "7nKq9VXFF3a1J7rj4tFMbM6UhafUKaXXtV5yhkubs8U/SocLU7m0E0cgxdr8ixMZbwYExsX1d46kncIZ1wCjBQ=="
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"verificationMaterial": {
|
|
15
|
+
"x509CertificateChain": {
|
|
16
|
+
"certificates": [
|
|
17
|
+
{
|
|
18
|
+
"rawBytes": "MIIDTTCCAv+gAwIBAgIQBUiE8XK0h+xFHqyfZxbTXjAFBgMrZXAwKjEVMBMGA1UECgwMc2lnc3RvcmUuZGV2MREwDwYDVQQDDAhzaWdzdG9yZTAeFw0yNjAxMDYxNDAwMDBaFw0yNjAxMDYxNDEwMDBaMAAwKjAFBgMrZXADIQC4nHlTkS1xW0AQsB+d05CE0PzyBGyWvNGEP+EJHrgYY6OCAmMwggJfMA4GA1UdDwEB/wQEAwIAgDATBgNVHSUEDDAKBggrBgEFBQcDAzBhBgNVHREBAf8EVzBVhlNodHRwczovL2dpdGh1Yi5jb20vZml4dHVyZS9yZXBvc2l0b3J5Ly5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbjAQBgorBgEEAYO/MAEIBAIMADBjBgorBgEEAYO/MAEJBFUMU2h0dHBzOi8vZ2l0aHViLmNvbS9maXh0dXJlL3JlcG9zaXRvcnkvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMBAGCisGAQQBg78wAQoEAgwAMBAGCisGAQQBg78wAQsEAgwAMCMGCisGAQQBg78wAQwEFQwTaHR0cHM6Ly9naXRodWIuY29tLzAQBgorBgEEAYO/MAENBAIMADAQBgorBgEEAYO/MAEOBAIMADAQBgorBgEEAYO/MAEPBAIMADAjBgorBgEEAYO/MAEQBBUME2h0dHBzOi8vZ2l0aHViLmNvbS8wEAYKKwYBBAGDvzABEQQCDAAwIwYKKwYBBAGDvzABEgQVDBNodHRwczovL2dpdGh1Yi5jb20vMBAGCisGAQQBg78wARMEAgwAMBAGCisGAQQBg78wARQEAgwAMDsGCisGAQQBg78wARUELQwraHR0cHM6Ly9naXRodWIuY29tLy9hY3Rpb25zL3J1bnMvL2F0dGVtcHRzLzAQBgorBgEEAYO/MAEWBAIMADAQBgorBgEEAYO/MAEYBAIMADAFBgMrZXADQQAwr/lCtMpb0F+w6OzzNVHwjnQtMxTO1WN7W1J3NeIPC8mWcWASE8c41uXJflOk3cXWpV3dFRqZ2c9cjBJszjkI"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"tlogEntries": [
|
|
23
|
+
{
|
|
24
|
+
"logIndex": "0",
|
|
25
|
+
"logId": {
|
|
26
|
+
"keyId": "dF/sfCyHqi/AtAHN6MyKhdQjWjwmAUydfFFJIBxj/xU="
|
|
27
|
+
},
|
|
28
|
+
"kindVersion": {
|
|
29
|
+
"kind": "dsse",
|
|
30
|
+
"version": "0.0.1"
|
|
31
|
+
},
|
|
32
|
+
"integratedTime": "1767708000",
|
|
33
|
+
"canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNjEyNGVlZjYyMmQ0MDgwYTVhM2Q2NGMxNWU0NWZjODk3NTBmNzJiYWNmYjEwN2MzMTY5MzY5YTI2OGZhNGIzZSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjdhOTgyY2I1ZjczMTc5NDhiM2FlZmFlMDljYjNiYzMzNDdkMDFiNTFlNDdjMDJhZTE2ZTk1NjhiODkxMGVlNTEifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiN25LcTlWWEZGM2ExSjdyajR0Rk1iTTZVaGFmVUthWFh0VjV5aGt1YnM4VS9Tb2NMVTdtMEUwY2d4ZHI4aXhNWmJ3WUV4c1gxZDQ2a25jSVoxd0NqQlE9PSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVUlVWRU5EUVhZclowRjNTVUpCWjBsUlFsVnBSVGhZU3pCb0szaEdTSEY1WmxwNFlsUllha0ZHUW1kTmNscFlRWGRMYWtWV1RVSk5SMEV4VlVVS1EyZDNUV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5Va1YzUkhkWlJGWlJVVVJFUVdoNllWZGtlbVJIT1hsYVZFRmxSbmN3ZVU1cVFYaE5SRmw0VGtSQmR3cE5SRUpoUm5jd2VVNXFRWGhOUkZsNFRrUkZkMDFFUW1GTlFVRjNTMnBCUmtKblRYSmFXRUZFU1ZGRE5HNUliRlJyVXpGNFZ6QkJVWE5DSzJRd05VTkZDakJRZW5sQ1IzbFhkazVIUlZBclJVcEljbWRaV1RaUFEwRnRUWGRuWjBwbVRVRTBSMEV4VldSRWQwVkNMM2RSUlVGM1NVRm5SRUZVUW1kT1ZraFRWVVVLUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UW1oQ1owNVdTRkpGUWtGbU9FVldla0pXYUd4T2IyUklVbmRqZW05MlRESmtjR1JIYURGWmFUVnFZakl3ZGdwYWJXdzBaRWhXZVZwVE9YbGFXRUoyWXpKc01HSXpTalZNZVRWdVlWaFNiMlJYU1haa01qbDVZVEphYzJJelpIcE1NMHBzWWtkV2FHTXlWWFZsVnpGekNsRklTbXhhYmsxMllVZFdhRnBJVFhaaVYwWndZbXBCVVVKbmIzSkNaMFZGUVZsUEwwMUJSVWxDUVVsTlFVUkNha0puYjNKQ1owVkZRVmxQTDAxQlJVb0tRa1pWVFZVeWFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1cxaFdHZ3daRmhLYkV3elNteGpSemw2WVZoU2RtTnVhM1pNYldSd1pFZG9NUXBaYVRrellqTktjbHB0ZUhaa00wMTJZMjFXYzFwWFJucGFVelUxWWxkNFFXTnRWbTFqZVRsdldsZEdhMk41T1hSWlYyeDFUVUpCUjBOcGMwZEJVVkZDQ21jM09IZEJVVzlGUVdkM1FVMUNRVWREYVhOSFFWRlJRbWMzT0hkQlVYTkZRV2QzUVUxRFRVZERhWE5IUVZGUlFtYzNPSGRCVVhkRlJsRjNWR0ZJVWpBS1kwaE5Oa3g1T1c1aFdGSnZaRmRKZFZreU9YUk1la0ZSUW1kdmNrSm5SVVZCV1U4dlRVRkZUa0pCU1UxQlJFRlJRbWR2Y2tKblJVVkJXVTh2VFVGRlR3cENRVWxOUVVSQlVVSm5iM0pDWjBWRlFWbFBMMDFCUlZCQ1FVbE5RVVJCYWtKbmIzSkNaMFZGUVZsUEwwMUJSVkZDUWxWTlJUSm9NR1JJUW5wUGFUaDJDbG95YkRCaFNGWnBURzFPZG1KVE9IZEZRVmxMUzNkWlFrSkJSMFIyZWtGQ1JWRlJRMFJCUVhkSmQxbExTM2RaUWtKQlIwUjJla0ZDUldkUlZrUkNUbThLWkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJUVUpCUjBOcGMwZEJVVkZDWnpjNGQwRlNUVVZCWjNkQlRVSkJSME5wYzBkQlVWRkNaemM0ZHdwQlVsRkZRV2QzUVUxRWMwZERhWE5IUVZGUlFtYzNPSGRCVWxWRlRGRjNjbUZJVWpCalNFMDJUSGs1Ym1GWVVtOWtWMGwxV1RJNWRFeDVPV2haTTFKd0NtSXlOWHBNTTBveFltNU5ka3d5UmpCa1IxWjBZMGhTZWt4NlFWRkNaMjl5UW1kRlJVRlpUeTlOUVVWWFFrRkpUVUZFUVZGQ1oyOXlRbWRGUlVGWlR5OEtUVUZGV1VKQlNVMUJSRUZHUW1kTmNscFlRVVJSVVVGM2NpOXNRM1JOY0dJd1JpdDNOazk2ZWs1V1NIZHFibEYwVFhoVVR6RlhUamRYTVVvelRtVkpVQXBET0cxWFkxZEJVMFU0WXpReGRWaEtabXhQYXpOaldGZHdWak5rUmxKeFdqSmpPV05xUWtwemVtcHJTUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In1dfX0=",
|
|
34
|
+
"inclusionPromise": {
|
|
35
|
+
"signedEntryTimestamp": "ak9H7xUyGle+Y9ffFmG7fcJhmuebWzMA20XrRdW8iCoauQr05gHY7SKB/Ol3Eim/PkWbeXEVh6OE8zBFhmjcDA=="
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"trust": {
|
|
42
|
+
"certificates": [
|
|
43
|
+
"-----BEGIN CERTIFICATE-----\nMIIBezCCAS2gAwIBAgIUaz/8CSoFR9V9lm1MgOxfV29zAfQwBQYDK2VwMCoxFTAT\nBgNVBAoMDHNpZ3N0b3JlLmRldjERMA8GA1UEAwwIc2lnc3RvcmUwIBcNMjAwMTAx\nMDAwMDAwWhgPMjEyMDAxMDEwMDAwMDBaMCoxFTATBgNVBAoMDHNpZ3N0b3JlLmRl\ndjERMA8GA1UEAwwIc2lnc3RvcmUwKjAFBgMrZXADIQDE41ebQe3wxijT90pJKpcI\ncMLr3RWwHjbTcE0wFY8MZ6NjMGEwHQYDVR0OBBYEFCCwTCsZsoFzvP8pW8nIipgg\n1APNMB8GA1UdIwQYMBaAFCCwTCsZsoFzvP8pW8nIipgg1APNMA8GA1UdEwEB/wQF\nMAMBAf8wDgYDVR0PAQH/BAQDAgAGMAUGAytlcANBAG5I7Hn72nt51DGymOMMFimq\n7UQ/RqT8zGPUcRC36HTxmW0p5GS62VbT/X9B+Hl/k+xAOMXfD1OdE0zt4N+V3AY=\n-----END CERTIFICATE-----\n"
|
|
44
|
+
],
|
|
45
|
+
"logKeys": [
|
|
46
|
+
"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA6ndE8AARgTs3biA59jhDL22PH2EgZqWnk4I5vugGtl8=\n-----END PUBLIC KEY-----\n"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"otherLogKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAE8WDvbqLEaI2gSsDadEZGTtJz1L1qvL8hqLgXFR0qpg=\n-----END PUBLIC KEY-----\n"
|
|
50
|
+
}
|