@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.
Files changed (179) hide show
  1. package/app-route.cjs +26 -25
  2. package/app-route.d.cts +1 -0
  3. package/dist/app-route.cjs +26 -25
  4. package/dist/app-route.d.cts +1 -0
  5. package/dist/generated/pack-facts.json +409 -352
  6. package/dist/inject.cjs +474 -95
  7. package/dist/network-policy.cjs +108 -13
  8. package/dist/network-policy.d.cts +8 -3
  9. package/dist/src/actions.js +2 -1
  10. package/dist/src/anthropic-wire.d.ts +2 -0
  11. package/dist/src/anthropic-wire.js +5 -0
  12. package/dist/src/attestation.d.ts +6 -0
  13. package/dist/src/attestation.js +27 -0
  14. package/dist/src/bytes.d.ts +3 -0
  15. package/dist/src/bytes.js +17 -0
  16. package/dist/src/changeset.js +2 -1
  17. package/dist/src/clickhouse/sql.js +73 -14
  18. package/dist/src/derived-core.d.ts +210 -32
  19. package/dist/src/derived-core.js +510 -244
  20. package/dist/src/derived-real.d.ts +1 -1
  21. package/dist/src/derived-real.js +704 -93
  22. package/dist/src/derived.d.ts +18 -2
  23. package/dist/src/derived.js +21 -2
  24. package/dist/src/events.d.ts +28 -45
  25. package/dist/src/events.js +40 -58
  26. package/dist/src/exact-json.d.ts +10 -0
  27. package/dist/src/exact-json.js +98 -0
  28. package/dist/src/executor.d.ts +17 -0
  29. package/dist/src/executor.js +91 -30
  30. package/dist/src/file-response.js +1 -1
  31. package/dist/src/form.d.ts +8 -0
  32. package/dist/src/form.js +76 -0
  33. package/dist/src/git/format.d.ts +16 -0
  34. package/dist/src/git/format.js +110 -0
  35. package/dist/src/git/index.d.ts +1 -0
  36. package/dist/src/git/index.js +1 -0
  37. package/dist/src/git/lfs.d.ts +1 -1
  38. package/dist/src/git/lfs.js +4 -3
  39. package/dist/src/graphql-wire.d.ts +6 -0
  40. package/dist/src/graphql-wire.js +6 -3
  41. package/dist/src/grpc-wire.js +2 -1
  42. package/dist/src/head.d.ts +1 -0
  43. package/dist/src/head.js +15 -4
  44. package/dist/src/held-reads.d.ts +6 -0
  45. package/dist/src/held-reads.js +60 -0
  46. package/dist/src/history.js +5 -4
  47. package/dist/src/host-port.d.ts +5 -0
  48. package/dist/src/host-port.js +14 -0
  49. package/dist/src/index.d.ts +11 -3
  50. package/dist/src/index.js +9 -4
  51. package/dist/src/log.js +12 -7
  52. package/dist/src/machines.d.ts +81 -5
  53. package/dist/src/machines.js +125 -9
  54. package/dist/src/managed-database.d.ts +15 -11
  55. package/dist/src/managed-database.js +38 -37
  56. package/dist/src/multipart.d.ts +21 -3
  57. package/dist/src/multipart.js +112 -38
  58. package/dist/src/observe.d.ts +3 -0
  59. package/dist/src/observe.js +6 -2
  60. package/dist/src/openai-wire.d.ts +16 -0
  61. package/dist/src/openai-wire.js +43 -12
  62. package/dist/src/pack-fetch.d.ts +4 -6
  63. package/dist/src/pack-fetch.js +189 -57
  64. package/dist/src/packRegistry.d.ts +7 -1
  65. package/dist/src/packRegistry.js +20 -3
  66. package/dist/src/private-transport.d.ts +1 -0
  67. package/dist/src/private-transport.js +11 -0
  68. package/dist/src/protobuf.js +10 -4
  69. package/dist/src/rateBudget.d.ts +8 -1
  70. package/dist/src/rateBudget.js +1 -0
  71. package/dist/src/redis/engine.d.ts +14 -5
  72. package/dist/src/redis/engine.js +271 -72
  73. package/dist/src/redis/frames.d.ts +17 -0
  74. package/dist/src/redis/frames.js +123 -0
  75. package/dist/src/redis/index.d.ts +6 -0
  76. package/dist/src/redis/index.js +6 -0
  77. package/dist/src/redis/json.d.ts +2 -0
  78. package/dist/src/redis/json.js +14 -0
  79. package/dist/src/redis/lua-libs.d.ts +4 -0
  80. package/dist/src/redis/lua-libs.js +173 -0
  81. package/dist/src/redis/lua.d.ts +1 -0
  82. package/dist/src/redis/lua.js +36 -0
  83. package/dist/src/redis/member-storage.d.ts +5 -0
  84. package/dist/src/redis/member-storage.js +75 -0
  85. package/dist/src/redis/resp2.d.ts +4 -0
  86. package/dist/src/redis/resp2.js +29 -0
  87. package/dist/src/redis/stream.d.ts +14 -0
  88. package/dist/src/redis/stream.js +335 -0
  89. package/dist/src/remote-execute.d.ts +2 -2
  90. package/dist/src/request-body.d.ts +11 -0
  91. package/dist/src/request-body.js +11 -0
  92. package/dist/src/runtime.d.ts +6 -5
  93. package/dist/src/runtime.js +5 -5
  94. package/dist/src/s3/wire.d.ts +4 -4
  95. package/dist/src/s3/wire.js +5 -3
  96. package/dist/src/scenario.d.ts +22 -0
  97. package/dist/src/scenario.js +35 -31
  98. package/dist/src/serve-http.d.ts +2 -1
  99. package/dist/src/serve-http.js +10 -1
  100. package/dist/src/serve.js +7 -4
  101. package/dist/src/signing.d.ts +23 -5
  102. package/dist/src/signing.js +74 -11
  103. package/dist/src/sigv4.d.ts +3 -1
  104. package/dist/src/sigv4.js +4 -2
  105. package/dist/src/smtp.js +2 -1
  106. package/dist/src/sockets.js +8 -3
  107. package/dist/src/state-system.d.ts +3 -0
  108. package/dist/src/storage.js +5 -2
  109. package/dist/src/twin-fetch.d.ts +3 -1
  110. package/dist/src/twin-fetch.js +3 -2
  111. package/dist/src/vendor-call.d.ts +10 -3
  112. package/dist/src/vendor-call.js +58 -14
  113. package/dist/test-fixtures/attestation.SOURCE.md +1 -0
  114. package/dist/test-fixtures/attestation.json +50 -0
  115. package/generated/pack-facts.json +409 -352
  116. package/inject.cjs +474 -95
  117. package/network-policy.cjs +108 -13
  118. package/network-policy.d.cts +8 -3
  119. package/package.json +11 -4
  120. package/src/actions.ts +2 -1
  121. package/src/anthropic-wire.ts +6 -0
  122. package/src/attestation.ts +23 -0
  123. package/src/bytes.ts +21 -0
  124. package/src/changeset.ts +2 -1
  125. package/src/clickhouse/sql.ts +50 -16
  126. package/src/derived-core.ts +573 -249
  127. package/src/derived-real.ts +552 -42
  128. package/src/derived.ts +23 -4
  129. package/src/events.ts +54 -85
  130. package/src/exact-json.ts +71 -0
  131. package/src/executor.ts +86 -27
  132. package/src/file-response.ts +1 -1
  133. package/src/form.ts +66 -0
  134. package/src/git/format.ts +89 -0
  135. package/src/git/index.ts +1 -0
  136. package/src/git/lfs.ts +5 -4
  137. package/src/graphql-wire.ts +8 -3
  138. package/src/grpc-wire.ts +2 -1
  139. package/src/head.ts +15 -5
  140. package/src/held-reads.ts +54 -0
  141. package/src/history.ts +5 -4
  142. package/src/host-port.ts +9 -0
  143. package/src/index.ts +10 -4
  144. package/src/log.ts +11 -7
  145. package/src/machines.ts +135 -13
  146. package/src/managed-database.ts +48 -40
  147. package/src/multipart.ts +110 -35
  148. package/src/observe.ts +6 -3
  149. package/src/openai-wire.ts +45 -14
  150. package/src/pack-fetch.ts +173 -50
  151. package/src/packRegistry.ts +27 -4
  152. package/src/private-transport.ts +8 -0
  153. package/src/protobuf.ts +8 -4
  154. package/src/rateBudget.ts +6 -1
  155. package/src/redis/engine.ts +168 -63
  156. package/src/redis/frames.ts +53 -0
  157. package/src/redis/index.ts +6 -0
  158. package/src/redis/json.ts +13 -0
  159. package/src/redis/lua-libs.ts +62 -0
  160. package/src/redis/lua.ts +20 -0
  161. package/src/redis/member-storage.ts +58 -0
  162. package/src/redis/resp2.ts +25 -0
  163. package/src/redis/stream.ts +102 -0
  164. package/src/remote-execute.ts +2 -2
  165. package/src/request-body.ts +12 -0
  166. package/src/runtime.ts +6 -5
  167. package/src/s3/wire.ts +8 -5
  168. package/src/scenario.ts +34 -13
  169. package/src/serve-http.ts +11 -2
  170. package/src/serve.ts +4 -3
  171. package/src/signing.ts +71 -17
  172. package/src/sigv4.ts +5 -3
  173. package/src/smtp.ts +3 -2
  174. package/src/sockets.ts +8 -3
  175. package/src/state-system.ts +3 -0
  176. package/src/storage.ts +5 -2
  177. package/src/twin-fetch.ts +4 -3
  178. package/src/vendor-call.ts +59 -12
  179. 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(JSON.parse(line) as TwinRequestJournalEntry); } catch { /* a torn line */ } }
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(JSON.parse(line) as TwinRequestJournalEntry); } catch { /* a torn line */ } }
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; // upgraded (websocket) — nothing served
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 names by
15
- * `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs none. */
16
- export type SigningKey = { alg: 'RS256' | 'RS384' | 'RS512'; privatePem: string; publicPem?: string } | { alg: 'HS256' | 'HS384' | 'HS512'; secret: string };
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. RS256 tokens carry the key's `kid`. */
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 => JSON.parse(fromBase64url(s).toString('utf8')) as 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 >= payload.exp) return { valid: false, reason: 'expired' };
58
- if (typeof payload.nbf === 'number' && now < payload.nbf) return { valid: false, reason: 'not_yet_valid' };
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 canonicalHeaders = names.map((n) => `${n}:${n === 'host' ? (input.host ?? url.host) : ''}\n`).join('');
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
- const refusal = worldEgressRefusal(`http://${route.host}:${route.port}/`);
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
- let nextSession = 1;
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++, socket: decl.id, state: {},
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
- if (o.engine.close) o.queue = o.queue.then(async () => o.engine.close!(o.session, await o.context())).catch(() => {});
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
  }
@@ -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(JSON.parse(line) as T);
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 JSON.parse(raw) as T;
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 write seam refusing its writes, and a
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);
@@ -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). This is the one network a handler has;
7
- // importing this module does nothing.
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
- /** `url`, a vendor's own address, answered by the World's twin of that vendor. */
31
- export async function vendorFetch(url: string | URL, init: RequestInit = {}): Promise<Response> {
32
- const target = new URL(String(url));
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 === undefined) throw new VendorUnreachableError(`${target.host}${target.pathname}: this World runs no twin of it, and a twin calls only the World's twins`);
36
- const headers = new Headers(init.headers);
37
- headers.set('x-volter-twin-original-host', target.host);
38
- const key = process.env.VOLTER_TWINS_KEY;
39
- if (key && !headers.has('x-twins-key')) headers.set('x-twins-key', key);
40
- return fetch(`${twins[vendor]!.replace(/\/$/, '')}${target.pathname}${target.search}`, { ...init, headers });
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
+ }