@ory/argus 0.1.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 (80) hide show
  1. package/README.md +134 -0
  2. package/assets/commands/local-down.md +19 -0
  3. package/assets/commands/local-up.md +27 -0
  4. package/assets/skills/auth-setup/SKILL.md +279 -0
  5. package/assets/skills/local-dev/SKILL.md +206 -0
  6. package/assets/skills/login-flow/SKILL.md +383 -0
  7. package/assets/skills/social-login/SKILL.md +312 -0
  8. package/dist/agent-auth.d.ts +204 -0
  9. package/dist/agent-auth.js +553 -0
  10. package/dist/auth-gate.d.ts +71 -0
  11. package/dist/auth-gate.js +308 -0
  12. package/dist/auth-store.d.ts +75 -0
  13. package/dist/auth-store.js +261 -0
  14. package/dist/auth.d.ts +93 -0
  15. package/dist/auth.js +323 -0
  16. package/dist/cli.d.ts +73 -0
  17. package/dist/cli.js +484 -0
  18. package/dist/client.d.ts +158 -0
  19. package/dist/client.js +679 -0
  20. package/dist/config.d.ts +135 -0
  21. package/dist/config.js +344 -0
  22. package/dist/denial.d.ts +79 -0
  23. package/dist/denial.js +103 -0
  24. package/dist/dev.d.ts +95 -0
  25. package/dist/dev.js +514 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +137 -0
  28. package/dist/local/cli.d.ts +12 -0
  29. package/dist/local/cli.js +95 -0
  30. package/dist/local/configs.d.ts +89 -0
  31. package/dist/local/configs.js +634 -0
  32. package/dist/local/health.d.ts +32 -0
  33. package/dist/local/health.js +65 -0
  34. package/dist/local/index.d.ts +6 -0
  35. package/dist/local/index.js +38 -0
  36. package/dist/local/jaeger-main.d.ts +13 -0
  37. package/dist/local/jaeger-main.js +85 -0
  38. package/dist/local/jaeger.d.ts +50 -0
  39. package/dist/local/jaeger.js +162 -0
  40. package/dist/local/main.d.ts +7 -0
  41. package/dist/local/main.js +14 -0
  42. package/dist/local/manager.d.ts +45 -0
  43. package/dist/local/manager.js +676 -0
  44. package/dist/local/seed.d.ts +71 -0
  45. package/dist/local/seed.js +237 -0
  46. package/dist/logger.d.ts +29 -0
  47. package/dist/logger.js +139 -0
  48. package/dist/mcp.d.ts +76 -0
  49. package/dist/mcp.js +122 -0
  50. package/dist/otel/exporter.d.ts +17 -0
  51. package/dist/otel/exporter.js +12 -0
  52. package/dist/otel/index.d.ts +2 -0
  53. package/dist/otel/index.js +8 -0
  54. package/dist/otel/otlp-http.d.ts +116 -0
  55. package/dist/otel/otlp-http.js +322 -0
  56. package/dist/registry/cli.d.ts +12 -0
  57. package/dist/registry/cli.js +76 -0
  58. package/dist/registry/config.d.ts +23 -0
  59. package/dist/registry/config.js +80 -0
  60. package/dist/registry/index.d.ts +3 -0
  61. package/dist/registry/index.js +21 -0
  62. package/dist/registry/main.d.ts +7 -0
  63. package/dist/registry/main.js +14 -0
  64. package/dist/registry/manager.d.ts +38 -0
  65. package/dist/registry/manager.js +674 -0
  66. package/dist/setup.d.ts +118 -0
  67. package/dist/setup.js +398 -0
  68. package/dist/skills.d.ts +78 -0
  69. package/dist/skills.js +264 -0
  70. package/dist/subject.d.ts +43 -0
  71. package/dist/subject.js +55 -0
  72. package/dist/tool-metadata.d.ts +41 -0
  73. package/dist/tool-metadata.js +127 -0
  74. package/dist/tracer.d.ts +172 -0
  75. package/dist/tracer.js +452 -0
  76. package/dist/types.d.ts +57 -0
  77. package/dist/types.js +3 -0
  78. package/dist/watch-sandbox.d.ts +9 -0
  79. package/dist/watch-sandbox.js +81 -0
  80. package/package.json +79 -0
