@nylorun/runtime 0.15.2-beta → 0.17.0-beta

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 (291) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +68 -29
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/sandbox/local.d.ts +10 -0
  6. package/dist/adapters/sandbox/local.js +195 -0
  7. package/dist/adapters/sandbox/virtual.js +36 -1
  8. package/dist/admin-openapi.json +341 -7
  9. package/dist/api/a2a/tasks.js +6 -1
  10. package/dist/api/ag-ui/endpoint.js +1 -2
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.d.ts +1 -1
  13. package/dist/api/ag-ui/routes.js +15 -13
  14. package/dist/api/components.d.ts +2667 -711
  15. package/dist/api/components.js +24 -11
  16. package/dist/api/gate/keys.d.ts +4 -3
  17. package/dist/api/gate/routes.d.ts +19 -3
  18. package/dist/api/gate/routes.js +162 -53
  19. package/dist/api/http/app.js +6 -0
  20. package/dist/api/http/define.d.ts +19 -12
  21. package/dist/api/http/define.js +13 -27
  22. package/dist/api/http/routes/access.d.ts +3 -19
  23. package/dist/api/http/routes/access.js +12 -172
  24. package/dist/api/http/routes/actions.js +0 -1
  25. package/dist/api/http/routes/artifacts.d.ts +11 -0
  26. package/dist/api/http/routes/artifacts.js +516 -0
  27. package/dist/api/http/routes/me.d.ts +13 -0
  28. package/dist/api/http/routes/me.js +45 -0
  29. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  30. package/dist/api/http/routes/sandboxes.js +150 -0
  31. package/dist/api/http/routes/sessions.js +9 -13
  32. package/dist/api/http/routes/tenant.js +36 -2
  33. package/dist/api/http/routes/vaults.d.ts +12 -3
  34. package/dist/api/http/routes/vaults.js +122 -54
  35. package/dist/api/openapi.js +4 -10
  36. package/dist/artifacts/config.d.ts +23 -0
  37. package/dist/artifacts/config.js +27 -0
  38. package/dist/artifacts/export.d.ts +8 -0
  39. package/dist/artifacts/export.js +205 -0
  40. package/dist/artifacts/files.d.ts +36 -0
  41. package/dist/artifacts/files.js +45 -0
  42. package/dist/artifacts/folders.d.ts +60 -0
  43. package/dist/artifacts/folders.js +209 -0
  44. package/dist/artifacts/links.d.ts +28 -0
  45. package/dist/artifacts/links.js +92 -0
  46. package/dist/artifacts/media-types.d.ts +13 -0
  47. package/dist/artifacts/media-types.js +80 -0
  48. package/dist/artifacts/parts.d.ts +34 -0
  49. package/dist/artifacts/parts.js +38 -0
  50. package/dist/artifacts/service.d.ts +74 -0
  51. package/dist/artifacts/service.js +301 -0
  52. package/dist/artifacts/workspace.d.ts +60 -0
  53. package/dist/artifacts/workspace.js +57 -0
  54. package/dist/artifacts/zip.d.ts +10 -0
  55. package/dist/artifacts/zip.js +126 -0
  56. package/dist/blob/fs.d.ts +13 -0
  57. package/dist/blob/fs.js +194 -0
  58. package/dist/blob/index.d.ts +9 -0
  59. package/dist/blob/index.js +9 -0
  60. package/dist/blob/s3.d.ts +42 -0
  61. package/dist/blob/s3.js +366 -0
  62. package/dist/blob/types.d.ts +108 -0
  63. package/dist/blob/types.js +85 -0
  64. package/dist/core/sandbox-routes.d.ts +2 -2
  65. package/dist/core/sandbox-routes.js +4 -3
  66. package/dist/execution/memory.d.ts +11 -1
  67. package/dist/execution/memory.js +77 -3
  68. package/dist/execution/types.d.ts +54 -1
  69. package/dist/execution/types.js +31 -0
  70. package/dist/gates/contract.d.ts +14 -13
  71. package/dist/gates/contract.js +13 -12
  72. package/dist/gates/egress.d.ts +72 -0
  73. package/dist/gates/egress.js +319 -0
  74. package/dist/gates/handler.d.ts +3 -0
  75. package/dist/gates/handler.js +3 -2
  76. package/dist/gates/http-client.d.ts +3 -2
  77. package/dist/gates/http-client.js +17 -6
  78. package/dist/gates/in-process.d.ts +13 -1
  79. package/dist/gates/in-process.js +11 -0
  80. package/dist/gates/inflight.d.ts +23 -4
  81. package/dist/gates/inflight.js +18 -4
  82. package/dist/gates/mcp-handler.js +5 -1
  83. package/dist/gates/model-gate.d.ts +6 -2
  84. package/dist/gates/tenant-vaults.d.ts +18 -9
  85. package/dist/gates/tenant-vaults.js +17 -12
  86. package/dist/gates/tool-calls.d.ts +9 -5
  87. package/dist/gates/tool-calls.js +6 -6
  88. package/dist/gates/tool-client.d.ts +9 -2
  89. package/dist/gates/tool-client.js +56 -22
  90. package/dist/gates/tool-contract.d.ts +12 -7
  91. package/dist/gates/tool-contract.js +10 -4
  92. package/dist/gates/tool-gate.d.ts +1 -0
  93. package/dist/harness/calls.d.ts +52 -0
  94. package/dist/harness/calls.js +113 -0
  95. package/dist/harness/executors.d.ts +22 -0
  96. package/dist/harness/executors.js +96 -0
  97. package/dist/harness/main.d.ts +6 -0
  98. package/dist/harness/main.js +103 -0
  99. package/dist/harness/pod.d.ts +21 -0
  100. package/dist/harness/pod.js +119 -0
  101. package/dist/harness/run-tokens.d.ts +18 -0
  102. package/dist/harness/run-tokens.js +15 -0
  103. package/dist/harness/service.d.ts +46 -0
  104. package/dist/harness/service.js +179 -0
  105. package/dist/harness/ws-client.d.ts +43 -0
  106. package/dist/harness/ws-client.js +122 -0
  107. package/dist/harness/ws-port.d.ts +22 -0
  108. package/dist/harness/ws-port.js +78 -0
  109. package/dist/harness-api/in-process.d.ts +24 -0
  110. package/dist/harness-api/in-process.js +119 -0
  111. package/dist/harness-api/record.d.ts +44 -0
  112. package/dist/harness-api/record.js +250 -0
  113. package/dist/harness-api/renew.d.ts +37 -0
  114. package/dist/harness-api/renew.js +77 -0
  115. package/dist/harness-api/server.d.ts +104 -0
  116. package/dist/harness-api/server.js +408 -0
  117. package/dist/harness-api/start.d.ts +36 -0
  118. package/dist/harness-api/start.js +118 -0
  119. package/dist/harness-api/workspace.d.ts +53 -0
  120. package/dist/harness-api/workspace.js +201 -0
  121. package/dist/harness-api/ws-server.d.ts +34 -0
  122. package/dist/harness-api/ws-server.js +227 -0
  123. package/dist/host/admin-api.d.ts +12 -5
  124. package/dist/host/admin-api.js +72 -6
  125. package/dist/host/app.d.ts +0 -1
  126. package/dist/host/app.js +35 -24
  127. package/dist/host/config-for.d.ts +4 -0
  128. package/dist/host/config-for.js +2 -0
  129. package/dist/host/config.d.ts +0 -6
  130. package/dist/host/create-host.d.ts +3 -10
  131. package/dist/host/create-host.js +4 -1
  132. package/dist/host/gates.d.ts +27 -2
  133. package/dist/host/gates.js +52 -2
  134. package/dist/host/http.d.ts +4 -1
  135. package/dist/host/http.js +6 -1
  136. package/dist/host/main.js +167 -37
  137. package/dist/host/stack-config.d.ts +112 -9
  138. package/dist/host/stack-config.js +222 -20
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +2 -0
  141. package/dist/keys/client.js +9 -1
  142. package/dist/keys/keys.d.ts +12 -2
  143. package/dist/keys/keys.js +8 -0
  144. package/dist/mcp/connect.js +2 -1
  145. package/dist/mcp/pool.d.ts +1 -0
  146. package/dist/mcp/pool.js +1 -0
  147. package/dist/mcp/snapshot.d.ts +8 -1
  148. package/dist/mcp/snapshot.js +28 -0
  149. package/dist/media.d.ts +5 -24
  150. package/dist/media.js +5 -0
  151. package/dist/model/pi-model.d.ts +6 -2
  152. package/dist/model/pi-model.js +39 -22
  153. package/dist/node/index.d.ts +1 -1
  154. package/dist/node/index.js +1 -1
  155. package/dist/openapi.json +14084 -9426
  156. package/dist/record/index.d.ts +1 -0
  157. package/dist/record/index.js +1 -0
  158. package/dist/record/sandbox.d.ts +25 -0
  159. package/dist/record/sandbox.js +43 -0
  160. package/dist/sandbox/capability.d.ts +3 -0
  161. package/dist/sandbox/capability.js +10 -0
  162. package/dist/sandbox/egress-token.d.ts +26 -0
  163. package/dist/sandbox/egress-token.js +8 -0
  164. package/dist/sandbox/join.d.ts +37 -0
  165. package/dist/sandbox/join.js +165 -0
  166. package/dist/sandbox/manager.d.ts +99 -34
  167. package/dist/sandbox/manager.js +216 -84
  168. package/dist/sandbox/placement.d.ts +27 -0
  169. package/dist/sandbox/placement.js +44 -0
  170. package/dist/sandbox/pods/client.d.ts +90 -0
  171. package/dist/sandbox/pods/client.js +79 -0
  172. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  173. package/dist/sandbox/pods/lifecycle.js +178 -0
  174. package/dist/sandbox/pods/name.d.ts +1 -0
  175. package/dist/sandbox/pods/name.js +28 -0
  176. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  177. package/dist/sandbox/pods/network-gate.js +71 -0
  178. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  179. package/dist/sandbox/pods/reconcile.js +152 -0
  180. package/dist/sandbox/pods/spec.d.ts +45 -0
  181. package/dist/sandbox/pods/spec.js +62 -0
  182. package/dist/sandbox/records.d.ts +29 -0
  183. package/dist/sandbox/records.js +79 -0
  184. package/dist/sandbox/resolve.d.ts +5 -0
  185. package/dist/sandbox/resolve.js +3 -2
  186. package/dist/sandbox/session-sandbox.js +7 -5
  187. package/dist/sandbox/share.d.ts +11 -0
  188. package/dist/sandbox/share.js +10 -0
  189. package/dist/sandbox/tenant-config.d.ts +16 -1
  190. package/dist/sandbox/tenant-config.js +9 -0
  191. package/dist/sandbox/types.d.ts +29 -1
  192. package/dist/sandbox/types.js +13 -1
  193. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  194. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  195. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  196. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  197. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  198. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  199. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  200. package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
  201. package/dist/store/postgres/record-writer.d.ts +6 -1
  202. package/dist/store/postgres/record-writer.js +20 -2
  203. package/dist/store/postgres/schema.d.ts +1168 -250
  204. package/dist/store/postgres/schema.js +174 -28
  205. package/dist/store/postgres/store.js +339 -103
  206. package/dist/store/postgres/tenant.d.ts +1 -1
  207. package/dist/store/postgres/tenant.js +2 -2
  208. package/dist/store/types.d.ts +163 -32
  209. package/dist/streams/control.d.ts +2 -5
  210. package/dist/streams/control.js +5 -8
  211. package/dist/streams/types.d.ts +8 -8
  212. package/dist/streams/types.js +1 -1
  213. package/dist/tenant/advance.js +218 -160
  214. package/dist/tenant/artifact-tool.d.ts +5 -0
  215. package/dist/tenant/artifact-tool.js +119 -0
  216. package/dist/tenant/auth.d.ts +13 -9
  217. package/dist/tenant/auth.js +25 -32
  218. package/dist/tenant/commands.js +31 -10
  219. package/dist/tenant/context.d.ts +83 -26
  220. package/dist/tenant/delivery-token.js +4 -4
  221. package/dist/tenant/effects.d.ts +24 -14
  222. package/dist/tenant/effects.js +10 -398
  223. package/dist/tenant/ephemeral.d.ts +14 -2
  224. package/dist/tenant/ephemeral.js +8 -1
  225. package/dist/tenant/history.d.ts +13 -23
  226. package/dist/tenant/history.js +14 -62
  227. package/dist/tenant/host-token.d.ts +92 -0
  228. package/dist/tenant/host-token.js +179 -0
  229. package/dist/tenant/identity-file.d.ts +75 -0
  230. package/dist/tenant/identity-file.js +202 -0
  231. package/dist/tenant/issuers.d.ts +48 -0
  232. package/dist/tenant/issuers.js +311 -0
  233. package/dist/tenant/jwt.js +3 -3
  234. package/dist/tenant/module.js +1 -0
  235. package/dist/tenant/operator-keys.d.ts +18 -0
  236. package/dist/tenant/operator-keys.js +55 -0
  237. package/dist/tenant/outbound.d.ts +30 -0
  238. package/dist/tenant/outbound.js +142 -10
  239. package/dist/tenant/paths.d.ts +2 -1
  240. package/dist/tenant/paths.js +3 -1
  241. package/dist/tenant/principals.d.ts +2 -11
  242. package/dist/tenant/principals.js +10 -30
  243. package/dist/tenant/reset.d.ts +5 -2
  244. package/dist/tenant/reset.js +22 -2
  245. package/dist/tenant/run-grants.d.ts +49 -0
  246. package/dist/tenant/run-grants.js +62 -0
  247. package/dist/tenant/run-token.d.ts +70 -0
  248. package/dist/tenant/run-token.js +138 -0
  249. package/dist/tenant/runtime.d.ts +42 -0
  250. package/dist/tenant/runtime.js +134 -33
  251. package/dist/tenant/sandboxes.d.ts +119 -0
  252. package/dist/tenant/sandboxes.js +551 -0
  253. package/dist/tenant/session-streams.d.ts +6 -10
  254. package/dist/tenant/session-streams.js +9 -27
  255. package/dist/tenant/session.d.ts +2 -13
  256. package/dist/tenant/session.js +4 -28
  257. package/dist/tenant/sessions.d.ts +6 -1
  258. package/dist/tenant/sessions.js +29 -2
  259. package/dist/tenant/signing-keys.d.ts +2 -1
  260. package/dist/tenant/signing-keys.js +2 -2
  261. package/dist/tenant/status.d.ts +8 -3
  262. package/dist/tenant/status.js +5 -0
  263. package/dist/tenant/streams.d.ts +8 -2
  264. package/dist/tenant/streams.js +25 -6
  265. package/dist/tenant/sweep.js +37 -4
  266. package/dist/tenant/types.d.ts +39 -1
  267. package/dist/tenant/worker.d.ts +11 -5
  268. package/dist/tenant/worker.js +12 -6
  269. package/dist/vault/crypto.d.ts +7 -0
  270. package/dist/vault/crypto.js +16 -0
  271. package/dist/vault/oauth.d.ts +55 -0
  272. package/dist/vault/oauth.js +186 -0
  273. package/dist/vault/service.d.ts +54 -7
  274. package/dist/vault/service.js +261 -17
  275. package/dist/vault/sources.d.ts +54 -0
  276. package/dist/vault/sources.js +185 -0
  277. package/dist/version.d.ts +1 -1
  278. package/dist/version.js +1 -1
  279. package/package.json +14 -10
  280. package/dist/adapters/media.d.ts +0 -24
  281. package/dist/adapters/media.js +0 -111
  282. package/dist/host/cors.d.ts +0 -27
  283. package/dist/host/cors.js +0 -64
  284. package/dist/tenant/access-policy.d.ts +0 -30
  285. package/dist/tenant/access-policy.js +0 -52
  286. package/dist/tenant/browser.d.ts +0 -15
  287. package/dist/tenant/browser.js +0 -39
  288. package/dist/tenant/subject-limits.d.ts +0 -16
  289. package/dist/tenant/subject-limits.js +0 -45
  290. package/dist/tenant/tokens.d.ts +0 -14
  291. package/dist/tenant/tokens.js +0 -180
