@atlasauth/emulator-core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/adopt.d.ts +50 -0
  2. package/dist/adopt.js +57 -0
  3. package/dist/adopt.js.map +1 -0
  4. package/dist/coverage.d.ts +15 -0
  5. package/dist/coverage.js +31 -0
  6. package/dist/coverage.js.map +1 -0
  7. package/dist/crypto-util.d.ts +10 -0
  8. package/dist/crypto-util.js +76 -0
  9. package/dist/crypto-util.js.map +1 -0
  10. package/dist/engine.d.ts +33 -2
  11. package/dist/engine.js +53 -8
  12. package/dist/engine.js.map +1 -1
  13. package/dist/error-catalog.js +8 -0
  14. package/dist/error-catalog.js.map +1 -1
  15. package/dist/event-gate.d.ts +3 -0
  16. package/dist/event-gate.js +372 -0
  17. package/dist/event-gate.js.map +1 -0
  18. package/dist/generic.js +9 -1
  19. package/dist/generic.js.map +1 -1
  20. package/dist/handlers/approvals.d.ts +2 -0
  21. package/dist/handlers/approvals.js +365 -0
  22. package/dist/handlers/approvals.js.map +1 -0
  23. package/dist/handlers/authz.d.ts +2 -0
  24. package/dist/handlers/authz.js +347 -0
  25. package/dist/handlers/authz.js.map +1 -0
  26. package/dist/handlers/billing.d.ts +2 -0
  27. package/dist/handlers/billing.js +398 -0
  28. package/dist/handlers/billing.js.map +1 -0
  29. package/dist/handlers/connections.d.ts +2 -0
  30. package/dist/handlers/connections.js +332 -0
  31. package/dist/handlers/connections.js.map +1 -0
  32. package/dist/handlers/device.d.ts +47 -0
  33. package/dist/handlers/device.js +648 -0
  34. package/dist/handlers/device.js.map +1 -0
  35. package/dist/handlers/federation.d.ts +2 -0
  36. package/dist/handlers/federation.js +481 -0
  37. package/dist/handlers/federation.js.map +1 -0
  38. package/dist/handlers/governance.d.ts +2 -0
  39. package/dist/handlers/governance.js +460 -0
  40. package/dist/handlers/governance.js.map +1 -0
  41. package/dist/handlers/hooks.js +4 -1
  42. package/dist/handlers/hooks.js.map +1 -1
  43. package/dist/handlers/index.d.ts +5 -1
  44. package/dist/handlers/index.js +32 -3
  45. package/dist/handlers/index.js.map +1 -1
  46. package/dist/handlers/keys.js +6 -1
  47. package/dist/handlers/keys.js.map +1 -1
  48. package/dist/handlers/machines.js +55 -8
  49. package/dist/handlers/machines.js.map +1 -1
  50. package/dist/handlers/messaging.d.ts +4 -0
  51. package/dist/handlers/messaging.js +486 -0
  52. package/dist/handlers/messaging.js.map +1 -0
  53. package/dist/handlers/oauth-flows.d.ts +28 -0
  54. package/dist/handlers/oauth-flows.js +419 -0
  55. package/dist/handlers/oauth-flows.js.map +1 -0
  56. package/dist/handlers/oauth.d.ts +75 -0
  57. package/dist/handlers/oauth.js +608 -0
  58. package/dist/handlers/oauth.js.map +1 -0
  59. package/dist/handlers/passkeys.d.ts +3 -0
  60. package/dist/handlers/passkeys.js +293 -0
  61. package/dist/handlers/passkeys.js.map +1 -0
  62. package/dist/handlers/platform.d.ts +15 -0
  63. package/dist/handlers/platform.js +454 -0
  64. package/dist/handlers/platform.js.map +1 -0
  65. package/dist/handlers/round2.d.ts +8 -0
  66. package/dist/handlers/round2.js +372 -0
  67. package/dist/handlers/round2.js.map +1 -0
  68. package/dist/handlers/scim.d.ts +2 -0
  69. package/dist/handlers/scim.js +408 -0
  70. package/dist/handlers/scim.js.map +1 -0
  71. package/dist/handlers/sessions.d.ts +7 -3
  72. package/dist/handlers/sessions.js +55 -25
  73. package/dist/handlers/sessions.js.map +1 -1
  74. package/dist/handlers/support.d.ts +20 -0
  75. package/dist/handlers/support.js +62 -0
  76. package/dist/handlers/support.js.map +1 -0
  77. package/dist/handlers/users.js +4 -0
  78. package/dist/handlers/users.js.map +1 -1
  79. package/dist/handlers/workflows.d.ts +2 -0
  80. package/dist/handlers/workflows.js +428 -0
  81. package/dist/handlers/workflows.js.map +1 -0
  82. package/dist/index.d.ts +8 -0
  83. package/dist/index.js +23 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/jws.d.ts +41 -0
  86. package/dist/jws.js +133 -0
  87. package/dist/jws.js.map +1 -0
  88. package/dist/messages.d.ts +7 -0
  89. package/dist/messages.js +8 -4
  90. package/dist/messages.js.map +1 -1
  91. package/dist/parity-bin.js +3 -1
  92. package/dist/parity-bin.js.map +1 -1
  93. package/dist/parity-routes.d.ts +14 -0
  94. package/dist/parity-routes.js +421 -0
  95. package/dist/parity-routes.js.map +1 -0
  96. package/dist/parity.d.ts +10 -3
  97. package/dist/parity.js +14 -5
  98. package/dist/parity.js.map +1 -1
  99. package/dist/tokens.d.ts +17 -0
  100. package/dist/tokens.js +46 -0
  101. package/dist/tokens.js.map +1 -1
  102. package/dist/webauthn-virtual.d.ts +70 -0
  103. package/dist/webauthn-virtual.js +186 -0
  104. package/dist/webauthn-virtual.js.map +1 -0
  105. package/dist/webhook-ext.d.ts +16 -0
  106. package/dist/webhook-ext.js +218 -0
  107. package/dist/webhook-ext.js.map +1 -0
  108. package/dist/webhooks.d.ts +3 -0
  109. package/dist/webhooks.js +37 -3
  110. package/dist/webhooks.js.map +1 -1
  111. package/package.json +2 -2