package/dist/client.js ADDED
@@ -0,0 +1,679 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OryAgentClient = void 0;
4
+ exports.extractOryRequestId = extractOryRequestId;
5
+ const client_1 = require("@ory/client");
6
+ const logger_js_1 = require("./logger.js");
7
+ const tracer_js_1 = require("./tracer.js");
8
+ const config_js_1 = require("./config.js");
9
+ const otlp_http_js_1 = require("./otel/otlp-http.js");
10
+ class OryAgentClient {
11
+ frontend;
12
+ oauth2;
13
+ permission;
14
+ relationship;
15
+ config;
16
+ logger;
17
+ tracer;
18
+ sessionCache = new Map();
19
+ sessionCacheTtlMs;
20
+ /**
21
+ * The human user — the principal whose permissions we check.
22
+ * Populated by `ensureUserAuthenticated` on session start.
23
+ */
24
+ _userPrincipal = {};
25
+ /**
26
+ * The AI agent process — the principal that authenticates outgoing
27
+ * Ory API calls. Populated by `ensureAgentIdentity`.
28
+ */
29
+ _agentPrincipal = {};
30
+ constructor(config) {
31
+ this.config = config;
32
+ this.sessionCacheTtlMs = config.sessionCacheTtlMs ?? 60_000;
33
+ this.logger = new logger_js_1.DebugLogger(config.harness, {
34
+ logFile: config.logFile,
35
+ });
36
+ this.tracer = new tracer_js_1.Tracer({
37
+ harness: config.harness,
38
+ traceFile: config.traceFile,
39
+ ...(config.exporter ? { exporter: config.exporter } : {}),
40
+ });
41
+ // Seed agent principal from the constructor `apiKey` so existing
42
+ // call sites keep working until they migrate to ensureAgentIdentity.
43
+ if (config.apiKey)
44
+ this._agentPrincipal.token = config.apiKey;
45
+ const { frontend, oauth2, permission, relationship } = this.buildApis();
46
+ this.frontend = frontend;
47
+ this.oauth2 = oauth2;
48
+ this.permission = permission;
49
+ this.relationship = relationship;
50
+ }
51
+ /** Build a fresh set of Ory API instances using the current agent token. */
52
+ buildApis() {
53
+ const accessToken = this._agentPrincipal.token;
54
+ const apiConfig = new client_1.Configuration({
55
+ basePath: this.config.projectUrl,
56
+ ...(accessToken ? { accessToken } : {}),
57
+ });
58
+ return {
59
+ frontend: new client_1.FrontendApi(apiConfig),
60
+ oauth2: new client_1.OAuth2Api(apiConfig),
61
+ permission: new client_1.PermissionApi(apiConfig),
62
+ relationship: new client_1.RelationshipApi(apiConfig),
63
+ };
64
+ }
65
+ /** Snapshot of the current user principal. */
66
+ get userPrincipal() {
67
+ return { ...this._userPrincipal };
68
+ }
69
+ /** Snapshot of the current agent principal. */
70
+ get agentPrincipal() {
71
+ return { ...this._agentPrincipal };
72
+ }
73
+ /**
74
+ * Set or update the human user principal. The user is the subject of
75
+ * permission checks but does not authenticate outgoing API calls.
76
+ * Pass `{}` (or fields set to undefined) to clear.
77
+ */
78
+ setUserPrincipal(principal) {
79
+ this._userPrincipal = { ...principal };
80
+ }
81
+ /**
82
+ * Set or update the AI agent principal. The agent's token (when
83
+ * present) is used in the Authorization header for all outgoing Ory
84
+ * API calls — so the audit log shows "agent X acting on behalf of
85
+ * user Y". Rebuilds the underlying Ory API instances when the token
86
+ * actually changes so subsequent calls pick it up; otherwise leaves
87
+ * the API instances alone (so test stubs survive a no-op update).
88
+ */
89
+ setAgentPrincipal(principal) {
90
+ const tokenChanged = this._agentPrincipal.token !== principal.token;
91
+ this._agentPrincipal = { ...principal };
92
+ if (!tokenChanged)
93
+ return;
94
+ const { frontend, oauth2, permission, relationship } = this.buildApis();
95
+ this.frontend = frontend;
96
+ this.oauth2 = oauth2;
97
+ this.permission = permission;
98
+ this.relationship = relationship;
99
+ }
100
+ // ─── Session (Ory Identities / Kratos) ──────────────────────────
101
+ /**
102
+ * Verify an existing session token. Returns cached result if available.
103
+ */
104
+ async verifySession(sessionToken) {
105
+ this.logger.debug("session.verify.start", { hasToken: !!sessionToken });
106
+ // Check cache
107
+ const cached = this.sessionCache.get(sessionToken);
108
+ if (cached && cached.expiresAt > Date.now()) {
109
+ this.logger.debug("session.verify.cached", {
110
+ sessionId: cached.info.sessionId,
111
+ });
112
+ return cached.info;
113
+ }
114
+ const span = this.tracer.startSpan("session.verify");
115
+ try {
116
+ const response = await this.frontend.toSession({
117
+ xSessionToken: sessionToken,
118
+ });
119
+ const session = response.data;
120
+ const oryRequestId = extractOryRequestId(response.headers);
121
+ const info = {
122
+ sessionId: session.id,
123
+ identityId: session.identity.id,
124
+ active: session.active ?? false,
125
+ authenticatedAt: session.authenticated_at ?? "",
126
+ expiresAt: session.expires_at,
127
+ authenticatorAssuranceLevel: session.authenticator_assurance_level,
128
+ authenticationMethods: session.authentication_methods?.map((m) => ({
129
+ method: m.method ?? "unknown",
130
+ completedAt: m.completed_at,
131
+ })),
132
+ traits: session.identity.traits ?? {},
133
+ };
134
+ // Cache the result
135
+ if (this.sessionCacheTtlMs > 0) {
136
+ this.sessionCache.set(sessionToken, {
137
+ info,
138
+ expiresAt: Date.now() + this.sessionCacheTtlMs,
139
+ });
140
+ }
141
+ this.logger.info("session.verify.success", {
142
+ sessionId: info.sessionId,
143
+ identityId: info.identityId,
144
+ active: info.active,
145
+ aal: info.authenticatorAssuranceLevel,
146
+ });
147
+ span.end("ok", {
148
+ sessionId: info.sessionId,
149
+ identityId: info.identityId,
150
+ active: info.active,
151
+ ...(oryRequestId ? { oryRequestId } : {}),
152
+ });
153
+ return info;
154
+ }
155
+ catch (err) {
156
+ const oryErr = this.classifyError(err);
157
+ this.logger.error("session.verify.failed", {
158
+ code: oryErr.code,
159
+ status: oryErr.status,
160
+ message: oryErr.message,
161
+ });
162
+ span.end("error", {
163
+ error: oryErr.code,
164
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
165
+ });
166
+ throw oryErr;
167
+ }
168
+ }
169
+ // ─── OAuth2 Token (Ory OAuth2 / Hydra) ──────────────────────────
170
+ /**
171
+ * Introspect an OAuth2 access token. Use this when agents authenticate
172
+ * via client credentials flow instead of session tokens.
173
+ */
174
+ async introspectToken(token, scope) {
175
+ this.logger.debug("oauth2.introspect.start", { hasToken: !!token });
176
+ const span = this.tracer.startSpan("oauth2.introspect");
177
+ try {
178
+ const response = await this.oauth2.introspectOAuth2Token({
179
+ token,
180
+ scope,
181
+ });
182
+ const data = response.data;
183
+ const oryRequestId = extractOryRequestId(response.headers);
184
+ const info = {
185
+ active: data.active,
186
+ clientId: data.client_id,
187
+ scope: data.scope,
188
+ subject: data.sub,
189
+ audience: data.aud,
190
+ expiresAt: data.exp,
191
+ issuedAt: data.iat,
192
+ tokenUse: data.token_use,
193
+ extra: data.ext,
194
+ };
195
+ this.logger.info("oauth2.introspect.result", {
196
+ active: info.active,
197
+ clientId: info.clientId,
198
+ subject: info.subject,
199
+ });
200
+ span.end("ok", {
201
+ active: info.active,
202
+ clientId: info.clientId,
203
+ subject: info.subject,
204
+ ...(oryRequestId ? { oryRequestId } : {}),
205
+ });
206
+ return info;
207
+ }
208
+ catch (err) {
209
+ const oryErr = this.classifyError(err);
210
+ this.logger.error("oauth2.introspect.failed", {
211
+ code: oryErr.code,
212
+ status: oryErr.status,
213
+ message: oryErr.message,
214
+ });
215
+ span.end("error", {
216
+ error: oryErr.code,
217
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
218
+ });
219
+ throw oryErr;
220
+ }
221
+ }
222
+ // ─── Permissions (Ory Permissions / Keto) ────────────────────────
223
+ /**
224
+ * Check if a subject has permission to perform an action.
225
+ *
226
+ * `options.spanAttributes` are merged into the `permission.check` trace
227
+ * span so callers can attach harness-side context (toolName, mcpServer,
228
+ * etc.) that the wire-level `PermissionCheck` shape doesn't carry.
229
+ */
230
+ async checkPermission(check, options) {
231
+ this.logger.debug("permission.check.start", { ...check });
232
+ const span = this.tracer.startSpan("permission.check", {
233
+ attributes: {
234
+ object: check.object,
235
+ relation: check.relation,
236
+ ...this.principalSpanAttributes(),
237
+ ...options?.spanAttributes,
238
+ },
239
+ });
240
+ try {
241
+ const response = await this.permission.checkPermission({
242
+ namespace: check.namespace,
243
+ object: check.object,
244
+ relation: check.relation,
245
+ subjectId: check.subjectId,
246
+ subjectSetNamespace: check.subjectSet?.namespace,
247
+ subjectSetObject: check.subjectSet?.object,
248
+ subjectSetRelation: check.subjectSet?.relation,
249
+ });
250
+ const data = response.data;
251
+ const oryRequestId = extractOryRequestId(response.headers);
252
+ const result = {
253
+ allowed: data.allowed,
254
+ checkedAt: new Date().toISOString(),
255
+ check,
256
+ };
257
+ this.logger.info("permission.check.result", {
258
+ allowed: result.allowed,
259
+ namespace: check.namespace,
260
+ object: check.object,
261
+ relation: check.relation,
262
+ subjectId: check.subjectId,
263
+ subjectSet: check.subjectSet,
264
+ });
265
+ span.end(result.allowed ? "ok" : "denied", {
266
+ allowed: result.allowed,
267
+ ...(oryRequestId ? { oryRequestId } : {}),
268
+ });
269
+ return result;
270
+ }
271
+ catch (err) {
272
+ const oryErr = this.classifyError(err);
273
+ this.logger.error("permission.check.failed", {
274
+ code: oryErr.code,
275
+ status: oryErr.status,
276
+ message: oryErr.message,
277
+ ...check,
278
+ });
279
+ span.end("error", {
280
+ error: oryErr.code,
281
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
282
+ });
283
+ throw oryErr;
284
+ }
285
+ }
286
+ /**
287
+ * Check multiple permissions in a single request.
288
+ *
289
+ * `options.spanAttributes` are merged into the `permission.batch_check`
290
+ * span — same purpose as `checkPermission`'s spanAttributes.
291
+ */
292
+ async batchCheckPermissions(checks, options) {
293
+ this.logger.debug("permission.batch_check.start", {
294
+ count: checks.length,
295
+ });
296
+ const span = this.tracer.startSpan("permission.batch_check", {
297
+ attributes: {
298
+ count: checks.length,
299
+ ...this.principalSpanAttributes(),
300
+ ...options?.spanAttributes,
301
+ },
302
+ });
303
+ try {
304
+ const response = await this.permission.batchCheckPermission({
305
+ batchCheckPermissionBody: {
306
+ tuples: checks.map((c) => ({
307
+ namespace: c.namespace,
308
+ object: c.object,
309
+ relation: c.relation,
310
+ subject_id: c.subjectId,
311
+ subject_set: c.subjectSet
312
+ ? {
313
+ namespace: c.subjectSet.namespace,
314
+ object: c.subjectSet.object,
315
+ relation: c.subjectSet.relation,
316
+ }
317
+ : undefined,
318
+ })),
319
+ },
320
+ });
321
+ const data = response.data;
322
+ const oryRequestId = extractOryRequestId(response.headers);
323
+ const result = {
324
+ results: data.results.map((r) => ({
325
+ allowed: r.allowed,
326
+ error: r.error,
327
+ })),
328
+ checks,
329
+ checkedAt: new Date().toISOString(),
330
+ };
331
+ const allowed = result.results.filter((r) => r.allowed).length;
332
+ const denied = result.results.filter((r) => !r.allowed).length;
333
+ this.logger.info("permission.batch_check.result", {
334
+ count: checks.length,
335
+ allowed,
336
+ denied,
337
+ });
338
+ span.end("ok", {
339
+ allowed,
340
+ denied,
341
+ ...(oryRequestId ? { oryRequestId } : {}),
342
+ });
343
+ return result;
344
+ }
345
+ catch (err) {
346
+ const oryErr = this.classifyError(err);
347
+ this.logger.error("permission.batch_check.failed", {
348
+ code: oryErr.code,
349
+ status: oryErr.status,
350
+ message: oryErr.message,
351
+ });
352
+ span.end("error", {
353
+ error: oryErr.code,
354
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
355
+ });
356
+ throw oryErr;
357
+ }
358
+ }
359
+ // ─── Relationships (Ory Permissions / Keto write API) ────────────
360
+ /**
361
+ * Create a relation tuple in Keto. Idempotent: a 409 (tuple already
362
+ * exists) is swallowed and reported as success. All other errors are
363
+ * classified and re-thrown — callers wrap in fail-open semantics if
364
+ * the tuple is non-critical (e.g. audit-only delegation tracking).
365
+ *
366
+ * `options.spanAttributes` are merged into the `relationship.create`
367
+ * trace span so callers can attach context like `delegation: "user→agent"`.
368
+ */
369
+ async createRelationship(check, options) {
370
+ this.logger.debug("relationship.create.start", { ...check });
371
+ const span = this.tracer.startSpan("relationship.create", {
372
+ attributes: {
373
+ namespace: check.namespace,
374
+ object: check.object,
375
+ relation: check.relation,
376
+ ...this.principalSpanAttributes(),
377
+ ...options?.spanAttributes,
378
+ },
379
+ });
380
+ try {
381
+ const response = await this.relationship.createRelationship({
382
+ createRelationshipBody: {
383
+ namespace: check.namespace,
384
+ object: check.object,
385
+ relation: check.relation,
386
+ subject_id: check.subjectId,
387
+ ...(check.subjectSet
388
+ ? {
389
+ subject_set: {
390
+ namespace: check.subjectSet.namespace,
391
+ object: check.subjectSet.object,
392
+ relation: check.subjectSet.relation,
393
+ },
394
+ }
395
+ : {}),
396
+ },
397
+ });
398
+ const oryRequestId = extractOryRequestId(response.headers);
399
+ this.logger.info("relationship.create.result", {
400
+ namespace: check.namespace,
401
+ object: check.object,
402
+ relation: check.relation,
403
+ subjectId: check.subjectId,
404
+ });
405
+ span.end("ok", {
406
+ created: true,
407
+ ...(oryRequestId ? { oryRequestId } : {}),
408
+ });
409
+ return { created: true, alreadyExisted: false };
410
+ }
411
+ catch (err) {
412
+ const oryErr = this.classifyError(err);
413
+ // Keto returns 409 when the tuple already exists; treat as success
414
+ // so callers can call this on every session start without dedup.
415
+ if (oryErr.status === 409) {
416
+ this.logger.debug("relationship.create.exists", {
417
+ namespace: check.namespace,
418
+ object: check.object,
419
+ relation: check.relation,
420
+ });
421
+ span.end("ok", {
422
+ created: false,
423
+ alreadyExisted: true,
424
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
425
+ });
426
+ return { created: false, alreadyExisted: true };
427
+ }
428
+ this.logger.error("relationship.create.failed", {
429
+ code: oryErr.code,
430
+ status: oryErr.status,
431
+ message: oryErr.message,
432
+ ...check,
433
+ });
434
+ span.end("error", {
435
+ error: oryErr.code,
436
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
437
+ });
438
+ throw oryErr;
439
+ }
440
+ }
441
+ /**
442
+ * Delete a relation tuple in Keto. Missing tuples (404) are treated as
443
+ * success so callers can call this idempotently when unwinding state.
444
+ */
445
+ async deleteRelationship(check, options) {
446
+ this.logger.debug("relationship.delete.start", { ...check });
447
+ const span = this.tracer.startSpan("relationship.delete", {
448
+ attributes: {
449
+ namespace: check.namespace,
450
+ object: check.object,
451
+ relation: check.relation,
452
+ ...this.principalSpanAttributes(),
453
+ ...options?.spanAttributes,
454
+ },
455
+ });
456
+ try {
457
+ const response = await this.relationship.deleteRelationships({
458
+ namespace: check.namespace,
459
+ object: check.object,
460
+ relation: check.relation,
461
+ subjectId: check.subjectId,
462
+ ...(check.subjectSet
463
+ ? {
464
+ subjectSetNamespace: check.subjectSet.namespace,
465
+ subjectSetObject: check.subjectSet.object,
466
+ subjectSetRelation: check.subjectSet.relation,
467
+ }
468
+ : {}),
469
+ });
470
+ const oryRequestId = extractOryRequestId(response.headers);
471
+ span.end("ok", {
472
+ deleted: true,
473
+ ...(oryRequestId ? { oryRequestId } : {}),
474
+ });
475
+ return { deleted: true, notFound: false };
476
+ }
477
+ catch (err) {
478
+ const oryErr = this.classifyError(err);
479
+ if (oryErr.code === "not_found") {
480
+ span.end("ok", {
481
+ deleted: false,
482
+ notFound: true,
483
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
484
+ });
485
+ return { deleted: false, notFound: true };
486
+ }
487
+ this.logger.error("relationship.delete.failed", {
488
+ code: oryErr.code,
489
+ status: oryErr.status,
490
+ message: oryErr.message,
491
+ ...check,
492
+ });
493
+ span.end("error", {
494
+ error: oryErr.code,
495
+ ...(oryErr.requestId ? { oryRequestId: oryErr.requestId } : {}),
496
+ });
497
+ throw oryErr;
498
+ }
499
+ }
500
+ /**
501
+ * Span attributes describing which principals were attached to the
502
+ * client when the span was started. Useful for correlating audit logs
503
+ * across the user/agent split.
504
+ */
505
+ principalSpanAttributes() {
506
+ const out = {};
507
+ if (this._userPrincipal.subject)
508
+ out.userSubject = this._userPrincipal.subject;
509
+ if (this._agentPrincipal.subject)
510
+ out.agentSubject = this._agentPrincipal.subject;
511
+ return out;
512
+ }
513
+ // ─── Error Classification ────────────────────────────────────────
514
+ /**
515
+ * Classify an error from any Ory API call into a structured OryError.
516
+ */
517
+ classifyError(err) {
518
+ // Axios errors have a `response` property with status + data
519
+ if (this.isAxiosError(err)) {
520
+ const status = err.response?.status;
521
+ const body = err.response?.data;
522
+ const errorId = body?.error?.id;
523
+ const message = body?.error?.message ?? err.message ?? "Unknown Ory error";
524
+ const requestId = extractOryRequestId(err.response?.headers);
525
+ let code;
526
+ switch (status) {
527
+ case 401:
528
+ code =
529
+ errorId === "session_aal2_required"
530
+ ? "session_aal2_required"
531
+ : "session_inactive";
532
+ break;
533
+ case 403:
534
+ code =
535
+ errorId === "session_aal2_required"
536
+ ? "session_aal2_required"
537
+ : "forbidden";
538
+ break;
539
+ case 404:
540
+ code = "not_found";
541
+ break;
542
+ case 429:
543
+ code = "rate_limited";
544
+ break;
545
+ default:
546
+ code = "unknown";
547
+ }
548
+ return {
549
+ code,
550
+ status,
551
+ message,
552
+ ...(requestId ? { requestId } : {}),
553
+ cause: err,
554
+ };
555
+ }
556
+ // Network errors (no response received)
557
+ if (err instanceof Error && "code" in err) {
558
+ const netErr = err;
559
+ if (netErr.code === "ECONNREFUSED" ||
560
+ netErr.code === "ECONNRESET" ||
561
+ netErr.code === "ENOTFOUND" ||
562
+ netErr.code === "ETIMEDOUT") {
563
+ return {
564
+ code: "network_error",
565
+ message: `Network error: ${netErr.code} — ${netErr.message}`,
566
+ cause: err,
567
+ };
568
+ }
569
+ }
570
+ return {
571
+ code: "unknown",
572
+ message: err instanceof Error ? err.message : String(err),
573
+ cause: err,
574
+ };
575
+ }
576
+ isAxiosError(err) {
577
+ return (typeof err === "object" &&
578
+ err !== null &&
579
+ "isAxiosError" in err &&
580
+ err.isAxiosError === true);
581
+ }
582
+ // ─── Factory ─────────────────────────────────────────────────────
583
+ /**
584
+ * Create a client from environment variables.
585
+ */
586
+ /**
587
+ * Create a client from environment variables, falling back to the
588
+ * shared config file at ~/.config/ory-agent-plugins/config.json.
589
+ *
590
+ * Resolution order for projectUrl and apiKey:
591
+ * 1. Environment variables (ORY_PROJECT_URL, ORY_API_KEY)
592
+ * 2. Config file (~/.config/ory-agent-plugins/config.json)
593
+ * 3. Placeholder (fail-open pass-through mode)
594
+ */
595
+ static fromEnv(harness) {
596
+ const resolved = (0, config_js_1.resolveConfig)();
597
+ const projectUrl = resolved.projectUrl;
598
+ if (!projectUrl) {
599
+ // Use a placeholder URL so the client can be constructed.
600
+ // API calls will fail with network_error which is handled as fail-open.
601
+ const client = new OryAgentClient({
602
+ projectUrl: "http://localhost:0",
603
+ apiKey: resolved.apiKey,
604
+ harness,
605
+ logFile: process.env.ORY_AGENT_LOG_FILE,
606
+ traceFile: process.env.ORY_AGENT_TRACE_FILE,
607
+ sessionCacheTtlMs: process.env.ORY_SESSION_CACHE_TTL_MS
608
+ ? parseInt(process.env.ORY_SESSION_CACHE_TTL_MS, 10)
609
+ : undefined,
610
+ });
611
+ client.logger.warn("config.missing_project_url", {
612
+ message: "ORY_PROJECT_URL is not set and no config file found. " +
613
+ "Ory integration is disabled — all hooks will pass through. " +
614
+ "Set ORY_PROJECT_URL or run the plugin's configure command.",
615
+ });
616
+ attachOtlpExporterFromEnv(client, harness);
617
+ return client;
618
+ }
619
+ const client = new OryAgentClient({
620
+ projectUrl,
621
+ apiKey: resolved.apiKey,
622
+ harness,
623
+ logFile: process.env.ORY_AGENT_LOG_FILE,
624
+ traceFile: process.env.ORY_AGENT_TRACE_FILE,
625
+ sessionCacheTtlMs: process.env.ORY_SESSION_CACHE_TTL_MS
626
+ ? parseInt(process.env.ORY_SESSION_CACHE_TTL_MS, 10)
627
+ : undefined,
628
+ });
629
+ client.logger.debug("config.resolved", {
630
+ projectUrlSource: resolved.projectUrlSource,
631
+ apiKeySource: resolved.apiKeySource,
632
+ });
633
+ attachOtlpExporterFromEnv(client, harness);
634
+ return client;
635
+ }
636
+ }
637
+ exports.OryAgentClient = OryAgentClient;
638
+ function attachOtlpExporterFromEnv(client, harness) {
639
+ const meta = client.tracer.processMetadata;
640
+ const exporter = (0, otlp_http_js_1.otlpExporterFromEnv)({
641
+ harness,
642
+ hostname: meta.hostname,
643
+ user: meta.user,
644
+ pluginVersion: meta.pluginVersion,
645
+ cwd: meta.cwd,
646
+ gitBranch: meta.gitBranch,
647
+ gitCommit: meta.gitCommit,
648
+ onError: (err) => client.logger.warn("otel.export.failed", {
649
+ message: err instanceof Error ? err.message : String(err),
650
+ }),
651
+ });
652
+ if (!exporter)
653
+ return;
654
+ client.tracer.setExporter(exporter);
655
+ client.logger.info("otel.export.enabled", {
656
+ endpoint: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
657
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
658
+ process.env.ORY_OTLP_ENDPOINT,
659
+ serviceName: process.env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${harness}`,
660
+ });
661
+ }
662
+ /**
663
+ * Pull the Ory backend request ID from a response headers bag (axios returns
664
+ * lowercase header names). Returns undefined when not present.
665
+ */
666
+ function extractOryRequestId(headers) {
667
+ if (!headers || typeof headers !== "object")
668
+ return undefined;
669
+ const bag = headers;
670
+ const value = bag["x-request-id"] ??
671
+ bag["X-Request-Id"] ??
672
+ bag["X-Request-ID"] ??
673
+ bag["ory-request-id"];
674
+ if (typeof value === "string" && value.length > 0)
675
+ return value;
676
+ if (Array.isArray(value) && typeof value[0] === "string")
677
+ return value[0];
678
+ return undefined;
679
+ }