@@ -1,5 +1,5 @@
1
- /** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
2
- export const CHUNK_BYTES = 256 * 1024;
1
+ import { applyUpdate, transcriptOf } from "@nylorun/core/harness-api";
2
+ export { CHUNK_BYTES, TranscriptFoldError, applyUpdate, applyUpdates, transcriptOf, transcriptUpdates, withTranscript, } from "@nylorun/core/harness-api";
3
3
  let shadow = false;
4
4
  /** Shadow mode: keep the transcript on the session row and check the fold against it. */
5
5
  export function transcriptShadow() {
@@ -11,12 +11,6 @@ export function setTranscriptShadow(on) {
11
11
  shadow = on;
12
12
  return previous;
13
13
  }
14
- export class TranscriptFoldError extends Error {
15
- constructor(message) {
16
- super(message);
17
- this.name = "TranscriptFoldError";
18
- }
19
- }
20
14
  /** The fold and the transcript on the row differ (shadow mode only). */
21
15
  export class TranscriptParityError extends Error {
22
16
  constructor(sessionId, at, folded, stored) {
@@ -26,59 +20,21 @@ export class TranscriptParityError extends Error {
26
20
  }
27
21
  }
28
22
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
29
- /** The engine state's transcript, or none. */
30
- export function transcriptOf(state) {
31
- const transcript = state?.transcript;
32
- return Array.isArray(transcript) ? transcript : [];
33
- }
34
- /**
35
- * The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
36
- * Empty when nothing changed.
37
- */
38
- export function transcriptUpdates(before, after, chunkBytes = CHUNK_BYTES) {
39
- let keep = 0;
40
- while (keep < before.length && keep < after.length && same(before[keep], after[keep]))
41
- keep += 1;
42
- if (keep === before.length && keep === after.length)
43
- return [];
44
- const added = after.slice(keep);
45
- if (added.length === 0)
46
- return [{ keep, entries: [], length: keep }];
47
- const updates = [];
48
- let chunk = [];
49
- let bytes = 0;
50
- let base = keep;
51
- const flush = () => {
52
- updates.push({ keep: base, entries: chunk, length: base + chunk.length });
53
- base += chunk.length;
54
- chunk = [];
55
- bytes = 0;
56
- };
57
- for (const entry of added) {
58
- const size = JSON.stringify(entry).length;
59
- if (chunk.length > 0 && bytes + size > chunkBytes)
60
- flush();
61
- chunk.push(entry);
62
- bytes += size;
63
- }
64
- flush();
65
- return updates;
66
- }
67
- /** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
68
- export function applyUpdate(transcript, update) {
69
- if (update.keep > transcript.length)
70
- throw new TranscriptFoldError(`transcript.updated keeps ${update.keep} entries of ${transcript.length}`);
71
- const next = [...transcript.slice(0, update.keep), ...update.entries];
72
- if (next.length !== update.length)
73
- throw new TranscriptFoldError(`transcript.updated should give ${update.length} entries, not ${next.length}`);
74
- return next;
75
- }
76
23
  /**
77
24
  * Folds a session's events, from the start of its record or from a snapshot, into the
78
25
  * transcript the engine resumes from.
79
26
  */
80
27
  export function foldTranscript(events) {
28
+ return foldTranscriptWithCursor(events).transcript;
29
+ }
30
+ /**
31
+ * `foldTranscript`, and the cursor of the fold: the `seq` of the last event that changed it
32
+ * (`transcript.updated`, `turn.cancelled`, `turn.failed`), or -1. A harness that holds the
33
+ * transcript at the same cursor holds this one.
34
+ */
35
+ export function foldTranscriptWithCursor(events) {
81
36
  let transcript = [];
37
+ let cursor = -1;
82
38
  // The transcript before the current turn's first edit: what a cancel or failure restores.
83
39
  let turn;
84
40
  for (const event of events) {
@@ -86,16 +42,18 @@ export function foldTranscript(events) {
86
42
  if (!turn || turn.id !== event.turnId)
87
43
  turn = { id: event.turnId, before: transcript };
88
44
  transcript = applyUpdate(transcript, event.payload);
45
+ cursor = event.seq ?? cursor;
89
46
  }
90
47
  else if (event.type === "turn.cancelled" || event.type === "turn.failed") {
91
48
  if (turn && turn.id === event.turnId)
92
49
  transcript = turn.before;
93
50
  turn = undefined;
51
+ cursor = event.seq ?? cursor;
94
52
  }
95
53
  else if (event.type === "turn.completed")
96
54
  turn = undefined;
97
55
  }
98
- return transcript;
56
+ return { transcript, cursor };
99
57
  }
100
58
  /** `state` with an empty transcript (what the session row stores outside shadow mode). */
101
59
  export function leanState(state) {
@@ -103,12 +61,6 @@ export function leanState(state) {
103
61
  return state;
104
62
  return { ...state, transcript: [] };
105
63
  }
106
- /** `state` with `transcript` (what the engine runs on). */
107
- export function withTranscript(state, transcript) {
108
- if (!state)
109
- return state;
110
- return { ...state, transcript: [...transcript] };
111
- }
112
64
  /** In shadow mode, checks the fold against the transcript the row kept. */
113
65
  export function checkParity(sessionId, folded, stored) {
114
66
  if (!shadow)
@@ -0,0 +1,92 @@
1
+ import { importJWK } from "jose";
2
+ import type { Keys } from "../keys/keys.js";
3
+ import type { SessionStore } from "../store/types.js";
4
+ export declare const HOST_TOKEN_TYP = "nylorun-host+jwt";
5
+ export declare const HOST_TOKEN_AUD = "nylorun-harness-api";
6
+ export declare const EGRESS_TOKEN_TYP = "nylorun-egress+jwt";
7
+ export declare const EGRESS_TOKEN_AUD = "nylorun-egress";
8
+ /** How long a host or egress token lives. The engine renews them before. */
9
+ export declare const HOST_TOKEN_TTL_SECONDS: number;
10
+ /** What a verified host or egress token says. */
11
+ export interface HostClaims {
12
+ readonly tenantId: string;
13
+ readonly sandboxId: string;
14
+ /** The host epoch: the fence. */
15
+ readonly epoch: number;
16
+ /** The UID of the pod the token was minted for. */
17
+ readonly podUid: string;
18
+ /** Milliseconds since the epoch. */
19
+ readonly expiresAt: number;
20
+ }
21
+ /** A host token and the egress token minted with it. */
22
+ export interface HostGrant {
23
+ readonly hostToken: string;
24
+ readonly egressToken: string;
25
+ readonly claims: HostClaims;
26
+ }
27
+ export interface HostTokenSigner {
28
+ readonly keys: Keys;
29
+ readonly config: {
30
+ readonly tenantId: string;
31
+ };
32
+ }
33
+ /** One egress token to mint: one pod of one sandbox at one host epoch. */
34
+ export interface EgressTokenRequest {
35
+ readonly tenantId: string;
36
+ readonly sandboxId: string;
37
+ /** The host epoch the join set. */
38
+ readonly epoch: number;
39
+ readonly podUid: string;
40
+ /** Seconds. Default and maximum `HOST_TOKEN_TTL_SECONDS`. */
41
+ readonly ttl?: number;
42
+ }
43
+ /**
44
+ * Mints an egress token (the only path that does): the proxy credential egress-gate accepts,
45
+ * with egress-gate's claims (no `sub`). `mintHostTokens` mints one with every host token.
46
+ */
47
+ export declare function mintEgressToken(signer: {
48
+ readonly keys: Keys;
49
+ }, request: EgressTokenRequest): Promise<{
50
+ readonly token: string;
51
+ readonly claims: HostClaims;
52
+ }>;
53
+ /** Mints the host and egress tokens of `sandboxId`'s pod `podUid` at host epoch `epoch`. */
54
+ export declare function mintHostTokens(signer: HostTokenSigner, host: {
55
+ readonly sandboxId: string;
56
+ readonly epoch: number;
57
+ readonly podUid: string;
58
+ }): Promise<HostGrant>;
59
+ /** A verified token's claims, or why it was refused (logged, never answered). */
60
+ export type HostTokenVerdict = {
61
+ readonly ok: true;
62
+ readonly claims: HostClaims;
63
+ } | {
64
+ readonly ok: false;
65
+ readonly reason: string;
66
+ };
67
+ type PublicKey = Awaited<ReturnType<typeof importJWK>>;
68
+ /** Public keys imported by key id; the key row is still read on every verification. */
69
+ export type HostTokenKeyCache = Map<string, PublicKey>;
70
+ declare const KINDS: {
71
+ readonly host: {
72
+ readonly typ: "nylorun-host+jwt";
73
+ readonly aud: "nylorun-harness-api";
74
+ };
75
+ readonly egress: {
76
+ readonly typ: "nylorun-egress+jwt";
77
+ readonly aud: "nylorun-egress";
78
+ };
79
+ };
80
+ /**
81
+ * Verifies a host (`kind: "host"`) or egress (`kind: "egress"`) token of Tenant `tenantId`:
82
+ * shape and header before any key is read, then the signature, `iss`, `aud`, `typ`, lifetime
83
+ * and claims. It does not read the sandbox: `currentHost` does.
84
+ */
85
+ export declare function verifyHostToken(store: SessionStore, tenantId: string, raw: string, kind: keyof typeof KINDS, cache?: HostTokenKeyCache): Promise<HostTokenVerdict>;
86
+ /**
87
+ * Why a host's claims are stale, or undefined while they are its sandbox's current host: the
88
+ * sandbox exists as a pod sandbox, is not deleted, lost or expired, and its epoch and pod are
89
+ * the token's. One indexed read of the sandbox's row.
90
+ */
91
+ export declare function staleHost(store: SessionStore, claims: HostClaims): Promise<string | undefined>;
92
+ export {};
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Host and egress tokens (F7.2, D42): the ES256 JWTs a pod sandbox's engine holds once its
3
+ * join token is exchanged (`sandbox/join.ts`). Both name one sandbox and one host epoch; a
4
+ * join, a relaunch, a loss, an expiry, a stop or a reset moves the epoch, and a token of an
5
+ * older one is refused. Signed by the keys service with the Tenant's current signing key,
6
+ * like run tokens, and told apart from them and from each other by `typ` and `aud`:
7
+ *
8
+ * ```text
9
+ * host { alg: ES256, typ: nylorun-host+jwt, kid }
10
+ * { iss: urn:nylorun:tenant:<id>, aud: nylorun-harness-api, sub: <sandboxId>,
11
+ * sbx: <sandboxId>, epc: <host epoch>, pod: <pod UID>, iat, exp, jti }
12
+ * egress { alg: ES256, typ: nylorun-egress+jwt, kid }
13
+ * { iss: urn:nylorun:tenant:<id>, aud: nylorun-egress,
14
+ * sbx: <sandboxId>, epc: <host epoch>, pod: <pod UID>, iat, exp, jti }
15
+ * ```
16
+ *
17
+ * The host token is accepted only by the Harness API listener (a connection serving that one
18
+ * sandbox, and `host.renew`); the egress token only by egress-gate, as the proxy credential
19
+ * (`Proxy-Authorization`). Neither is a bearer anywhere else: the Tenant API's bearer check
20
+ * accepts trusted issuers' and delivery tokens only, and the gates run tokens only.
21
+ *
22
+ * Verification reads the key row on every call (a revoked key refuses at once) and, with
23
+ * `currentHost`, the sandbox's row (one indexed read): the epoch must be the sandbox's, the
24
+ * pod its pod, and the sandbox neither lost, expired nor deleted.
25
+ */
26
+ import { randomUUID } from "node:crypto";
27
+ import { errors, importJWK, jwtVerify } from "jose";
28
+ import { tenantTokenIssuer } from "@nylorun/core/contracts";
29
+ import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
30
+ export const HOST_TOKEN_TYP = "nylorun-host+jwt";
31
+ export const HOST_TOKEN_AUD = "nylorun-harness-api";
32
+ export const EGRESS_TOKEN_TYP = "nylorun-egress+jwt";
33
+ export const EGRESS_TOKEN_AUD = "nylorun-egress";
34
+ /** How long a host or egress token lives. The engine renews them before. */
35
+ export const HOST_TOKEN_TTL_SECONDS = 15 * 60;
36
+ const LIFETIME_SLACK_SECONDS = 60;
37
+ /**
38
+ * Mints an egress token (the only path that does): the proxy credential egress-gate accepts,
39
+ * with egress-gate's claims (no `sub`). `mintHostTokens` mints one with every host token.
40
+ */
41
+ export async function mintEgressToken(signer, request) {
42
+ const { tenantId, sandboxId, epoch, podUid } = request;
43
+ const ttl = request.ttl ?? HOST_TOKEN_TTL_SECONDS;
44
+ if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > HOST_TOKEN_TTL_SECONDS)
45
+ throw new Error(`An egress token lives 1 to ${HOST_TOKEN_TTL_SECONDS} s, not ${ttl}`);
46
+ if (!Number.isSafeInteger(epoch) || epoch < 1)
47
+ throw new Error(`Host epoch ${epoch} is not a positive integer`);
48
+ if (sandboxId === "" || podUid === "")
49
+ throw new Error("An egress token names a sandbox and a pod");
50
+ const iat = Math.floor(Date.now() / 1000);
51
+ const exp = iat + ttl;
52
+ const { token } = await signer.keys.sign({
53
+ typ: EGRESS_TOKEN_TYP,
54
+ claims: {
55
+ iss: tenantTokenIssuer(tenantId),
56
+ aud: EGRESS_TOKEN_AUD,
57
+ sbx: sandboxId,
58
+ epc: epoch,
59
+ pod: podUid,
60
+ iat,
61
+ exp,
62
+ jti: randomUUID(),
63
+ },
64
+ });
65
+ return { token, claims: { tenantId, sandboxId, epoch, podUid, expiresAt: exp * 1000 } };
66
+ }
67
+ /** Mints the host and egress tokens of `sandboxId`'s pod `podUid` at host epoch `epoch`. */
68
+ export async function mintHostTokens(signer, host) {
69
+ const tenantId = signer.config.tenantId;
70
+ const iat = Math.floor(Date.now() / 1000);
71
+ const exp = iat + HOST_TOKEN_TTL_SECONDS;
72
+ const [hostToken, egress] = await Promise.all([
73
+ signer.keys.sign({
74
+ typ: HOST_TOKEN_TYP,
75
+ claims: {
76
+ iss: tenantTokenIssuer(tenantId),
77
+ aud: HOST_TOKEN_AUD,
78
+ sub: host.sandboxId,
79
+ sbx: host.sandboxId,
80
+ epc: host.epoch,
81
+ pod: host.podUid,
82
+ iat,
83
+ exp,
84
+ jti: randomUUID(),
85
+ },
86
+ }),
87
+ mintEgressToken(signer, { tenantId, ...host }),
88
+ ]);
89
+ return {
90
+ hostToken: hostToken.token,
91
+ egressToken: egress.token,
92
+ claims: {
93
+ tenantId,
94
+ sandboxId: host.sandboxId,
95
+ epoch: host.epoch,
96
+ podUid: host.podUid,
97
+ expiresAt: exp * 1000,
98
+ },
99
+ };
100
+ }
101
+ const KINDS = {
102
+ host: { typ: HOST_TOKEN_TYP, aud: HOST_TOKEN_AUD },
103
+ egress: { typ: EGRESS_TOKEN_TYP, aud: EGRESS_TOKEN_AUD },
104
+ };
105
+ /**
106
+ * Verifies a host (`kind: "host"`) or egress (`kind: "egress"`) token of Tenant `tenantId`:
107
+ * shape and header before any key is read, then the signature, `iss`, `aud`, `typ`, lifetime
108
+ * and claims. It does not read the sandbox: `currentHost` does.
109
+ */
110
+ export async function verifyHostToken(store, tenantId, raw, kind, cache = new Map()) {
111
+ const refused = (reason) => ({ ok: false, reason: `${kind}_token_${reason}` });
112
+ const { typ, aud } = KINDS[kind];
113
+ const checked = tokenKeyId(raw, typ);
114
+ if ("refused" in checked)
115
+ return refused(checked.refused);
116
+ const { kid } = checked;
117
+ const row = await store.tx((t) => t.signingKey(kid));
118
+ if (!row)
119
+ return refused("key_unknown");
120
+ let key = cache.get(kid);
121
+ if (!key) {
122
+ key = await importJWK({ ...JSON.parse(row.publicJwk), alg: "ES256" }, "ES256");
123
+ cache.set(kid, key);
124
+ }
125
+ let payload;
126
+ try {
127
+ const verified = await jwtVerify(raw, key, {
128
+ algorithms: ["ES256"],
129
+ issuer: tenantTokenIssuer(tenantId),
130
+ audience: aud,
131
+ typ,
132
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
133
+ requiredClaims: ["iat", "exp", "jti"],
134
+ });
135
+ payload = verified.payload;
136
+ }
137
+ catch (error) {
138
+ return refused(error instanceof errors.JWTExpired ? "expired" : "invalid");
139
+ }
140
+ if (row.state === "revoked")
141
+ return refused("key_revoked");
142
+ const { sub, sbx, epc, pod, iat, exp } = payload;
143
+ if (typeof sbx !== "string" ||
144
+ sbx === "" ||
145
+ (kind === "host" && sub !== sbx) ||
146
+ typeof pod !== "string" ||
147
+ pod === "" ||
148
+ typeof epc !== "number" ||
149
+ !Number.isSafeInteger(epc) ||
150
+ epc < 1 ||
151
+ exp - iat > HOST_TOKEN_TTL_SECONDS + LIFETIME_SLACK_SECONDS)
152
+ return refused("claims");
153
+ return {
154
+ ok: true,
155
+ claims: { tenantId, sandboxId: sbx, epoch: epc, podUid: pod, expiresAt: exp * 1000 },
156
+ };
157
+ }
158
+ /**
159
+ * Why a host's claims are stale, or undefined while they are its sandbox's current host: the
160
+ * sandbox exists as a pod sandbox, is not deleted, lost or expired, and its epoch and pod are
161
+ * the token's. One indexed read of the sandbox's row.
162
+ */
163
+ export async function staleHost(store, claims) {
164
+ const row = await store.tx((t) => t.sandboxResource(claims.sandboxId));
165
+ const pod = row?.pod;
166
+ if (!row || row.kind !== "pod" || !pod)
167
+ return "the sandbox is gone";
168
+ if (pod.desired === "deleted")
169
+ return "the sandbox was deleted";
170
+ if (pod.observed === "lost")
171
+ return "the sandbox was lost";
172
+ if (pod.observed === "expired")
173
+ return "the sandbox expired";
174
+ if (pod.hostEpoch !== claims.epoch)
175
+ return "another host holds the sandbox now";
176
+ if (pod.podUid !== claims.podUid)
177
+ return "the token is another pod's";
178
+ return undefined;
179
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The identity file (Host feature `trusted-issuers`, F9 I2): the YAML file
3
+ * `NYLORUN_IDENTITY_FILE` names, listing the issuers whose JWTs the Tenant API accepts as
4
+ * bearers. Read once at boot (`host/main.ts`); a change takes a restart. A malformed file stops
5
+ * the boot with a message naming the issuer and the field.
6
+ *
7
+ * ```yaml
8
+ * issuers:
9
+ * - name: keycloak
10
+ * issuer: https://sso.acme.dev/realms/eng
11
+ * audience: nylorun
12
+ * jwks: https://sso.acme.dev/realms/eng/protocol/openid-connect/certs # or keys: [<PEM>]
13
+ * subject: "u:{sub}" # scalar claims only
14
+ * scopes: { claim: nylorun_scopes } # or { fixed: [sessions:own, agents:read] }
15
+ * allowedScopes: [agents:read, sessions:own, sandboxes:write, studio]
16
+ * agents: [support] # optional allowlist; absent means all
17
+ * sandboxes: ["{org_id}/*"] # optional grants; absent reaches none
18
+ * maxLifetime: 15m
19
+ * ```
20
+ */
21
+ import { type KeyObject } from "node:crypto";
22
+ import { type IssuerScope } from "@nylorun/core/contracts";
23
+ /** The JWT algorithms an issuer may sign with. */
24
+ export declare const ISSUER_ALGORITHMS: readonly ["RS256", "ES256", "EdDSA"];
25
+ export type IssuerAlgorithm = (typeof ISSUER_ALGORITHMS)[number];
26
+ /** A public key an issuer signs with, and the one algorithm it verifies. */
27
+ export interface IssuerKey {
28
+ readonly key: KeyObject;
29
+ readonly alg: IssuerAlgorithm;
30
+ readonly kid?: string;
31
+ }
32
+ /** One trusted issuer, as the identity file declares it. */
33
+ export interface TrustedIssuerConfig {
34
+ /** Its name: `issuer:<name>` in `GET /v1/me` and the token's role. */
35
+ readonly name: string;
36
+ /** The `iss` its tokens carry, exactly. */
37
+ readonly issuer: string;
38
+ /** The `aud` its tokens must carry. */
39
+ readonly audience: string;
40
+ /** Where its public keys are fetched (a JWKS URL); or `keys`. */
41
+ readonly jwks?: string;
42
+ /** Its public keys, given in the file (PEM); or `jwks`. */
43
+ readonly keys?: readonly IssuerKey[];
44
+ /** The subject template, such as `u:{sub}`: at least one claim reference. */
45
+ readonly subject: string;
46
+ /** Where its tokens' scopes come from: a claim, or a fixed list. */
47
+ readonly scopes: {
48
+ readonly claim: string;
49
+ } | {
50
+ readonly fixed: readonly IssuerScope[];
51
+ };
52
+ /** The scopes its tokens may carry; others are dropped. */
53
+ readonly allowedScopes: readonly IssuerScope[];
54
+ /** The agents its tokens reach; absent reaches all. */
55
+ readonly agents?: readonly string[];
56
+ /** Sandbox grant templates, such as `{org_id}/*`; absent reaches no sandbox. */
57
+ readonly sandboxes?: readonly string[];
58
+ /** The longest `exp - iat` it may sign, in seconds. */
59
+ readonly maxLifetimeSeconds: number;
60
+ }
61
+ /** A malformed identity file: the message names the issuer and the field. */
62
+ export declare class IdentityFileError extends Error {
63
+ constructor(message: string);
64
+ }
65
+ /** The claim names a template references. */
66
+ export declare function claimReferences(template: string): string[];
67
+ /** `template` with every reference replaced by `value(claim)`; undefined when one has none. */
68
+ export declare function renderTemplate(template: string, value: (claim: string) => string | undefined): string | undefined;
69
+ /** A PEM public key an issuer may sign with, and its algorithm; throws a message otherwise. */
70
+ export declare function issuerKey(key: KeyObject, kid?: string): IssuerKey;
71
+ /**
72
+ * The issuers of an identity file's text. Throws `IdentityFileError` naming the file, the issuer
73
+ * and the field when the file is malformed.
74
+ */
75
+ export declare function parseIdentityFile(text: string, source?: string): TrustedIssuerConfig[];
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The identity file (Host feature `trusted-issuers`, F9 I2): the YAML file
3
+ * `NYLORUN_IDENTITY_FILE` names, listing the issuers whose JWTs the Tenant API accepts as
4
+ * bearers. Read once at boot (`host/main.ts`); a change takes a restart. A malformed file stops
5
+ * the boot with a message naming the issuer and the field.
6
+ *
7
+ * ```yaml
8
+ * issuers:
9
+ * - name: keycloak
10
+ * issuer: https://sso.acme.dev/realms/eng
11
+ * audience: nylorun
12
+ * jwks: https://sso.acme.dev/realms/eng/protocol/openid-connect/certs # or keys: [<PEM>]
13
+ * subject: "u:{sub}" # scalar claims only
14
+ * scopes: { claim: nylorun_scopes } # or { fixed: [sessions:own, agents:read] }
15
+ * allowedScopes: [agents:read, sessions:own, sandboxes:write, studio]
16
+ * agents: [support] # optional allowlist; absent means all
17
+ * sandboxes: ["{org_id}/*"] # optional grants; absent reaches none
18
+ * maxLifetime: 15m
19
+ * ```
20
+ */
21
+ import { createPublicKey } from "node:crypto";
22
+ import { parse as parseYaml } from "yaml";
23
+ import { z } from "zod";
24
+ import { ISSUER_SCOPES, isSandboxGrant, TOKEN_SANDBOX_GRANTS_MAX, } from "@nylorun/core/contracts";
25
+ import { parseSandboxDuration } from "@nylorun/core/define";
26
+ /** The JWT algorithms an issuer may sign with. */
27
+ export const ISSUER_ALGORITHMS = ["RS256", "ES256", "EdDSA"];
28
+ /** The longest `maxLifetime` an issuer may set. */
29
+ const MAX_LIFETIME_SECONDS = 24 * 60 * 60;
30
+ /** At most this many static `keys` per issuer. */
31
+ const MAX_STATIC_KEYS = 16;
32
+ const ISSUER_NAME = /^[a-z][a-z0-9-]{0,31}$/;
33
+ /** A claim reference in a template: `{claim}`, a top-level claim name without braces. */
34
+ const CLAIM_REFERENCE = /\{([^{}]+)\}/g;
35
+ /** A malformed identity file: the message names the issuer and the field. */
36
+ export class IdentityFileError extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "IdentityFileError";
40
+ }
41
+ }
42
+ /** The claim names a template references. */
43
+ export function claimReferences(template) {
44
+ return [...template.matchAll(CLAIM_REFERENCE)].map((match) => match[1]);
45
+ }
46
+ /** `template` with every reference replaced by `value(claim)`; undefined when one has none. */
47
+ export function renderTemplate(template, value) {
48
+ let missing = false;
49
+ const rendered = template.replace(CLAIM_REFERENCE, (_, claim) => {
50
+ const found = value(claim);
51
+ if (found === undefined)
52
+ missing = true;
53
+ return found ?? "";
54
+ });
55
+ return missing ? undefined : rendered;
56
+ }
57
+ /** True when every brace in `template` belongs to a `{claim}` reference. */
58
+ function bracesBalanced(template) {
59
+ return !/[{}]/.test(template.replace(CLAIM_REFERENCE, ""));
60
+ }
61
+ /** A PEM public key an issuer may sign with, and its algorithm; throws a message otherwise. */
62
+ export function issuerKey(key, kid) {
63
+ const details = key.asymmetricKeyDetails ?? {};
64
+ switch (key.asymmetricKeyType) {
65
+ case "rsa":
66
+ if ((details.modulusLength ?? 0) < 2048)
67
+ throw new Error("an RSA key must be at least 2048 bits");
68
+ return { key, alg: "RS256", ...(kid ? { kid } : {}) };
69
+ case "ec":
70
+ if (details.namedCurve !== "prime256v1")
71
+ throw new Error("an EC key must be on P-256 (ES256)");
72
+ return { key, alg: "ES256", ...(kid ? { kid } : {}) };
73
+ case "ed25519":
74
+ return { key, alg: "EdDSA", ...(kid ? { kid } : {}) };
75
+ default:
76
+ throw new Error(`a ${key.asymmetricKeyType ?? "secret"} key is not accepted: use RSA (RS256), P-256 (ES256) or Ed25519 (EdDSA)`);
77
+ }
78
+ }
79
+ const template = (what) => z
80
+ .string()
81
+ .min(1)
82
+ .max(200)
83
+ .refine(bracesBalanced, `${what} may use braces only around a claim name, as {sub}`);
84
+ const IssuerScopeSchema = z.enum(ISSUER_SCOPES, {
85
+ error: (issue) => `${String(issue.input)} is not a scope an issuer may grant (one of ${ISSUER_SCOPES.join(", ")})`,
86
+ });
87
+ const IssuerEntrySchema = z
88
+ .object({
89
+ name: z.string().regex(ISSUER_NAME, "must match ^[a-z][a-z0-9-]{0,31}$"),
90
+ issuer: z
91
+ .string()
92
+ .min(1)
93
+ .refine((value) => !value.startsWith("urn:nylorun:"), "urn:nylorun: issuers are the Runtime's own"),
94
+ audience: z.string().min(1),
95
+ jwks: z
96
+ .string()
97
+ .refine((value) => {
98
+ try {
99
+ const url = new URL(value);
100
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }, "must be an http(s) URL without credentials")
106
+ .optional(),
107
+ keys: z.array(z.string().min(1)).min(1).max(MAX_STATIC_KEYS).optional(),
108
+ subject: template("subject").refine((value) => claimReferences(value).length > 0, "must reference a claim, as u:{sub}: without one every token is the same person"),
109
+ scopes: z.union([
110
+ z.object({ claim: z.string().min(1) }).strict(),
111
+ z.object({ fixed: z.array(IssuerScopeSchema) }).strict(),
112
+ ], { error: "must be { claim: <name> } or { fixed: [<scope>, ...] }" }),
113
+ allowedScopes: z.array(IssuerScopeSchema).min(1),
114
+ agents: z.array(z.string().min(1)).optional(),
115
+ sandboxes: z
116
+ .array(template("a sandbox grant").refine(
117
+ // A rendered grant must be a grant: checked here with a sample value per claim.
118
+ (value) => isSandboxGrant(renderTemplate(value, () => "x")), "must render to a sandbox id or a prefix ending in /*, as {org_id}/*"))
119
+ .max(TOKEN_SANDBOX_GRANTS_MAX)
120
+ .optional(),
121
+ maxLifetime: z
122
+ .string()
123
+ .refine((value) => {
124
+ const ms = parseSandboxDuration(value);
125
+ return ms !== undefined && ms >= 1000 && ms <= MAX_LIFETIME_SECONDS * 1000;
126
+ }, "must be a duration from 1s to 24h, as 15m"),
127
+ })
128
+ .strict();
129
+ const IdentityFileSchema = z
130
+ .object({ issuers: z.array(z.unknown()) })
131
+ .strict();
132
+ function issueText(issues) {
133
+ return issues
134
+ .map((issue) => {
135
+ const path = issue.path.map((part) => (typeof part === "number" ? `[${part}]` : `.${String(part)}`)).join("").replace(/^\./, "");
136
+ return `${path || "(entry)"}: ${issue.message}`;
137
+ })
138
+ .join("; ");
139
+ }
140
+ /**
141
+ * The issuers of an identity file's text. Throws `IdentityFileError` naming the file, the issuer
142
+ * and the field when the file is malformed.
143
+ */
144
+ export function parseIdentityFile(text, source = "identity file") {
145
+ const fail = (message) => {
146
+ throw new IdentityFileError(`${source}: ${message}`);
147
+ };
148
+ let raw;
149
+ try {
150
+ raw = parseYaml(text);
151
+ }
152
+ catch (error) {
153
+ return fail(`not YAML: ${error instanceof Error ? error.message : String(error)}`);
154
+ }
155
+ const file = IdentityFileSchema.safeParse(raw ?? {});
156
+ if (!file.success)
157
+ return fail(issueText(file.error.issues));
158
+ const issuers = [];
159
+ file.data.issuers.forEach((entry, index) => {
160
+ const named = entry && typeof entry === "object" && typeof entry.name === "string"
161
+ ? entry.name
162
+ : `#${index + 1}`;
163
+ const where = `issuer ${named}`;
164
+ const parsed = IssuerEntrySchema.safeParse(entry);
165
+ if (!parsed.success)
166
+ return fail(`${where}: ${issueText(parsed.error.issues)}`);
167
+ const value = parsed.data;
168
+ if ((value.jwks === undefined) === (value.keys === undefined))
169
+ return fail(`${where}: jwks or keys: give exactly one of them`);
170
+ if ("fixed" in value.scopes) {
171
+ const outside = value.scopes.fixed.filter((scope) => !value.allowedScopes.includes(scope));
172
+ if (outside.length > 0)
173
+ return fail(`${where}: scopes.fixed: ${outside.join(", ")} not in allowedScopes`);
174
+ }
175
+ const keys = value.keys?.map((pem, keyIndex) => {
176
+ try {
177
+ return issuerKey(createPublicKey(pem));
178
+ }
179
+ catch (error) {
180
+ return fail(`${where}: keys[${keyIndex}]: ${error instanceof Error ? error.message : String(error)}`);
181
+ }
182
+ });
183
+ if (issuers.some((other) => other.name === value.name))
184
+ return fail(`${where}: name: another issuer has this name`);
185
+ if (issuers.some((other) => other.issuer === value.issuer))
186
+ return fail(`${where}: issuer: another issuer has ${value.issuer}`);
187
+ issuers.push({
188
+ name: value.name,
189
+ issuer: value.issuer,
190
+ audience: value.audience,
191
+ ...(value.jwks ? { jwks: value.jwks } : {}),
192
+ ...(keys ? { keys } : {}),
193
+ subject: value.subject,
194
+ scopes: value.scopes,
195
+ allowedScopes: [...new Set(value.allowedScopes)],
196
+ ...(value.agents ? { agents: [...new Set(value.agents)] } : {}),
197
+ ...(value.sandboxes ? { sandboxes: value.sandboxes } : {}),
198
+ maxLifetimeSeconds: Math.floor(parseSandboxDuration(value.maxLifetime) / 1000),
199
+ });
200
+ });
201
+ return issuers;
202
+ }