@@ -0,0 +1,648 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cnfOf = exports.presentAccessRequest = exports.presentAgent = exports.tokenGrant = exports.resetDevice = void 0;
4
+ exports.resourceServerOf = resourceServerOf;
5
+ exports.agentGrant = agentGrant;
6
+ exports.deviceHandlers = deviceHandlers;
7
+ exports.enforceTokenBinding = enforceTokenBinding;
8
+ exports.issueMachineToken = issueMachineToken;
9
+ exports.provedBindingOf = provedBindingOf;
10
+ const crypto_util_1 = require("../crypto-util");
11
+ const errors_1 = require("../errors");
12
+ const store_1 = require("../store");
13
+ const jws_1 = require("../jws");
14
+ const util_1 = require("./util");
15
+ const CHALLENGE_TTL_MS = 5 * 60_000;
16
+ const MACHINE_TOKEN_TTL_S = 600;
17
+ const USER_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ'; // no look-alikes
18
+ const hashOf = async (s) => (0, crypto_util_1.bytesToB64url)(await (0, crypto_util_1.sha256)(s));
19
+ const defaultAgentConfig = () => ({ enabled: false, allow_anonymous: false, allow_service_auth: false, allow_refresh: false, untrusted_scopes: [], trusted_scopes: [], claim_ttl_seconds: 600, auto_approve_rules: [] });
20
+ const states = new WeakMap();
21
+ const st = (e) => {
22
+ let s = states.get(e);
23
+ if (!s)
24
+ states.set(e, (s = { nonces: new Map(), lookup: new Map(), agentConfig: defaultAgentConfig() }));
25
+ return s;
26
+ };
27
+ const resetDevice = (e) => {
28
+ states.delete(e);
29
+ dpopStore.delete(e);
30
+ };
31
+ exports.resetDevice = resetDevice;
32
+ /** The unified `grants` claim entry (machine and agent grants share one shape). */
33
+ const tokenGrant = (g) => ({
34
+ type: g.type,
35
+ id: g.id,
36
+ resource: g.resource,
37
+ scopes: [...g.scopes],
38
+ ...(g.kind ? { kind: g.kind } : {}),
39
+ ...(g.constraints ? { constraints: g.constraints } : {}),
40
+ ...(g.expiresAt ? { exp: Math.floor(g.expiresAt / 1000) } : {}),
41
+ });
42
+ exports.tokenGrant = tokenGrant;
43
+ /** A resource server by identifier or id (the generic CRUD layer stores them), or a bare identifier when none is stored. */
44
+ function resourceServerOf(e, ref) {
45
+ const row = e.generic.get('resource_servers')?.find((r) => r.id === ref || r.identifier === ref) ?? null;
46
+ return row ? { id: row.id, identifier: row.identifier ?? row.id, row } : { id: null, identifier: ref, row: null };
47
+ }
48
+ const deviceView = (e, m) => e.shape('Machine', {
49
+ object: 'machine',
50
+ id: m.id,
51
+ name: m.name,
52
+ organization_id: m.organization_id ?? null,
53
+ owner_user_id: m.owner_user_id ?? null,
54
+ status: m.status,
55
+ effective_status: m.status,
56
+ metadata: m.metadata ?? {},
57
+ created_at: m.created_at,
58
+ updated_at: m.updated_at,
59
+ });
60
+ const userCode = () => {
61
+ const raw = crypto.getRandomValues(new Uint8Array(8));
62
+ return [...raw].map((b) => USER_CODE_ALPHABET[b % USER_CODE_ALPHABET.length]).join('');
63
+ };
64
+ const normCode = (s) => s.replace(/[\s-]/g, '').toUpperCase();
65
+ async function resolveEnrolment(e, presented) {
66
+ const key = /^[A-Za-z]{4}[\s-]?[A-Za-z]{4}$/.test(presented.trim()) ? `code:${await hashOf(normCode(presented))}` : `tok:${await hashOf(presented.trim())}`;
67
+ const row = e.db.enrolmentTokens.get(st(e).lookup.get(key));
68
+ if (!row)
69
+ throw new errors_1.EmulatorError('ENROLMENT_TOKEN_INVALID', 'The enrolment token is not valid.');
70
+ if (row.expires_at <= e.clock.now())
71
+ throw new errors_1.EmulatorError('ENROLMENT_TOKEN_EXPIRED', 'The enrolment token has expired.');
72
+ if (row.uses >= row.max_uses)
73
+ throw new errors_1.EmulatorError('ENROLMENT_TOKEN_EXHAUSTED', 'The enrolment token has no uses left.');
74
+ return row;
75
+ }
76
+ const presentEnrolmentToken = (t) => ({
77
+ object: 'machine_enrolment_token',
78
+ id: t.id,
79
+ format: t.format,
80
+ organization_id: t.organization_id ?? null,
81
+ max_uses: t.max_uses,
82
+ uses: t.uses,
83
+ max_machines: t.max_machines ?? null,
84
+ requires_approval: t.requires_approval === true,
85
+ single_session: t.single_session === true,
86
+ owner_user_id: t.owner_user_id ?? null,
87
+ data: t.data ?? {},
88
+ expires_at: t.expires_at,
89
+ created_at: t.created_at,
90
+ });
91
+ // ── agents ─────────────────────────────────────────────────────────────
92
+ const presentAgent = (m) => ({
93
+ object: 'agent',
94
+ id: m.id,
95
+ name: m.name,
96
+ identity: m.agent?.identity ?? null,
97
+ tier: m.agent?.tier ?? null,
98
+ status: m.status,
99
+ owner_user_id: m.owner_user_id ?? null,
100
+ organization_id: m.organization_id ?? null,
101
+ claimed_at: m.agent?.claimed_at ?? null,
102
+ created_at: m.created_at,
103
+ });
104
+ exports.presentAgent = presentAgent;
105
+ const presentAccessRequest = (r) => ({
106
+ object: 'agent_access_request',
107
+ id: r.id,
108
+ agent_id: r.machine_id,
109
+ kind: r.kind,
110
+ resource: r.resource,
111
+ scopes: r.scopes,
112
+ reason: r.reason ?? null,
113
+ status: r.status,
114
+ constraints: r.constraints ?? null,
115
+ auto_approved: r.auto_approved === true,
116
+ decided_by: r.decided_by ?? null,
117
+ decided_at: r.decided_at ?? null,
118
+ expires_at: r.expires_at ?? null,
119
+ created_at: r.created_at,
120
+ });
121
+ exports.presentAccessRequest = presentAccessRequest;
122
+ /** An agent grant is an approved, unexpired access request. The usage routes (`/v1/grants/agent/:id/...`) resolve through this. */
123
+ function agentGrant(e, id) {
124
+ const r = e.db.agentRequests.get(id);
125
+ if (!r || r.status !== 'approved')
126
+ return undefined;
127
+ return { id: r.id, machine_id: r.machine_id, resource_server_id: r.resource, scopes: r.scopes, constraints: r.constraints ?? null, expires_at: r.expires_at ?? null, kind: r.kind, created_at: r.created_at, updated_at: r.decided_at ?? r.created_at };
128
+ }
129
+ const liveAgentGrants = (e, machineId) => e.db.agentRequests
130
+ .filter((r) => r.machine_id === machineId && r.status === 'approved' && !(r.expires_at && r.expires_at <= e.clock.now()))
131
+ .map((r) => agentGrant(e, r.id));
132
+ const ruleMatches = (rules, kind, resource) => (rules ?? []).some((r) => (r.kind === undefined || r.kind === kind) && (r.resource === undefined || r.resource === resource || (typeof r.resource === 'string' && r.resource.endsWith('*') && resource.startsWith(r.resource.slice(0, -1)))));
133
+ const agentRow = (e, id) => {
134
+ const m = e.db.machines.get(id);
135
+ if (!m || m.deleted || !m.agent)
136
+ throw (0, errors_1.notFound)('Agent');
137
+ return m;
138
+ };
139
+ function deviceHandlers() {
140
+ const agentCfg = (e) => st(e).agentConfig;
141
+ const enabled = (e) => {
142
+ const c = agentCfg(e);
143
+ // A disabled instance is a plain 404 so its existence is not revealed.
144
+ if (!c.enabled)
145
+ throw (0, errors_1.notFound)('Instance');
146
+ return c;
147
+ };
148
+ const agentByCredentials = (e, id, secret) => {
149
+ if (typeof id !== 'string' || !id)
150
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'An `agent_id` is required.', 'agent_id');
151
+ if (typeof secret !== 'string' || !secret)
152
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'An `agent_secret` is required.', 'agent_secret');
153
+ const m = e.db.machines.get(id);
154
+ if (!m || m.deleted || !m.agent || m.secret !== secret || m.status !== 'active')
155
+ throw new errors_1.EmulatorError('UNAUTHENTICATED', 'Invalid agent credentials.');
156
+ return m;
157
+ };
158
+ const scopesFor = (cfg, tier) => (tier === 'trusted' ? cfg.trusted_scopes : cfg.untrusted_scopes) ?? [];
159
+ const decide = (e, r, status, by, extra = {}) => {
160
+ r.status = status;
161
+ r.decided_by = by;
162
+ r.decided_at = e.clock.now();
163
+ r.auto_approved = extra.auto === true;
164
+ if (extra.expiresInSeconds)
165
+ r.expires_at = e.clock.now() + extra.expiresInSeconds * 1000;
166
+ if (extra.constraints)
167
+ r.constraints = extra.constraints;
168
+ e.webhooks.emit('agent.access_decided', (0, exports.presentAccessRequest)(r));
169
+ return r;
170
+ };
171
+ return [
172
+ // ── machine device-key flow (public: the presented credential is the authentication) ──
173
+ {
174
+ method: 'POST',
175
+ pattern: '/v1/machine_enrolment_tokens',
176
+ handler: async (c) => {
177
+ const e = c.engine;
178
+ const format = c.body.format === 'user_code' ? 'user_code' : 'token';
179
+ let maxUses = typeof c.body.max_uses === 'number' ? c.body.max_uses : 1;
180
+ let ttl = typeof c.body.expires_in_seconds === 'number' ? c.body.expires_in_seconds : 3600;
181
+ if (format === 'user_code') {
182
+ if (maxUses > 1)
183
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'A user_code is single use.', 'max_uses');
184
+ maxUses = 1;
185
+ ttl = typeof c.body.expires_in_seconds === 'number' ? c.body.expires_in_seconds : 600;
186
+ if (ttl < 300 || ttl > 900)
187
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'expires_in_seconds must be 300 to 900 for a user_code.', 'expires_in_seconds');
188
+ }
189
+ if (c.body.owner_user_id && !e.db.users.get(c.body.owner_user_id))
190
+ throw new errors_1.EmulatorError('NOT_FOUND', 'owner_user_id is not a user.', 'owner_user_id');
191
+ if (c.body.organization_id && !e.db.organizations.get(c.body.organization_id))
192
+ throw new errors_1.EmulatorError('NOT_FOUND', 'organization_id is not an organization.', 'organization_id');
193
+ const secret = `met_${(0, store_1.randomToken)(24)}`;
194
+ const code = format === 'user_code' ? userCode() : null;
195
+ const now = e.clock.now();
196
+ const row = e.db.enrolmentTokens.insert({
197
+ format,
198
+ organization_id: c.body.organization_id ?? null,
199
+ max_uses: maxUses,
200
+ uses: 0,
201
+ max_machines: c.body.max_machines ?? null,
202
+ requires_approval: c.body.requires_approval === true,
203
+ single_session: c.body.single_session === true,
204
+ owner_user_id: c.body.owner_user_id ?? null,
205
+ data: c.body.data ?? {},
206
+ expires_at: now + ttl * 1000,
207
+ created_at: now,
208
+ });
209
+ st(e).lookup.set(code ? `code:${await hashOf(code)}` : `tok:${await hashOf(secret)}`, row.id);
210
+ return { status: 201, body: { ...presentEnrolmentToken(row), ...(code ? { user_code: `${code.slice(0, 4)}-${code.slice(4)}` } : { secret }) } };
211
+ },
212
+ },
213
+ {
214
+ method: 'GET',
215
+ pattern: '/v1/machine_enrolment_tokens',
216
+ handler: (c) => {
217
+ const org = c.query.get('organization_id');
218
+ return (0, util_1.listReply)(c, c.engine.db.enrolmentTokens.filter((t) => org === null || (org === 'null' ? !t.organization_id : t.organization_id === org)), presentEnrolmentToken);
219
+ },
220
+ },
221
+ {
222
+ method: 'DELETE',
223
+ pattern: '/v1/machine_enrolment_tokens/:id',
224
+ handler: (c) => {
225
+ const t = c.engine.db.enrolmentTokens.get(c.params.id);
226
+ if (!t)
227
+ throw (0, errors_1.notFound)('Enrolment token');
228
+ c.engine.db.enrolmentTokens.delete(t.id);
229
+ return { body: { object: 'machine_enrolment_token', id: t.id, deleted: true } };
230
+ },
231
+ },
232
+ {
233
+ method: 'POST',
234
+ pattern: '/v1/machines/enroll',
235
+ public: true,
236
+ handler: async (c) => {
237
+ const e = c.engine;
238
+ const presented = String((0, util_1.need)(c.body, 'enrolment_token'));
239
+ const pem = String((0, util_1.need)(c.body, 'public_key_pem'));
240
+ if (!(await (0, crypto_util_1.isDeviceKey)(pem)))
241
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'public_key_pem must be an SPKI PEM Ed25519, ECDSA P-256 or RSA public key.', 'public_key_pem');
242
+ const tok = await resolveEnrolment(e, presented);
243
+ const deviceKey = typeof c.body.device_key === 'string' ? c.body.device_key : null;
244
+ const now = e.clock.now();
245
+ const superseded = deviceKey ? e.db.machines.filter((m) => !m.deleted && m.device_key === deviceKey && m.status !== 'revoked') : [];
246
+ // The cap counts this token's live machines, after a re-enrolment frees its own slot.
247
+ const mine = e.db.machines.filter((m) => !m.deleted && m.enrolment_token_id === tok.id && m.status !== 'revoked' && !superseded.some((x) => x.id === m.id));
248
+ if (tok.max_machines && mine.length >= tok.max_machines)
249
+ throw new errors_1.EmulatorError('FORBIDDEN', `This enrolment token is capped at ${tok.max_machines} machines.`);
250
+ for (const prior of superseded) {
251
+ prior.status = 'revoked';
252
+ prior.updated_at = now;
253
+ e.changed({ type: 'machine.revoked', data: { ...deviceView(e, prior), reason: 'superseded' }, resource: 'machine', id: prior.id, op: 'update' });
254
+ }
255
+ if (!superseded.length)
256
+ tok.uses += 1; // a supersede spends no extra use
257
+ const status = tok.requires_approval ? 'pending' : 'active';
258
+ const m = e.db.machines.insert({
259
+ name: typeof c.body.name === 'string' ? c.body.name : 'device',
260
+ owner_user_id: tok.owner_user_id ?? null,
261
+ organization_id: tok.organization_id ?? null,
262
+ status,
263
+ metadata: { ...(tok.data ?? {}), ...(c.body.metadata ?? {}) },
264
+ secret: e.tokens.secret('mk'),
265
+ public_key_pem: pem,
266
+ public_key_jkt: await (0, crypto_util_1.jwkThumbprint)(pem),
267
+ device_key: deviceKey,
268
+ enrolment_token_id: tok.id,
269
+ enrolled_at: now,
270
+ created_at: now,
271
+ updated_at: now,
272
+ });
273
+ e.changed({ type: 'machine.enrolled', data: deviceView(e, m), resource: 'machine', id: m.id, op: 'create' });
274
+ return { status: 201, body: { object: 'machine', id: m.id, name: m.name, organization_id: m.organization_id, status, public_key_jkt: m.public_key_jkt, device_key: deviceKey, enrolled_at: now, approval_required: status === 'pending' } };
275
+ },
276
+ },
277
+ {
278
+ method: 'POST',
279
+ pattern: '/v1/machines/redeem',
280
+ public: true,
281
+ handler: async (c) => {
282
+ const tok = await resolveEnrolment(c.engine, String((0, util_1.need)(c.body, 'enrolment_token')));
283
+ tok.uses += 1;
284
+ return { body: { object: 'machine_enrolment_redemption', id: tok.id, data: tok.data ?? {}, owner_user_id: tok.owner_user_id ?? null, organization_id: tok.organization_id ?? null } };
285
+ },
286
+ },
287
+ {
288
+ method: 'POST',
289
+ pattern: '/v1/machines/challenge',
290
+ public: true,
291
+ handler: (c) => {
292
+ const e = c.engine;
293
+ const m = e.db.machines.get(String((0, util_1.need)(c.body, 'machine_id')));
294
+ if (!m || m.deleted)
295
+ throw (0, errors_1.notFound)('Machine');
296
+ if (!m.public_key_pem)
297
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'This machine has no registered key; enrol it first.', 'machine_id');
298
+ const nonce = `${(0, store_1.randomToken)(24)}.${e.clock.now() + CHALLENGE_TTL_MS}`;
299
+ st(e).nonces.set(nonce, { machineId: m.id, expiresAt: e.clock.now() + CHALLENGE_TTL_MS, spent: false });
300
+ return { body: { object: 'machine_challenge', nonce, expires_in_seconds: CHALLENGE_TTL_MS / 1000 } };
301
+ },
302
+ },
303
+ { method: 'POST', pattern: '/v1/machines/token', public: true, handler: (c) => machineToken(c) },
304
+ // ── agents ──
305
+ { method: 'GET', pattern: '/v1/agent_registration', handler: (c) => ({ body: { object: 'agent_registration', ...agentCfg(c.engine) } }) },
306
+ {
307
+ method: 'PUT',
308
+ pattern: '/v1/agent_registration',
309
+ handler: (c) => {
310
+ const cfg = agentCfg(c.engine);
311
+ for (const k of Object.keys(defaultAgentConfig()))
312
+ if (k in c.body)
313
+ cfg[k] = c.body[k];
314
+ return { body: { object: 'agent_registration', ...cfg } };
315
+ },
316
+ },
317
+ {
318
+ method: 'POST',
319
+ pattern: '/v1/agents/register',
320
+ public: true,
321
+ handler: (c) => {
322
+ const e = c.engine;
323
+ const cfg = enabled(e);
324
+ const identity = c.body.identity;
325
+ if (identity === 'refresh') {
326
+ if (!cfg.allow_refresh)
327
+ throw new errors_1.EmulatorError('FORBIDDEN', 'Refresh registration is not enabled.');
328
+ const m = agentByCredentials(e, c.body.agent_id, c.body.agent_secret);
329
+ m.secret = e.tokens.secret('mk');
330
+ m.updated_at = e.clock.now();
331
+ return { status: 200, body: { ...(0, exports.presentAgent)(m), scopes: scopesFor(cfg, m.agent.tier), secret: m.secret } };
332
+ }
333
+ if (identity !== 'anonymous' && identity !== 'service_auth')
334
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', '`identity` must be anonymous, service_auth or refresh.', 'identity');
335
+ const name = typeof c.body.name === 'string' ? c.body.name.trim() : '';
336
+ if (!name || name.length > 120)
337
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'A `name` of 1-120 characters is required.', 'name');
338
+ let tier = 'untrusted';
339
+ let owner = null;
340
+ if (identity === 'anonymous') {
341
+ if (!cfg.allow_anonymous)
342
+ throw new errors_1.EmulatorError('FORBIDDEN', 'Anonymous agent registration is not enabled.');
343
+ }
344
+ else {
345
+ if (!cfg.allow_service_auth)
346
+ throw new errors_1.EmulatorError('FORBIDDEN', 'Service-authenticated agent registration is not enabled.');
347
+ // The caller proves it is the tenant's backend with a secret key; the agent starts trusted.
348
+ const key = e.authenticateKey(c.request);
349
+ if (key.scopes && !key.scopes.includes('machines:write') && !key.scopes.includes('*'))
350
+ throw new errors_1.EmulatorError('SCOPE_MISSING', 'This key lacks the `machines:write` scope.', undefined, { meta: { required_scope: 'machines:write' } });
351
+ tier = 'trusted';
352
+ if (typeof c.body.owner_user_id === 'string') {
353
+ if (!e.db.users.get(c.body.owner_user_id))
354
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'Unknown user.', 'owner_user_id');
355
+ owner = c.body.owner_user_id;
356
+ }
357
+ }
358
+ const now = e.clock.now();
359
+ const secret = e.tokens.secret('mk');
360
+ const m = e.db.machines.insert({ name, owner_user_id: owner, organization_id: c.body.organization_id ?? null, status: 'active', metadata: c.body.metadata ?? {}, secret, agent: { identity, tier }, created_at: now, updated_at: now });
361
+ return { status: 201, body: { ...(0, exports.presentAgent)(m), scopes: scopesFor(cfg, tier), secret } };
362
+ },
363
+ },
364
+ {
365
+ method: 'POST',
366
+ pattern: '/v1/agents/token',
367
+ public: true,
368
+ handler: async (c) => {
369
+ const e = c.engine;
370
+ const cfg = enabled(e);
371
+ const m = agentByCredentials(e, c.body.agent_id, c.body.agent_secret);
372
+ const tier = m.agent.tier;
373
+ const scopes = scopesFor(cfg, tier);
374
+ const grants = liveAgentGrants(e, m.id);
375
+ const token = await e.tokens.signClaims({
376
+ sub: m.id,
377
+ jti: e.db.newId('m2mToken'),
378
+ token_use: 'm2m',
379
+ agent_tier: tier,
380
+ scope: scopes.join(' '),
381
+ ...(m.organization_id ? { org_id: m.organization_id } : {}),
382
+ grants: grants.map((g) => (0, exports.tokenGrant)({ type: 'agent', id: g.id, resource: g.resource_server_id, scopes: g.scopes, kind: g.kind, constraints: g.constraints, expiresAt: g.expires_at })),
383
+ agent_grants: grants.map((g) => ({ grant_id: g.id, grant_type: 'agent', kind: g.kind, resource: g.resource_server_id, scopes: g.scopes, ...(g.constraints ? { constraints: g.constraints } : {}) })),
384
+ ...(m.owner_user_id ? { act_for: m.owner_user_id } : {}),
385
+ }, MACHINE_TOKEN_TTL_S);
386
+ return { headers: { 'cache-control': 'no-store' }, body: { object: 'agent_token', token, token_type: 'Bearer', expires_in: MACHINE_TOKEN_TTL_S, agent_id: m.id, tier, scope: scopes.join(' ') } };
387
+ },
388
+ },
389
+ {
390
+ method: 'POST',
391
+ pattern: '/v1/agents/claim',
392
+ public: true,
393
+ handler: (c) => {
394
+ const e = c.engine;
395
+ const cfg = enabled(e);
396
+ const m = agentByCredentials(e, c.body.agent_id, c.body.agent_secret);
397
+ if (m.owner_user_id)
398
+ throw new errors_1.EmulatorError('CONFLICT', 'This agent is already claimed.');
399
+ const code = userCode();
400
+ m.agent.claim = { code, expires_at: e.clock.now() + cfg.claim_ttl_seconds * 1000 };
401
+ return { headers: { 'cache-control': 'no-store' }, body: { object: 'agent_claim', agent_id: m.id, user_code: code, expires_in: cfg.claim_ttl_seconds, claim_endpoint: '/v1/client/me/agents/claim' } };
402
+ },
403
+ },
404
+ {
405
+ method: 'POST',
406
+ pattern: '/v1/agents/access_requests',
407
+ public: true,
408
+ handler: (c) => {
409
+ const e = c.engine;
410
+ const cfg = enabled(e);
411
+ const m = agentByCredentials(e, c.body.agent_id, c.body.agent_secret);
412
+ if (c.body.kind !== 'tool' && c.body.kind !== 'connection')
413
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', '`kind` must be tool or connection.', 'kind');
414
+ const resource = typeof c.body.resource === 'string' ? c.body.resource.trim() : '';
415
+ if (!resource || resource.length > 200)
416
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'A `resource` of 1-200 characters is required.', 'resource');
417
+ const scopes = c.body.scopes ?? [];
418
+ if (!Array.isArray(scopes) || !scopes.every((x) => typeof x === 'string' && x.length > 0 && x.length <= 100))
419
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', '`scopes` must be an array of strings.', 'scopes');
420
+ if (e.db.agentRequests.filter((r) => r.machine_id === m.id && r.status === 'pending').length >= 20)
421
+ throw new errors_1.EmulatorError('RATE_LIMITED', 'Too many pending access requests for this agent.');
422
+ const r = e.db.agentRequests.insert({ machine_id: m.id, kind: c.body.kind, resource, scopes, constraints: c.body.constraints ?? null, reason: c.body.reason ?? null, status: 'pending', auto_approved: false, created_at: e.clock.now() });
423
+ // Only a claimed (trusted) agent can be auto-approved: an unclaimed one has no accountable owner.
424
+ if (m.agent.tier === 'trusted' && ruleMatches(cfg.auto_approve_rules, r.kind, r.resource))
425
+ decide(e, r, 'approved', 'auto_approve_rule', { auto: true });
426
+ else
427
+ e.webhooks.emit('agent.access_requested', { ...(0, exports.presentAccessRequest)(r), agent: (0, exports.presentAgent)(m) });
428
+ return { status: 201, body: (0, exports.presentAccessRequest)(r) };
429
+ },
430
+ },
431
+ {
432
+ method: 'GET',
433
+ pattern: '/v1/agent_access_requests',
434
+ handler: (c) => {
435
+ const status = c.query.get('status');
436
+ const agent = c.query.get('agent_id');
437
+ return (0, util_1.listReply)(c, c.engine.db.agentRequests.filter((r) => (!status || r.status === status) && (!agent || r.machine_id === agent)), exports.presentAccessRequest);
438
+ },
439
+ },
440
+ ...['approve', 'deny'].map((verb) => ({
441
+ method: 'POST',
442
+ pattern: `/v1/agent_access_requests/:id/${verb}`,
443
+ handler: (c) => {
444
+ const e = c.engine;
445
+ const r = e.db.agentRequests.get(c.params.id);
446
+ if (!r)
447
+ throw (0, errors_1.notFound)('Access request');
448
+ const secs = c.body.expires_in_seconds;
449
+ if (secs !== undefined && (!Number.isInteger(secs) || secs < 60 || secs > 31_536_000))
450
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', '`expires_in_seconds` must be an integer between 60 and 31536000.', 'expires_in_seconds');
451
+ if (r.status !== 'pending')
452
+ throw new errors_1.EmulatorError('CONFLICT', 'This request is no longer pending.');
453
+ return { body: (0, exports.presentAccessRequest)(decide(e, r, verb === 'approve' ? 'approved' : 'denied', c.body.decided_by ?? 'api_key', { expiresInSeconds: secs, constraints: c.body.constraints })) };
454
+ },
455
+ })),
456
+ {
457
+ method: 'GET',
458
+ pattern: '/v1/agents',
459
+ handler: (c) => {
460
+ const owner = c.query.get('owner_user_id');
461
+ const tier = c.query.get('tier');
462
+ return (0, util_1.listReply)(c, c.engine.db.machines.filter((m) => !m.deleted && m.agent && (!owner || m.owner_user_id === owner) && (!tier || m.agent.tier === tier)), exports.presentAgent);
463
+ },
464
+ },
465
+ { method: 'GET', pattern: '/v1/agents/:id', handler: (c) => ({ body: (0, exports.presentAgent)(agentRow(c.engine, c.params.id)) }) },
466
+ {
467
+ method: 'POST',
468
+ pattern: '/v1/agents/:id/claim',
469
+ handler: (c) => {
470
+ const e = c.engine;
471
+ const m = agentRow(e, c.params.id);
472
+ const uid = String((0, util_1.need)(c.body, 'user_id'));
473
+ if (!e.db.users.get(uid))
474
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'Unknown user.', 'user_id');
475
+ m.agent = { identity: m.agent.identity, tier: 'trusted', claimed_at: e.clock.now() };
476
+ m.owner_user_id = uid;
477
+ return { body: (0, exports.presentAgent)(m) };
478
+ },
479
+ },
480
+ {
481
+ method: 'PUT',
482
+ pattern: '/v1/agents/:id/owner',
483
+ handler: (c) => {
484
+ const e = c.engine;
485
+ const m = agentRow(e, c.params.id);
486
+ const uid = c.body.user_id ?? null;
487
+ if (uid !== null && !e.db.users.get(uid))
488
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'Unknown user.', 'user_id');
489
+ m.owner_user_id = uid;
490
+ return { body: (0, exports.presentAgent)(m) };
491
+ },
492
+ },
493
+ {
494
+ method: 'POST',
495
+ pattern: '/v1/agents/:id/revoke_access',
496
+ handler: (c) => {
497
+ const e = c.engine;
498
+ const m = agentRow(e, c.params.id);
499
+ const grants = e.db.agentRequests.filter((x) => x.machine_id === m.id && x.status === 'approved');
500
+ for (const r of grants)
501
+ r.status = 'revoked';
502
+ if (c.body.disable === true)
503
+ m.status = 'revoked';
504
+ return { body: { ...(0, exports.presentAgent)(m), revoked_grants: grants.length } };
505
+ },
506
+ },
507
+ ];
508
+ }
509
+ /** Throws the validation error a machine's `token_binding` demands when `proved` does not satisfy it. */
510
+ function enforceTokenBinding(m, proved) {
511
+ const b = m.token_binding ?? 'none';
512
+ if (b === 'dpop' && !proved.jkt)
513
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', "This machine's tokens must be DPoP-bound: send a DPoP proof.", 'dpop');
514
+ if (b === 'mtls' && !proved.x5t)
515
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', "This machine's tokens must be certificate-bound: present a client certificate.", 'mtls');
516
+ if (b === 'any' && !proved.jkt && !proved.x5t)
517
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', "This machine's tokens must be sender-constrained: send a DPoP proof or present a client certificate.", 'dpop');
518
+ }
519
+ const cnfOf = (p) => (p.jkt || p.x5t ? { ...(p.jkt ? { jkt: p.jkt } : {}), ...(p.x5t ? { 'x5t#S256': p.x5t } : {}) } : undefined);
520
+ exports.cnfOf = cnfOf;
521
+ /**
522
+ * Mint a machine token for `m` from the request fields (`audience`, `scopes`, `organization_id`). Every route that
523
+ * issues one (signed challenge, federation) goes through here, so claims never differ by way in. `beforeIssue` runs after
524
+ * every refusal and before the token is signed (the challenge route spends its nonce there).
525
+ */
526
+ async function issueMachineToken(c, m, req, opts = {}) {
527
+ const e = c.engine;
528
+ let bound = null;
529
+ const audience = typeof req.audience === 'string' ? req.audience : null;
530
+ if (audience) {
531
+ const rs = resourceServerOf(e, audience);
532
+ const now = e.clock.now();
533
+ const grant = e.db.grants.find((g) => g.machine_id === m.id && (g.resource_server_id === rs.id || g.resource_server_id === rs.identifier) && !(g.expires_at && g.expires_at <= now));
534
+ if (!grant)
535
+ throw new errors_1.EmulatorError('FORBIDDEN', 'This machine has no grant for that resource server.');
536
+ const asked = Array.isArray(req.scopes) ? req.scopes : [];
537
+ const scopes = asked.length ? asked : [...grant.scopes];
538
+ const outside = scopes.filter((s) => !grant.scopes.includes(s));
539
+ if (outside.length)
540
+ throw new errors_1.EmulatorError('FORBIDDEN', `The grant does not include: ${outside.join(' ')}.`, 'scopes');
541
+ bound = { rs, grant, scopes };
542
+ }
543
+ else if (req.scopes || req.scope_presets)
544
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', '`scopes` needs an `audience`.', 'audience');
545
+ let orgId = m.organization_id ?? null;
546
+ const orgClaims = {};
547
+ if (req.organization_id) {
548
+ orgId = String(req.organization_id);
549
+ const mm = e.generic.get('organizations/machine_memberships')?.find((x) => x._parent === orgId && x.machine_id === m.id);
550
+ if (orgId !== m.organization_id && !mm)
551
+ throw new errors_1.EmulatorError('FORBIDDEN', 'The machine is not a member of that organization.', 'organization_id');
552
+ const org = e.db.organizations.get(orgId);
553
+ if (org)
554
+ Object.assign(orgClaims, { org_slug: org.slug, ...(mm?.role ? { org_role: mm.role } : {}) });
555
+ }
556
+ opts.beforeIssue?.();
557
+ const ttl = Math.min(MACHINE_TOKEN_TTL_S, opts.maxTtlSeconds ?? MACHINE_TOKEN_TTL_S);
558
+ const cnf = (0, exports.cnfOf)(opts.proved ?? {});
559
+ const token = await e.tokens.signClaims({
560
+ sub: m.id,
561
+ jti: e.db.newId('m2mToken'),
562
+ token_use: 'm2m',
563
+ ...(orgId ? { org_id: orgId } : {}),
564
+ ...orgClaims,
565
+ ...(cnf ? { cnf } : {}),
566
+ ...(opts.extraClaims ?? {}),
567
+ ...(bound
568
+ ? {
569
+ aud: bound.rs.identifier,
570
+ scope: bound.scopes.join(' '),
571
+ grant_id: bound.grant.id,
572
+ grant_type: 'machine',
573
+ grants: [(0, exports.tokenGrant)({ type: 'machine', id: bound.grant.id, resource: bound.rs.identifier, scopes: bound.scopes, constraints: bound.grant.constraints, expiresAt: bound.grant.expires_at })],
574
+ ...(bound.grant.constraints ? { constraints: bound.grant.constraints } : {}),
575
+ }
576
+ : {}),
577
+ }, ttl);
578
+ m.metadata = { ...(m.metadata ?? {}), last_seen_at: e.clock.now() };
579
+ return {
580
+ body: {
581
+ object: 'machine_token',
582
+ token,
583
+ token_type: opts.proved?.jkt ? 'DPoP' : 'Bearer',
584
+ expires_in: ttl,
585
+ machine_id: m.id,
586
+ ...(bound ? { audience: bound.rs.identifier, scope: bound.scopes.join(' '), ...(bound.grant.constraints ? { constraints: bound.grant.constraints } : {}) } : {}),
587
+ ...(orgClaims.org_role ? { organization_id: orgId, org_role: orgClaims.org_role } : {}),
588
+ },
589
+ };
590
+ }
591
+ /** The binding a request proves on a machine route: a DPoP proof (htm POST, htu this URL, single-use jti) and/or a forwarded client certificate. */
592
+ async function provedBindingOf(c) {
593
+ const e = c.engine;
594
+ const proved = {};
595
+ const raw = c.request.headers.get('dpop');
596
+ if (raw !== null) {
597
+ try {
598
+ const r = await (0, jws_1.verifyDpopProof)({ proof: raw, htm: 'POST', htu: c.request.url, nowMs: e.clock.now() });
599
+ const seen = dpopSeen(e);
600
+ const key = `${r.jkt}:${r.jti}`;
601
+ if (seen.has(key))
602
+ throw new Error('The DPoP proof has already been used.');
603
+ seen.set(key, e.clock.now() + (jws_1.DPOP_IAT_WINDOW_S + 5) * 1000);
604
+ for (const [k, until] of seen)
605
+ if (until <= e.clock.now())
606
+ seen.delete(k);
607
+ proved.jkt = r.jkt;
608
+ }
609
+ catch (err) {
610
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', err.message, 'dpop');
611
+ }
612
+ }
613
+ const x5t = await (0, jws_1.forwardedCertThumbprint)(c.request.headers);
614
+ if (x5t === null)
615
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'The forwarded client certificate could not be read.', 'mtls');
616
+ if (x5t)
617
+ proved.x5t = x5t;
618
+ return proved;
619
+ }
620
+ async function machineToken(c) {
621
+ const e = c.engine;
622
+ const machineId = String((0, util_1.need)(c.body, 'machine_id')).trim();
623
+ const nonce = String((0, util_1.need)(c.body, 'nonce')).trim();
624
+ const signature = String((0, util_1.need)(c.body, 'signature')).trim();
625
+ const m = e.db.machines.get(machineId);
626
+ if (!m || m.deleted)
627
+ throw (0, errors_1.notFound)('Machine');
628
+ if (m.status !== 'active' || !m.public_key_pem)
629
+ throw new errors_1.EmulatorError('FORBIDDEN', 'This machine is not permitted to mint tokens.');
630
+ const rec = st(e).nonces.get(nonce);
631
+ if (!rec || rec.machineId !== m.id || rec.spent || rec.expiresAt <= e.clock.now())
632
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'The challenge nonce is invalid or expired.', 'nonce');
633
+ // The security boundary: a bad signature means the caller does not hold the private key.
634
+ if (!(await (0, crypto_util_1.verifyDeviceSignature)(m.public_key_pem, nonce, signature)))
635
+ throw new errors_1.EmulatorError('VALIDATION_FAILED', 'The signature does not verify against the registered key.', 'signature');
636
+ // A binding the machine demands is checked before the nonce is spent, so a corrected request can retry.
637
+ const proved = await provedBindingOf(c);
638
+ enforceTokenBinding(m, proved);
639
+ return issueMachineToken(c, m, c.body, { proved, beforeIssue: () => void (rec.spent = true) });
640
+ }
641
+ const dpopSeen = (e) => {
642
+ let s = dpopStore.get(e);
643
+ if (!s)
644
+ dpopStore.set(e, (s = new Map()));
645
+ return s;
646
+ };
647
+ const dpopStore = new WeakMap();
648
+ //# sourceMappingURL=device.js.map