@caronte-sdk/node 0.2.1 → 0.2.3

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 (37) hide show
  1. package/README.md +17 -0
  2. package/bin/caronte-node.js +62 -0
  3. package/dist/adapters/apollo.cjs +18 -3
  4. package/dist/adapters/apollo.cjs.map +1 -1
  5. package/dist/adapters/apollo.d.cts +1 -1
  6. package/dist/adapters/apollo.d.ts +1 -1
  7. package/dist/adapters/apollo.js +15 -4
  8. package/dist/adapters/apollo.js.map +1 -1
  9. package/dist/adapters/express.cjs +10 -0
  10. package/dist/adapters/express.cjs.map +1 -1
  11. package/dist/adapters/express.d.cts +1 -1
  12. package/dist/adapters/express.d.ts +1 -1
  13. package/dist/adapters/express.js +7 -1
  14. package/dist/adapters/express.js.map +1 -1
  15. package/dist/adapters/fastify.cjs +10 -1
  16. package/dist/adapters/fastify.cjs.map +1 -1
  17. package/dist/adapters/fastify.d.cts +1 -1
  18. package/dist/adapters/fastify.d.ts +1 -1
  19. package/dist/adapters/fastify.js +10 -1
  20. package/dist/adapters/fastify.js.map +1 -1
  21. package/dist/adapters/nest/index.cjs +58 -1
  22. package/dist/adapters/nest/index.cjs.map +1 -1
  23. package/dist/adapters/nest/index.d.cts +1 -1
  24. package/dist/adapters/nest/index.d.ts +1 -1
  25. package/dist/adapters/nest/index.js +12 -2
  26. package/dist/adapters/nest/index.js.map +1 -1
  27. package/dist/{chunk-XMA2NAR7.js → chunk-M46BRRWA.js} +50 -3
  28. package/dist/chunk-M46BRRWA.js.map +1 -0
  29. package/dist/{client-TcOSwgrn.d.cts → client-CSokWFYn.d.cts} +3 -0
  30. package/dist/{client-TcOSwgrn.d.ts → client-CSokWFYn.d.ts} +3 -0
  31. package/dist/index.cjs +48 -0
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +17 -3
  34. package/dist/index.d.ts +17 -3
  35. package/dist/index.js +1 -1
  36. package/package.json +18 -5
  37. package/dist/chunk-XMA2NAR7.js.map +0 -1
package/README.md CHANGED
@@ -179,6 +179,23 @@ You can always pass the method explicitly as the third argument to `guard()`.
179
179
  | `realmId` | Name or UUID of the realm (e.g. `purp`) |
180
180
  | `appId` | UUID of the app registered in `auth.apps` |
181
181
  | `secret` | Plain-text app secret — use env vars, never commit |
182
+ | `otelCollectorUrl` | Optional. OTLP HTTP endpoint (e.g. `http://localhost:4318`) of an OpenTelemetry Collector. When set, `startup()` initializes the Node OTel SDK — traces (auto-instrumentation for HTTP/Express/Fastify/GraphQL/pg, etc.), metrics (e.g. `http.server.duration` from that same auto-instrumentation), and a bootstrap log record proving the log pipeline is wired — all tagged with `service.name=appId` and `service.namespace=realmId`. Omit to skip OTel entirely — a misconfigured or unreachable collector never breaks `startup()`. |
183
+
184
+ ### Running with `otelCollectorUrl` on an ESM app
185
+
186
+ If your app is ESM (`"type": "module"` in `package.json` — this includes any app run via `tsx`), auto-instrumentation (HTTP/Express/Fastify/GraphQL spans and metrics) **will not work** unless a loader hook is registered before your app's own imports run. This is a Node.js platform constraint, not something `otelCollectorUrl` alone can fix from inside `startup()` — by the time `startup()` executes, your `import express from 'express'` (and everything it pulls in) has already finished loading. See [Node's ESM instrumentation docs](https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/esm-support.md).
187
+
188
+ `@caronte-sdk/node` ships a `caronte-node` bin that handles this for you — put it in front of whatever already runs your app:
189
+
190
+ ```jsonc
191
+ // package.json
192
+ "scripts": {
193
+ "dev": "caronte-node tsx watch src/main.ts",
194
+ "start": "caronte-node tsx src/main.ts"
195
+ }
196
+ ```
197
+
198
+ It's a no-op when `otelCollectorUrl` is unset (the hook is registered either way, but nothing gets instrumented unless `startup()` actually calls `NodeSDK.start()`). CommonJS apps (no `"type": "module"`, e.g. via `ts-node`) aren't affected by this — auto-instrumentation there works without `caronte-node` — but running everything through it is still fine either way.
182
199
 
183
200
  ## How it works
184
201
 
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // Wrapper around whatever actually runs the app (tsx, ts-node, plain node,
3
+ // ...) so consumers get working OTel auto-instrumentation for free on ESM
4
+ // apps without editing their run command by hand.
5
+ //
6
+ // Why this has to exist: Node's ESM loader hooks (needed to patch core/
7
+ // third-party modules for tracing) can only be registered *before* the
8
+ // process starts loading the app's module graph — a static `import` at the
9
+ // top of main.ts is already resolved by the time any of that file's own
10
+ // code (including CaronteClient.startup()) runs, so nothing inside the SDK
11
+ // itself can register the hook late enough to matter. It has to happen at
12
+ // the process level, which is exactly what `caronte-node` does: it re-spawns
13
+ // the real command with the loader hook pre-registered via NODE_OPTIONS.
14
+ // (See https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/esm-support.md)
15
+ //
16
+ // Always registers the hook — harmless when otelCollectorUrl isn't set,
17
+ // since CaronteClient.startup() only calls NodeSDK.start() when it is.
18
+ import { spawn } from 'node:child_process';
19
+
20
+ const [command, ...args] = process.argv.slice(2);
21
+
22
+ if (!command) {
23
+ console.error('Usage: caronte-node <command> [args...]');
24
+ console.error('Example: caronte-node tsx watch src/main.ts');
25
+ process.exit(1);
26
+ }
27
+
28
+ // Modern, non-experimental replacement for --experimental-loader: register()
29
+ // via a data: URL import. Resolve hook.mjs's absolute location *from this
30
+ // script's own position inside @caronte-sdk/node* (not from the consuming
31
+ // app's cwd) — @opentelemetry/instrumentation is a transitive dependency of
32
+ // @caronte-sdk/node, and npm has no obligation to hoist it into the
33
+ // consumer's own top-level node_modules (in practice, with a `file:`
34
+ // dependency pointing at an already-built package, it usually doesn't).
35
+ // Resolving from here matches exactly how otel.ts itself resolves the same
36
+ // specifier, so it works regardless of the consumer's hoisting layout.
37
+ const hookUrl = import.meta.resolve('@opentelemetry/instrumentation/hook.mjs');
38
+ const registerScript = `import { register } from 'node:module'; register(${JSON.stringify(hookUrl)});`;
39
+ const importUrl = `data:text/javascript,${encodeURIComponent(registerScript)}`;
40
+
41
+ const nodeOptions = [process.env.NODE_OPTIONS, `--import=${importUrl}`].filter(Boolean).join(' ');
42
+
43
+ // Pass the whole invocation as a single string (not a separate args array) —
44
+ // shell:true + an args array is deprecated (DEP0190) because the args aren't
45
+ // escaped before being handed to the shell.
46
+ const quote = (arg) => (/\s/.test(arg) ? JSON.stringify(arg) : arg);
47
+ const fullCommand = [command, ...args].map(quote).join(' ');
48
+
49
+ const child = spawn(fullCommand, {
50
+ stdio: 'inherit',
51
+ shell: true,
52
+ env: { ...process.env, NODE_OPTIONS: nodeOptions },
53
+ });
54
+
55
+ child.on('exit', (code, signal) => {
56
+ if (signal) process.kill(process.pid, signal);
57
+ else process.exit(code ?? 0);
58
+ });
59
+ child.on('error', (err) => {
60
+ console.error(`[caronte-node] failed to launch "${command}": ${err.message}`);
61
+ process.exit(1);
62
+ });
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ var api = require('@opentelemetry/api');
4
+
5
+ // src/adapters/apollo.ts
6
+
3
7
  // src/exceptions.ts
4
8
  var CaronteError = class extends Error {
5
9
  constructor(message) {
@@ -39,6 +43,7 @@ function carontePlugin(client) {
39
43
  if (scheme?.toLowerCase() === "bearer" && token) {
40
44
  try {
41
45
  ctx.contextValue.caronteUser = await client.validateToken(token);
46
+ api.trace.getActiveSpan()?.setAttribute("caronte.user", ctx.contextValue.caronteUser.sub);
42
47
  } catch (err) {
43
48
  if (err instanceof CaronteTokenError) {
44
49
  ctx.contextValue.caronteUser = void 0;
@@ -55,9 +60,19 @@ function createGuard(client) {
55
60
  const resolvedMethod = method ?? detectMethod(id);
56
61
  registerOperation({ identifier: id, method: resolvedMethod, level });
57
62
  return (parent, args, context, info) => {
58
- if (level === "public") return resolver(parent, args, context, info);
59
- if (!context.caronteUser) throw new Error("Unauthorized: Bearer token required.");
60
- if (!client.checkPermission(context.caronteUser, id, resolvedMethod)) {
63
+ const span = api.trace.getActiveSpan();
64
+ span?.setAttribute("caronte.operation_id", id);
65
+ if (level === "public") {
66
+ span?.setAttribute("caronte.permission_result", "public");
67
+ return resolver(parent, args, context, info);
68
+ }
69
+ if (!context.caronteUser) {
70
+ span?.setAttribute("caronte.permission_result", "unauthorized");
71
+ throw new Error("Unauthorized: Bearer token required.");
72
+ }
73
+ const allowed = client.checkPermission(context.caronteUser, id, resolvedMethod);
74
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
75
+ if (!allowed) {
61
76
  throw new Error("Forbidden: Insufficient permissions.");
62
77
  }
63
78
  return resolver(parent, args, context, info);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exceptions.ts","../../src/registry.ts","../../src/adapters/apollo.ts"],"names":[],"mappings":"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","file":"apollo.cjs","sourcesContent":["export class CaronteError extends Error {\n constructor(message: string) {\n super(message);\n this.name = this.constructor.name;\n }\n}\n\nexport class CaronteAuthError extends CaronteError {}\nexport class CaronteTokenError extends CaronteError {}\nexport class CaronteForbiddenError extends CaronteError {}\nexport class CaronteSyncError extends CaronteError {}\nexport class CaronteConfigError extends CaronteError {}","import type { OperationDescriptor } from './models.js';\n\nconst _registry: OperationDescriptor[] = [];\n\n/** Infer operation method from the last segment of the operation id.\n * e.g. \"tasks:list\" → \"read\", \"tasks:create\" → \"write\", \"tasks:delete\" → \"delete\"\n */\nexport function detectMethod(id: string): string {\n const last = (id.split(':').pop() ?? '').toLowerCase();\n if (/^(get|list|fetch|read)/.test(last)) return 'read';\n if (/^(delete|remove|destroy)/.test(last)) return 'delete';\n if (/(stream|subscribe|watch|listen)/.test(last)) return 'stream';\n return 'write';\n}\n\n/** Register an operation in the global registry (used by startup to sync). */\nexport function registerOperation(op: OperationDescriptor): void {\n const exists = _registry.some(\n o => o.identifier === op.identifier && o.method === op.method,\n );\n if (!exists) _registry.push(op);\n}\n\nexport function getRegistry(): OperationDescriptor[] {\n return [..._registry];\n}\n\nexport function clearRegistry(): void {\n _registry.length = 0;\n}","import type { ApolloServerPlugin, BaseContext, GraphQLRequestContext } from '@apollo/server';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\nexport interface CaronteContext extends BaseContext {\n caronteUser?: TokenClaims;\n}\n\n/**\n * Apollo Server plugin for caronte.\n *\n * Validates the Bearer token on every request and stores the claims in\n * `context.caronteUser`. Use `createGuard(client)` to enforce per-resolver\n * permissions.\n */\nexport function carontePlugin(client: CaronteClient): ApolloServerPlugin<CaronteContext> {\n return {\n async requestDidStart() {\n return {\n async didResolveOperation(ctx: GraphQLRequestContext<CaronteContext>) {\n const httpAuth = ctx.request.http?.headers.get('authorization') ?? '';\n const wsAuth = ((ctx.contextValue as any)?.connectionParams?.Authorization as string | undefined) ?? '';\n const auth = httpAuth || wsAuth;\n\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n ctx.contextValue.caronteUser = await client.validateToken(token);\n } catch (err) {\n if (err instanceof CaronteTokenError) {\n ctx.contextValue.caronteUser = undefined;\n }\n }\n }\n },\n };\n },\n };\n}\n\n/**\n * Factory that returns a per-resolver guard bound to a `CaronteClient`.\n *\n * @example\n * ```ts\n * const guard = createGuard(client);\n *\n * const resolvers = {\n * Query: {\n * tasks: guard('tasks:list', 'private', async (_parent, _args, context) => {\n * const user = context.caronteUser;\n * return db.tasks.findAll();\n * }),\n * },\n * };\n * ```\n */\nexport function createGuard(client: CaronteClient) {\n return function guard<TParent, TArgs, TContext extends CaronteContext, TReturn>(\n id: string,\n level: string,\n resolver: (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn,\n method?: string,\n ): (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (parent, args, context, info) => {\n if (level === 'public') return resolver(parent, args, context, info);\n if (!context.caronteUser) throw new Error('Unauthorized: Bearer token required.');\n if (!client.checkPermission(context.caronteUser, id, resolvedMethod)) {\n throw new Error('Forbidden: Insufficient permissions.');\n }\n return resolver(parent, args, context, info);\n };\n };\n}"]}
1
+ {"version":3,"sources":["../../src/exceptions.ts","../../src/registry.ts","../../src/adapters/apollo.ts"],"names":["trace"],"mappings":"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","file":"apollo.cjs","sourcesContent":["export class CaronteError extends Error {\n constructor(message: string) {\n super(message);\n this.name = this.constructor.name;\n }\n}\n\nexport class CaronteAuthError extends CaronteError {}\nexport class CaronteTokenError extends CaronteError {}\nexport class CaronteForbiddenError extends CaronteError {}\nexport class CaronteSyncError extends CaronteError {}\nexport class CaronteConfigError extends CaronteError {}","import type { OperationDescriptor } from './models.js';\n\nconst _registry: OperationDescriptor[] = [];\n\n/** Infer operation method from the last segment of the operation id.\n * e.g. \"tasks:list\" → \"read\", \"tasks:create\" → \"write\", \"tasks:delete\" → \"delete\"\n */\nexport function detectMethod(id: string): string {\n const last = (id.split(':').pop() ?? '').toLowerCase();\n if (/^(get|list|fetch|read)/.test(last)) return 'read';\n if (/^(delete|remove|destroy)/.test(last)) return 'delete';\n if (/(stream|subscribe|watch|listen)/.test(last)) return 'stream';\n return 'write';\n}\n\n/** Register an operation in the global registry (used by startup to sync). */\nexport function registerOperation(op: OperationDescriptor): void {\n const exists = _registry.some(\n o => o.identifier === op.identifier && o.method === op.method,\n );\n if (!exists) _registry.push(op);\n}\n\nexport function getRegistry(): OperationDescriptor[] {\n return [..._registry];\n}\n\nexport function clearRegistry(): void {\n _registry.length = 0;\n}","import type { ApolloServerPlugin, BaseContext, GraphQLRequestContext } from '@apollo/server';\nimport { trace } from '@opentelemetry/api';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\nexport interface CaronteContext extends BaseContext {\n caronteUser?: TokenClaims;\n}\n\n/**\n * Apollo Server plugin for caronte.\n *\n * Validates the Bearer token on every request and stores the claims in\n * `context.caronteUser`. Use `createGuard(client)` to enforce per-resolver\n * permissions.\n */\nexport function carontePlugin(client: CaronteClient): ApolloServerPlugin<CaronteContext> {\n return {\n async requestDidStart() {\n return {\n async didResolveOperation(ctx: GraphQLRequestContext<CaronteContext>) {\n const httpAuth = ctx.request.http?.headers.get('authorization') ?? '';\n const wsAuth = ((ctx.contextValue as any)?.connectionParams?.Authorization as string | undefined) ?? '';\n const auth = httpAuth || wsAuth;\n\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n ctx.contextValue.caronteUser = await client.validateToken(token);\n trace.getActiveSpan()?.setAttribute('caronte.user', ctx.contextValue.caronteUser.sub);\n } catch (err) {\n if (err instanceof CaronteTokenError) {\n ctx.contextValue.caronteUser = undefined;\n }\n }\n }\n },\n };\n },\n };\n}\n\n/**\n * Factory that returns a per-resolver guard bound to a `CaronteClient`.\n *\n * @example\n * ```ts\n * const guard = createGuard(client);\n *\n * const resolvers = {\n * Query: {\n * tasks: guard('tasks:list', 'private', async (_parent, _args, context) => {\n * const user = context.caronteUser;\n * return db.tasks.findAll();\n * }),\n * },\n * };\n * ```\n */\nexport function createGuard(client: CaronteClient) {\n return function guard<TParent, TArgs, TContext extends CaronteContext, TReturn>(\n id: string,\n level: string,\n resolver: (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn,\n method?: string,\n ): (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (parent, args, context, info) => {\n const span = trace.getActiveSpan();\n span?.setAttribute('caronte.operation_id', id);\n\n if (level === 'public') {\n span?.setAttribute('caronte.permission_result', 'public');\n return resolver(parent, args, context, info);\n }\n if (!context.caronteUser) {\n span?.setAttribute('caronte.permission_result', 'unauthorized');\n throw new Error('Unauthorized: Bearer token required.');\n }\n const allowed = client.checkPermission(context.caronteUser, id, resolvedMethod);\n span?.setAttribute('caronte.permission_result', allowed ? 'allowed' : 'forbidden');\n if (!allowed) {\n throw new Error('Forbidden: Insufficient permissions.');\n }\n return resolver(parent, args, context, info);\n };\n };\n}"]}
@@ -1,5 +1,5 @@
1
1
  import { BaseContext, ApolloServerPlugin } from '@apollo/server';
2
- import { T as TokenClaims, C as CaronteClient } from '../client-TcOSwgrn.cjs';
2
+ import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.cjs';
3
3
 
4
4
  interface CaronteContext extends BaseContext {
5
5
  caronteUser?: TokenClaims;
@@ -1,5 +1,5 @@
1
1
  import { BaseContext, ApolloServerPlugin } from '@apollo/server';
2
- import { T as TokenClaims, C as CaronteClient } from '../client-TcOSwgrn.js';
2
+ import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.js';
3
3
 
4
4
  interface CaronteContext extends BaseContext {
5
5
  caronteUser?: TokenClaims;
@@ -1,7 +1,7 @@
1
1
  import { CaronteTokenError } from '../chunk-VEDVQOBD.js';
2
2
  import { detectMethod, registerOperation } from '../chunk-HV6X5RJY.js';
3
+ import { trace } from '@opentelemetry/api';
3
4
 
4
- // src/adapters/apollo.ts
5
5
  function carontePlugin(client) {
6
6
  return {
7
7
  async requestDidStart() {
@@ -14,6 +14,7 @@ function carontePlugin(client) {
14
14
  if (scheme?.toLowerCase() === "bearer" && token) {
15
15
  try {
16
16
  ctx.contextValue.caronteUser = await client.validateToken(token);
17
+ trace.getActiveSpan()?.setAttribute("caronte.user", ctx.contextValue.caronteUser.sub);
17
18
  } catch (err) {
18
19
  if (err instanceof CaronteTokenError) {
19
20
  ctx.contextValue.caronteUser = void 0;
@@ -30,9 +31,19 @@ function createGuard(client) {
30
31
  const resolvedMethod = method ?? detectMethod(id);
31
32
  registerOperation({ identifier: id, method: resolvedMethod, level });
32
33
  return (parent, args, context, info) => {
33
- if (level === "public") return resolver(parent, args, context, info);
34
- if (!context.caronteUser) throw new Error("Unauthorized: Bearer token required.");
35
- if (!client.checkPermission(context.caronteUser, id, resolvedMethod)) {
34
+ const span = trace.getActiveSpan();
35
+ span?.setAttribute("caronte.operation_id", id);
36
+ if (level === "public") {
37
+ span?.setAttribute("caronte.permission_result", "public");
38
+ return resolver(parent, args, context, info);
39
+ }
40
+ if (!context.caronteUser) {
41
+ span?.setAttribute("caronte.permission_result", "unauthorized");
42
+ throw new Error("Unauthorized: Bearer token required.");
43
+ }
44
+ const allowed = client.checkPermission(context.caronteUser, id, resolvedMethod);
45
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
46
+ if (!allowed) {
36
47
  throw new Error("Forbidden: Insufficient permissions.");
37
48
  }
38
49
  return resolver(parent, args, context, info);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/apollo.ts"],"names":[],"mappings":";;;;AAiBO,SAAS,cAAc,MAAA,EAA2D;AACvF,EAAA,OAAO;AAAA,IACL,MAAM,eAAA,GAAkB;AACtB,MAAA,OAAO;AAAA,QACL,MAAM,oBAAoB,GAAA,EAA4C;AACpE,UAAA,MAAM,WAAY,GAAA,CAAI,OAAA,CAAQ,MAAM,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA,IAAK,EAAA;AACpE,UAAA,MAAM,MAAA,GAAc,GAAA,CAAI,YAAA,EAAsB,gBAAA,EAAkB,aAAA,IAAwC,EAAA;AACxG,UAAA,MAAM,OAAY,QAAA,IAAY,MAAA;AAE9B,UAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,UAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,YAAA,IAAI;AACF,cAAA,GAAA,CAAI,YAAA,CAAa,WAAA,GAAc,MAAM,MAAA,CAAO,cAAc,KAAK,CAAA;AAAA,YACjE,SAAS,GAAA,EAAK;AACZ,cAAA,IAAI,eAAe,iBAAA,EAAmB;AACpC,gBAAA,GAAA,CAAI,aAAa,WAAA,GAAc,MAAA;AAAA,cACjC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF;AAmBO,SAAS,YAAY,MAAA,EAAuB;AACjD,EAAA,OAAO,SAAS,KAAA,CACd,EAAA,EACA,KAAA,EACA,UACA,MAAA,EAC6E;AAC7E,IAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,IAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,IAAA,OAAO,CAAC,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,IAAA,KAAS;AACtC,MAAA,IAAI,UAAU,QAAA,EAAU,OAAO,SAAS,MAAA,EAAQ,IAAA,EAAM,SAAS,IAAI,CAAA;AACnE,MAAA,IAAI,CAAC,OAAA,CAAQ,WAAA,EAAa,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAChF,MAAA,IAAI,CAAC,MAAA,CAAO,eAAA,CAAgB,QAAQ,WAAA,EAAa,EAAA,EAAI,cAAc,CAAA,EAAG;AACpE,QAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,MACxD;AACA,MAAA,OAAO,QAAA,CAAS,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,IAAI,CAAA;AAAA,IAC7C,CAAA;AAAA,EACF,CAAA;AACF","file":"apollo.js","sourcesContent":["import type { ApolloServerPlugin, BaseContext, GraphQLRequestContext } from '@apollo/server';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\nexport interface CaronteContext extends BaseContext {\n caronteUser?: TokenClaims;\n}\n\n/**\n * Apollo Server plugin for caronte.\n *\n * Validates the Bearer token on every request and stores the claims in\n * `context.caronteUser`. Use `createGuard(client)` to enforce per-resolver\n * permissions.\n */\nexport function carontePlugin(client: CaronteClient): ApolloServerPlugin<CaronteContext> {\n return {\n async requestDidStart() {\n return {\n async didResolveOperation(ctx: GraphQLRequestContext<CaronteContext>) {\n const httpAuth = ctx.request.http?.headers.get('authorization') ?? '';\n const wsAuth = ((ctx.contextValue as any)?.connectionParams?.Authorization as string | undefined) ?? '';\n const auth = httpAuth || wsAuth;\n\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n ctx.contextValue.caronteUser = await client.validateToken(token);\n } catch (err) {\n if (err instanceof CaronteTokenError) {\n ctx.contextValue.caronteUser = undefined;\n }\n }\n }\n },\n };\n },\n };\n}\n\n/**\n * Factory that returns a per-resolver guard bound to a `CaronteClient`.\n *\n * @example\n * ```ts\n * const guard = createGuard(client);\n *\n * const resolvers = {\n * Query: {\n * tasks: guard('tasks:list', 'private', async (_parent, _args, context) => {\n * const user = context.caronteUser;\n * return db.tasks.findAll();\n * }),\n * },\n * };\n * ```\n */\nexport function createGuard(client: CaronteClient) {\n return function guard<TParent, TArgs, TContext extends CaronteContext, TReturn>(\n id: string,\n level: string,\n resolver: (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn,\n method?: string,\n ): (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (parent, args, context, info) => {\n if (level === 'public') return resolver(parent, args, context, info);\n if (!context.caronteUser) throw new Error('Unauthorized: Bearer token required.');\n if (!client.checkPermission(context.caronteUser, id, resolvedMethod)) {\n throw new Error('Forbidden: Insufficient permissions.');\n }\n return resolver(parent, args, context, info);\n };\n };\n}"]}
1
+ {"version":3,"sources":["../../src/adapters/apollo.ts"],"names":[],"mappings":";;;;AAkBO,SAAS,cAAc,MAAA,EAA2D;AACvF,EAAA,OAAO;AAAA,IACL,MAAM,eAAA,GAAkB;AACtB,MAAA,OAAO;AAAA,QACL,MAAM,oBAAoB,GAAA,EAA4C;AACpE,UAAA,MAAM,WAAY,GAAA,CAAI,OAAA,CAAQ,MAAM,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA,IAAK,EAAA;AACpE,UAAA,MAAM,MAAA,GAAc,GAAA,CAAI,YAAA,EAAsB,gBAAA,EAAkB,aAAA,IAAwC,EAAA;AACxG,UAAA,MAAM,OAAY,QAAA,IAAY,MAAA;AAE9B,UAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,UAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,YAAA,IAAI;AACF,cAAA,GAAA,CAAI,YAAA,CAAa,WAAA,GAAc,MAAM,MAAA,CAAO,cAAc,KAAK,CAAA;AAC/D,cAAA,KAAA,CAAM,eAAc,EAAG,YAAA,CAAa,gBAAgB,GAAA,CAAI,YAAA,CAAa,YAAY,GAAG,CAAA;AAAA,YACtF,SAAS,GAAA,EAAK;AACZ,cAAA,IAAI,eAAe,iBAAA,EAAmB;AACpC,gBAAA,GAAA,CAAI,aAAa,WAAA,GAAc,MAAA;AAAA,cACjC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF;AAmBO,SAAS,YAAY,MAAA,EAAuB;AACjD,EAAA,OAAO,SAAS,KAAA,CACd,EAAA,EACA,KAAA,EACA,UACA,MAAA,EAC6E;AAC7E,IAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,IAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,IAAA,OAAO,CAAC,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,IAAA,KAAS;AACtC,MAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc;AACjC,MAAA,IAAA,EAAM,YAAA,CAAa,wBAAwB,EAAE,CAAA;AAE7C,MAAA,IAAI,UAAU,QAAA,EAAU;AACtB,QAAA,IAAA,EAAM,YAAA,CAAa,6BAA6B,QAAQ,CAAA;AACxD,QAAA,OAAO,QAAA,CAAS,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,IAAI,CAAA;AAAA,MAC7C;AACA,MAAA,IAAI,CAAC,QAAQ,WAAA,EAAa;AACxB,QAAA,IAAA,EAAM,YAAA,CAAa,6BAA6B,cAAc,CAAA;AAC9D,QAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,MACxD;AACA,MAAA,MAAM,UAAU,MAAA,CAAO,eAAA,CAAgB,OAAA,CAAQ,WAAA,EAAa,IAAI,cAAc,CAAA;AAC9E,MAAA,IAAA,EAAM,YAAA,CAAa,2BAAA,EAA6B,OAAA,GAAU,SAAA,GAAY,WAAW,CAAA;AACjF,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,MACxD;AACA,MAAA,OAAO,QAAA,CAAS,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,IAAI,CAAA;AAAA,IAC7C,CAAA;AAAA,EACF,CAAA;AACF","file":"apollo.js","sourcesContent":["import type { ApolloServerPlugin, BaseContext, GraphQLRequestContext } from '@apollo/server';\nimport { trace } from '@opentelemetry/api';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\nexport interface CaronteContext extends BaseContext {\n caronteUser?: TokenClaims;\n}\n\n/**\n * Apollo Server plugin for caronte.\n *\n * Validates the Bearer token on every request and stores the claims in\n * `context.caronteUser`. Use `createGuard(client)` to enforce per-resolver\n * permissions.\n */\nexport function carontePlugin(client: CaronteClient): ApolloServerPlugin<CaronteContext> {\n return {\n async requestDidStart() {\n return {\n async didResolveOperation(ctx: GraphQLRequestContext<CaronteContext>) {\n const httpAuth = ctx.request.http?.headers.get('authorization') ?? '';\n const wsAuth = ((ctx.contextValue as any)?.connectionParams?.Authorization as string | undefined) ?? '';\n const auth = httpAuth || wsAuth;\n\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n ctx.contextValue.caronteUser = await client.validateToken(token);\n trace.getActiveSpan()?.setAttribute('caronte.user', ctx.contextValue.caronteUser.sub);\n } catch (err) {\n if (err instanceof CaronteTokenError) {\n ctx.contextValue.caronteUser = undefined;\n }\n }\n }\n },\n };\n },\n };\n}\n\n/**\n * Factory that returns a per-resolver guard bound to a `CaronteClient`.\n *\n * @example\n * ```ts\n * const guard = createGuard(client);\n *\n * const resolvers = {\n * Query: {\n * tasks: guard('tasks:list', 'private', async (_parent, _args, context) => {\n * const user = context.caronteUser;\n * return db.tasks.findAll();\n * }),\n * },\n * };\n * ```\n */\nexport function createGuard(client: CaronteClient) {\n return function guard<TParent, TArgs, TContext extends CaronteContext, TReturn>(\n id: string,\n level: string,\n resolver: (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn,\n method?: string,\n ): (parent: TParent, args: TArgs, context: TContext, info: unknown) => TReturn {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (parent, args, context, info) => {\n const span = trace.getActiveSpan();\n span?.setAttribute('caronte.operation_id', id);\n\n if (level === 'public') {\n span?.setAttribute('caronte.permission_result', 'public');\n return resolver(parent, args, context, info);\n }\n if (!context.caronteUser) {\n span?.setAttribute('caronte.permission_result', 'unauthorized');\n throw new Error('Unauthorized: Bearer token required.');\n }\n const allowed = client.checkPermission(context.caronteUser, id, resolvedMethod);\n span?.setAttribute('caronte.permission_result', allowed ? 'allowed' : 'forbidden');\n if (!allowed) {\n throw new Error('Forbidden: Insufficient permissions.');\n }\n return resolver(parent, args, context, info);\n };\n };\n}"]}
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ var api = require('@opentelemetry/api');
4
+
5
+ // src/adapters/express.ts
6
+
3
7
  // src/registry.ts
4
8
  var _registry = [];
5
9
  function detectMethod(id) {
@@ -24,6 +28,7 @@ function caronte(client) {
24
28
  if (scheme?.toLowerCase() === "bearer" && token) {
25
29
  try {
26
30
  req.caronteUser = await client.validateToken(token);
31
+ api.trace.getActiveSpan()?.setAttribute("caronte.user", req.caronteUser.sub);
27
32
  } catch {
28
33
  }
29
34
  }
@@ -33,15 +38,20 @@ function caronte(client) {
33
38
  const resolvedMethod = method ?? detectMethod(id);
34
39
  registerOperation({ identifier: id, method: resolvedMethod, level });
35
40
  return (req, res, next) => {
41
+ const span = api.trace.getActiveSpan();
42
+ span?.setAttribute("caronte.operation_id", id);
36
43
  if (level === "public") {
44
+ span?.setAttribute("caronte.permission_result", "public");
37
45
  next();
38
46
  return;
39
47
  }
40
48
  if (!req.caronteUser) {
49
+ span?.setAttribute("caronte.permission_result", "unauthorized");
41
50
  res.status(401).json({ error: "unauthorized", detail: "Bearer token required." });
42
51
  return;
43
52
  }
44
53
  const allowed = client.checkPermission(req.caronteUser, id, resolvedMethod);
54
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
45
55
  if (!allowed) {
46
56
  res.status(403).json({ error: "forbidden", detail: "Insufficient permissions." });
47
57
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/registry.ts","../../src/adapters/express.ts"],"names":[],"mappings":";;;AAEA,IAAM,YAAmC,EAAC;AAKnC,SAAS,aAAa,EAAA,EAAoB;AAC/C,EAAA,MAAM,IAAA,GAAA,CAAQ,GAAG,KAAA,CAAM,GAAG,EAAE,GAAA,EAAI,IAAK,IAAI,WAAA,EAAY;AACrD,EAAA,IAAI,wBAAA,CAAyB,IAAA,CAAK,IAAI,CAAA,EAAS,OAAO,MAAA;AACtD,EAAA,IAAI,0BAAA,CAA2B,IAAA,CAAK,IAAI,CAAA,EAAQ,OAAO,QAAA;AACvD,EAAA,IAAI,iCAAA,CAAkC,IAAA,CAAK,IAAI,CAAA,EAAG,OAAO,QAAA;AACzD,EAAA,OAAO,OAAA;AACT;AAGO,SAAS,kBAAkB,EAAA,EAA+B;AAC/D,EAAA,MAAM,SAAS,SAAA,CAAU,IAAA;AAAA,IACvB,OAAK,CAAA,CAAE,UAAA,KAAe,GAAG,UAAA,IAAc,CAAA,CAAE,WAAW,EAAA,CAAG;AAAA,GACzD;AACA,EAAA,IAAI,CAAC,MAAA,EAAQ,SAAA,CAAU,IAAA,CAAK,EAAE,CAAA;AAChC;;;ACCO,SAAS,QAAQ,MAAA,EAA0C;AAEhE,EAAA,MAAM,UAAA,GAAa,OAAO,GAAA,EAAc,IAAA,EAAgB,IAAA,KAAsC;AAC5F,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAQ,aAAA,IAAiB,EAAA;AAC1C,IAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,IAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,MAAA,IAAI;AACF,QAAA,GAAA,CAAI,WAAA,GAAc,MAAM,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA;AAAA,MACpD,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AACA,IAAA,IAAA,EAAK;AAAA,EACP,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,EAAA,EAAY,KAAA,EAAe,MAAA,KAAoB;AAC5D,IAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,IAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,IAAA,OAAO,CAAC,GAAA,EAAc,GAAA,EAAe,IAAA,KAA6B;AAChE,MAAA,IAAI,UAAU,QAAA,EAAU;AAAE,QAAA,IAAA,EAAK;AAAG,QAAA;AAAA,MAAQ;AAE1C,MAAA,IAAI,CAAC,IAAI,WAAA,EAAa;AACpB,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,MAAA,EAAQ,wBAAA,EAA0B,CAAA;AAChF,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,UAAU,MAAA,CAAO,eAAA,CAAgB,GAAA,CAAI,WAAA,EAAa,IAAI,cAAc,CAAA;AAC1E,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,2BAAA,EAA6B,CAAA;AAChF,QAAA;AAAA,MACF;AAEA,MAAA,IAAA,EAAK;AAAA,IACP,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,EAAE,YAAY,KAAA,EAAM;AAC7B","file":"express.cjs","sourcesContent":["import type { OperationDescriptor } from './models.js';\n\nconst _registry: OperationDescriptor[] = [];\n\n/** Infer operation method from the last segment of the operation id.\n * e.g. \"tasks:list\" → \"read\", \"tasks:create\" → \"write\", \"tasks:delete\" → \"delete\"\n */\nexport function detectMethod(id: string): string {\n const last = (id.split(':').pop() ?? '').toLowerCase();\n if (/^(get|list|fetch|read)/.test(last)) return 'read';\n if (/^(delete|remove|destroy)/.test(last)) return 'delete';\n if (/(stream|subscribe|watch|listen)/.test(last)) return 'stream';\n return 'write';\n}\n\n/** Register an operation in the global registry (used by startup to sync). */\nexport function registerOperation(op: OperationDescriptor): void {\n const exists = _registry.some(\n o => o.identifier === op.identifier && o.method === op.method,\n );\n if (!exists) _registry.push(op);\n}\n\nexport function getRegistry(): OperationDescriptor[] {\n return [..._registry];\n}\n\nexport function clearRegistry(): void {\n _registry.length = 0;\n}","import type { NextFunction, Request, Response } from 'express';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare global {\n namespace Express {\n interface Request {\n caronteUser?: TokenClaims;\n }\n }\n}\n\nexport interface CaronteMiddleware {\n /** Global middleware — validates the Bearer token and injects `req.caronteUser`. */\n middleware: (req: Request, res: Response, next: NextFunction) => Promise<void>;\n /** Per-route guard — registers the operation and enforces permission at request time. */\n guard: (id: string, level: string, method?: string) =>\n (req: Request, res: Response, next: NextFunction) => void;\n}\n\nexport function caronte(client: CaronteClient): CaronteMiddleware {\n\n const middleware = async (req: Request, _res: Response, next: NextFunction): Promise<void> => {\n const auth = req.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n req.caronteUser = await client.validateToken(token);\n } catch {\n // invalid token — caronteUser stays undefined; guard handles the 401\n }\n }\n next();\n };\n\n const guard = (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n if (level === 'public') { next(); return; }\n\n if (!req.caronteUser) {\n res.status(401).json({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(req.caronteUser, id, resolvedMethod);\n if (!allowed) {\n res.status(403).json({ error: 'forbidden', detail: 'Insufficient permissions.' });\n return;\n }\n\n next();\n };\n };\n\n return { middleware, guard };\n}"]}
1
+ {"version":3,"sources":["../../src/registry.ts","../../src/adapters/express.ts"],"names":["trace"],"mappings":"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","file":"express.cjs","sourcesContent":["import type { OperationDescriptor } from './models.js';\n\nconst _registry: OperationDescriptor[] = [];\n\n/** Infer operation method from the last segment of the operation id.\n * e.g. \"tasks:list\" → \"read\", \"tasks:create\" → \"write\", \"tasks:delete\" → \"delete\"\n */\nexport function detectMethod(id: string): string {\n const last = (id.split(':').pop() ?? '').toLowerCase();\n if (/^(get|list|fetch|read)/.test(last)) return 'read';\n if (/^(delete|remove|destroy)/.test(last)) return 'delete';\n if (/(stream|subscribe|watch|listen)/.test(last)) return 'stream';\n return 'write';\n}\n\n/** Register an operation in the global registry (used by startup to sync). */\nexport function registerOperation(op: OperationDescriptor): void {\n const exists = _registry.some(\n o => o.identifier === op.identifier && o.method === op.method,\n );\n if (!exists) _registry.push(op);\n}\n\nexport function getRegistry(): OperationDescriptor[] {\n return [..._registry];\n}\n\nexport function clearRegistry(): void {\n _registry.length = 0;\n}","import type { NextFunction, Request, Response } from 'express';\nimport { trace } from '@opentelemetry/api';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare global {\n namespace Express {\n interface Request {\n caronteUser?: TokenClaims;\n }\n }\n}\n\nexport interface CaronteMiddleware {\n /** Global middleware — validates the Bearer token and injects `req.caronteUser`. */\n middleware: (req: Request, res: Response, next: NextFunction) => Promise<void>;\n /** Per-route guard — registers the operation and enforces permission at request time. */\n guard: (id: string, level: string, method?: string) =>\n (req: Request, res: Response, next: NextFunction) => void;\n}\n\nexport function caronte(client: CaronteClient): CaronteMiddleware {\n\n const middleware = async (req: Request, _res: Response, next: NextFunction): Promise<void> => {\n const auth = req.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n req.caronteUser = await client.validateToken(token);\n trace.getActiveSpan()?.setAttribute('caronte.user', req.caronteUser.sub);\n } catch {\n // invalid token — caronteUser stays undefined; guard handles the 401\n }\n }\n next();\n };\n\n const guard = (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const span = trace.getActiveSpan();\n span?.setAttribute('caronte.operation_id', id);\n\n if (level === 'public') {\n span?.setAttribute('caronte.permission_result', 'public');\n next();\n return;\n }\n\n if (!req.caronteUser) {\n span?.setAttribute('caronte.permission_result', 'unauthorized');\n res.status(401).json({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(req.caronteUser, id, resolvedMethod);\n span?.setAttribute('caronte.permission_result', allowed ? 'allowed' : 'forbidden');\n if (!allowed) {\n res.status(403).json({ error: 'forbidden', detail: 'Insufficient permissions.' });\n return;\n }\n\n next();\n };\n };\n\n return { middleware, guard };\n}"]}
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import { T as TokenClaims, C as CaronteClient } from '../client-TcOSwgrn.cjs';
2
+ import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.cjs';
3
3
 
4
4
  declare global {
5
5
  namespace Express {
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import { T as TokenClaims, C as CaronteClient } from '../client-TcOSwgrn.js';
2
+ import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.js';
3
3
 
4
4
  declare global {
5
5
  namespace Express {
@@ -1,6 +1,6 @@
1
1
  import { detectMethod, registerOperation } from '../chunk-HV6X5RJY.js';
2
+ import { trace } from '@opentelemetry/api';
2
3
 
3
- // src/adapters/express.ts
4
4
  function caronte(client) {
5
5
  const middleware = async (req, _res, next) => {
6
6
  const auth = req.headers.authorization ?? "";
@@ -8,6 +8,7 @@ function caronte(client) {
8
8
  if (scheme?.toLowerCase() === "bearer" && token) {
9
9
  try {
10
10
  req.caronteUser = await client.validateToken(token);
11
+ trace.getActiveSpan()?.setAttribute("caronte.user", req.caronteUser.sub);
11
12
  } catch {
12
13
  }
13
14
  }
@@ -17,15 +18,20 @@ function caronte(client) {
17
18
  const resolvedMethod = method ?? detectMethod(id);
18
19
  registerOperation({ identifier: id, method: resolvedMethod, level });
19
20
  return (req, res, next) => {
21
+ const span = trace.getActiveSpan();
22
+ span?.setAttribute("caronte.operation_id", id);
20
23
  if (level === "public") {
24
+ span?.setAttribute("caronte.permission_result", "public");
21
25
  next();
22
26
  return;
23
27
  }
24
28
  if (!req.caronteUser) {
29
+ span?.setAttribute("caronte.permission_result", "unauthorized");
25
30
  res.status(401).json({ error: "unauthorized", detail: "Bearer token required." });
26
31
  return;
27
32
  }
28
33
  const allowed = client.checkPermission(req.caronteUser, id, resolvedMethod);
34
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
29
35
  if (!allowed) {
30
36
  res.status(403).json({ error: "forbidden", detail: "Insufficient permissions." });
31
37
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/express.ts"],"names":[],"mappings":";;;AAsBO,SAAS,QAAQ,MAAA,EAA0C;AAEhE,EAAA,MAAM,UAAA,GAAa,OAAO,GAAA,EAAc,IAAA,EAAgB,IAAA,KAAsC;AAC5F,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAQ,aAAA,IAAiB,EAAA;AAC1C,IAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,IAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,MAAA,IAAI;AACF,QAAA,GAAA,CAAI,WAAA,GAAc,MAAM,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA;AAAA,MACpD,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AACA,IAAA,IAAA,EAAK;AAAA,EACP,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,EAAA,EAAY,KAAA,EAAe,MAAA,KAAoB;AAC5D,IAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,IAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,IAAA,OAAO,CAAC,GAAA,EAAc,GAAA,EAAe,IAAA,KAA6B;AAChE,MAAA,IAAI,UAAU,QAAA,EAAU;AAAE,QAAA,IAAA,EAAK;AAAG,QAAA;AAAA,MAAQ;AAE1C,MAAA,IAAI,CAAC,IAAI,WAAA,EAAa;AACpB,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,MAAA,EAAQ,wBAAA,EAA0B,CAAA;AAChF,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,UAAU,MAAA,CAAO,eAAA,CAAgB,GAAA,CAAI,WAAA,EAAa,IAAI,cAAc,CAAA;AAC1E,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,2BAAA,EAA6B,CAAA;AAChF,QAAA;AAAA,MACF;AAEA,MAAA,IAAA,EAAK;AAAA,IACP,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,EAAE,YAAY,KAAA,EAAM;AAC7B","file":"express.js","sourcesContent":["import type { NextFunction, Request, Response } from 'express';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare global {\n namespace Express {\n interface Request {\n caronteUser?: TokenClaims;\n }\n }\n}\n\nexport interface CaronteMiddleware {\n /** Global middleware — validates the Bearer token and injects `req.caronteUser`. */\n middleware: (req: Request, res: Response, next: NextFunction) => Promise<void>;\n /** Per-route guard — registers the operation and enforces permission at request time. */\n guard: (id: string, level: string, method?: string) =>\n (req: Request, res: Response, next: NextFunction) => void;\n}\n\nexport function caronte(client: CaronteClient): CaronteMiddleware {\n\n const middleware = async (req: Request, _res: Response, next: NextFunction): Promise<void> => {\n const auth = req.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n req.caronteUser = await client.validateToken(token);\n } catch {\n // invalid token — caronteUser stays undefined; guard handles the 401\n }\n }\n next();\n };\n\n const guard = (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n if (level === 'public') { next(); return; }\n\n if (!req.caronteUser) {\n res.status(401).json({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(req.caronteUser, id, resolvedMethod);\n if (!allowed) {\n res.status(403).json({ error: 'forbidden', detail: 'Insufficient permissions.' });\n return;\n }\n\n next();\n };\n };\n\n return { middleware, guard };\n}"]}
1
+ {"version":3,"sources":["../../src/adapters/express.ts"],"names":[],"mappings":";;;AAuBO,SAAS,QAAQ,MAAA,EAA0C;AAEhE,EAAA,MAAM,UAAA,GAAa,OAAO,GAAA,EAAc,IAAA,EAAgB,IAAA,KAAsC;AAC5F,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAQ,aAAA,IAAiB,EAAA;AAC1C,IAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,IAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,MAAA,IAAI;AACF,QAAA,GAAA,CAAI,WAAA,GAAc,MAAM,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA;AAClD,QAAA,KAAA,CAAM,eAAc,EAAG,YAAA,CAAa,cAAA,EAAgB,GAAA,CAAI,YAAY,GAAG,CAAA;AAAA,MACzE,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AACA,IAAA,IAAA,EAAK;AAAA,EACP,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,EAAA,EAAY,KAAA,EAAe,MAAA,KAAoB;AAC5D,IAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,IAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,IAAA,OAAO,CAAC,GAAA,EAAc,GAAA,EAAe,IAAA,KAA6B;AAChE,MAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc;AACjC,MAAA,IAAA,EAAM,YAAA,CAAa,wBAAwB,EAAE,CAAA;AAE7C,MAAA,IAAI,UAAU,QAAA,EAAU;AACtB,QAAA,IAAA,EAAM,YAAA,CAAa,6BAA6B,QAAQ,CAAA;AACxD,QAAA,IAAA,EAAK;AACL,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,IAAI,WAAA,EAAa;AACpB,QAAA,IAAA,EAAM,YAAA,CAAa,6BAA6B,cAAc,CAAA;AAC9D,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,MAAA,EAAQ,wBAAA,EAA0B,CAAA;AAChF,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,UAAU,MAAA,CAAO,eAAA,CAAgB,GAAA,CAAI,WAAA,EAAa,IAAI,cAAc,CAAA;AAC1E,MAAA,IAAA,EAAM,YAAA,CAAa,2BAAA,EAA6B,OAAA,GAAU,SAAA,GAAY,WAAW,CAAA;AACjF,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,2BAAA,EAA6B,CAAA;AAChF,QAAA;AAAA,MACF;AAEA,MAAA,IAAA,EAAK;AAAA,IACP,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,EAAE,YAAY,KAAA,EAAM;AAC7B","file":"express.js","sourcesContent":["import type { NextFunction, Request, Response } from 'express';\nimport { trace } from '@opentelemetry/api';\nimport { CaronteClient } from '../client.js';\nimport { CaronteTokenError } from '../exceptions.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare global {\n namespace Express {\n interface Request {\n caronteUser?: TokenClaims;\n }\n }\n}\n\nexport interface CaronteMiddleware {\n /** Global middleware — validates the Bearer token and injects `req.caronteUser`. */\n middleware: (req: Request, res: Response, next: NextFunction) => Promise<void>;\n /** Per-route guard — registers the operation and enforces permission at request time. */\n guard: (id: string, level: string, method?: string) =>\n (req: Request, res: Response, next: NextFunction) => void;\n}\n\nexport function caronte(client: CaronteClient): CaronteMiddleware {\n\n const middleware = async (req: Request, _res: Response, next: NextFunction): Promise<void> => {\n const auth = req.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n req.caronteUser = await client.validateToken(token);\n trace.getActiveSpan()?.setAttribute('caronte.user', req.caronteUser.sub);\n } catch {\n // invalid token — caronteUser stays undefined; guard handles the 401\n }\n }\n next();\n };\n\n const guard = (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const span = trace.getActiveSpan();\n span?.setAttribute('caronte.operation_id', id);\n\n if (level === 'public') {\n span?.setAttribute('caronte.permission_result', 'public');\n next();\n return;\n }\n\n if (!req.caronteUser) {\n span?.setAttribute('caronte.permission_result', 'unauthorized');\n res.status(401).json({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(req.caronteUser, id, resolvedMethod);\n span?.setAttribute('caronte.permission_result', allowed ? 'allowed' : 'forbidden');\n if (!allowed) {\n res.status(403).json({ error: 'forbidden', detail: 'Insufficient permissions.' });\n return;\n }\n\n next();\n };\n };\n\n return { middleware, guard };\n}"]}
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var fp = require('fastify-plugin');
4
+ var api = require('@opentelemetry/api');
4
5
 
5
6
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
7
 
@@ -33,6 +34,7 @@ var carontePlugin = async (fastify, { client }) => {
33
34
  if (scheme?.toLowerCase() === "bearer" && token) {
34
35
  try {
35
36
  request.caronteUser = await client.validateToken(token);
37
+ api.trace.getActiveSpan()?.setAttribute("caronte.user", request.caronteUser.sub);
36
38
  } catch {
37
39
  }
38
40
  }
@@ -43,12 +45,19 @@ var carontePlugin = async (fastify, { client }) => {
43
45
  const resolvedMethod = method ?? detectMethod(id);
44
46
  registerOperation({ identifier: id, method: resolvedMethod, level });
45
47
  return async (request, reply) => {
46
- if (level === "public") return;
48
+ const span = api.trace.getActiveSpan();
49
+ span?.setAttribute("caronte.operation_id", id);
50
+ if (level === "public") {
51
+ span?.setAttribute("caronte.permission_result", "public");
52
+ return;
53
+ }
47
54
  if (!request.caronteUser) {
55
+ span?.setAttribute("caronte.permission_result", "unauthorized");
48
56
  await reply.status(401).send({ error: "unauthorized", detail: "Bearer token required." });
49
57
  return;
50
58
  }
51
59
  const allowed = client.checkPermission(request.caronteUser, id, resolvedMethod);
60
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
52
61
  if (!allowed) {
53
62
  await reply.status(403).send({ error: "forbidden", detail: "Insufficient permissions." });
54
63
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/registry.ts","../../src/adapters/fastify.ts"],"names":["fp"],"mappings":";;;;;;;;;;;AAEA,IAAM,YAAmC,EAAC;AAKnC,SAAS,aAAa,EAAA,EAAoB;AAC/C,EAAA,MAAM,IAAA,GAAA,CAAQ,GAAG,KAAA,CAAM,GAAG,EAAE,GAAA,EAAI,IAAK,IAAI,WAAA,EAAY;AACrD,EAAA,IAAI,wBAAA,CAAyB,IAAA,CAAK,IAAI,CAAA,EAAS,OAAO,MAAA;AACtD,EAAA,IAAI,0BAAA,CAA2B,IAAA,CAAK,IAAI,CAAA,EAAQ,OAAO,QAAA;AACvD,EAAA,IAAI,iCAAA,CAAkC,IAAA,CAAK,IAAI,CAAA,EAAG,OAAO,QAAA;AACzD,EAAA,OAAO,OAAA;AACT;AAGO,SAAS,kBAAkB,EAAA,EAA+B;AAC/D,EAAA,MAAM,SAAS,SAAA,CAAU,IAAA;AAAA,IACvB,OAAK,CAAA,CAAE,UAAA,KAAe,GAAG,UAAA,IAAc,CAAA,CAAE,WAAW,EAAA,CAAG;AAAA,GACzD;AACA,EAAA,IAAI,CAAC,MAAA,EAAQ,SAAA,CAAU,IAAA,CAAK,EAAE,CAAA;AAChC;;;ACLA,IAAM,aAAA,GAA0D,OAC9D,OAAA,EACA,EAAE,QAAO,KACN;AAEH,EAAA,OAAA,CAAQ,eAAA,CAAyC,eAAe,MAAS,CAAA;AAGzE,EAAA,OAAA,CAAQ,OAAA,CAAQ,WAAA,EAAa,OAAO,OAAA,KAA4B;AAC9D,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAA,CAAQ,aAAA,IAAiB,EAAA;AAC9C,IAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,IAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,WAAA,GAAc,MAAM,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA;AAAA,MACxD,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,OAAA,CAAQ,QAAA;AAAA,IACN,cAAA;AAAA,IACA,CAAC,EAAA,EAAY,KAAA,EAAe,MAAA,KAAoB;AAC9C,MAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,MAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,MAAA,OAAO,OAAO,SAAyB,KAAA,KAAuC;AAC5E,QAAA,IAAI,UAAU,QAAA,EAAU;AAExB,QAAA,IAAI,CAAC,QAAQ,WAAA,EAAa;AACxB,UAAA,MAAM,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,MAAA,EAAQ,wBAAA,EAA0B,CAAA;AACxF,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,UAAU,MAAA,CAAO,eAAA,CAAgB,OAAA,CAAQ,WAAA,EAAa,IAAI,cAAc,CAAA;AAC9E,QAAA,IAAI,CAAC,OAAA,EAAS;AACZ,UAAA,MAAM,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,2BAAA,EAA6B,CAAA;AAAA,QAC1F;AAAA,MACF,CAAA;AAAA,IACF;AAAA,GACF;AACF,CAAA;AAEO,IAAM,oBAAA,GAAuBA,oBAAG,aAAA,EAAe;AAAA,EACpD,IAAA,EAAM,SAAA;AAAA,EACN,OAAA,EAAS;AACX,CAAC","file":"fastify.cjs","sourcesContent":["import type { OperationDescriptor } from './models.js';\n\nconst _registry: OperationDescriptor[] = [];\n\n/** Infer operation method from the last segment of the operation id.\n * e.g. \"tasks:list\" → \"read\", \"tasks:create\" → \"write\", \"tasks:delete\" → \"delete\"\n */\nexport function detectMethod(id: string): string {\n const last = (id.split(':').pop() ?? '').toLowerCase();\n if (/^(get|list|fetch|read)/.test(last)) return 'read';\n if (/^(delete|remove|destroy)/.test(last)) return 'delete';\n if (/(stream|subscribe|watch|listen)/.test(last)) return 'stream';\n return 'write';\n}\n\n/** Register an operation in the global registry (used by startup to sync). */\nexport function registerOperation(op: OperationDescriptor): void {\n const exists = _registry.some(\n o => o.identifier === op.identifier && o.method === op.method,\n );\n if (!exists) _registry.push(op);\n}\n\nexport function getRegistry(): OperationDescriptor[] {\n return [..._registry];\n}\n\nexport function clearRegistry(): void {\n _registry.length = 0;\n}","import type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\nimport fp from 'fastify-plugin';\nimport { CaronteClient } from '../client.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n caronteUser?: TokenClaims;\n }\n}\n\nexport interface CarontePluginOptions {\n client: CaronteClient;\n}\n\nconst carontePlugin: FastifyPluginAsync<CarontePluginOptions> = async (\n fastify: FastifyInstance,\n { client }: CarontePluginOptions,\n) => {\n // Decorate request with caronteUser\n fastify.decorateRequest<TokenClaims | undefined>('caronteUser', undefined);\n\n // Global hook — validates Bearer token on every request\n fastify.addHook('onRequest', async (request: FastifyRequest) => {\n const auth = request.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n request.caronteUser = await client.validateToken(token);\n } catch {\n // invalid token — caronteUser stays null; guard handles the 401\n }\n }\n });\n\n // Decorate fastify with a guard factory\n fastify.decorate(\n 'caronteGuard',\n (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n if (level === 'public') return;\n\n if (!request.caronteUser) {\n await reply.status(401).send({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(request.caronteUser, id, resolvedMethod);\n if (!allowed) {\n await reply.status(403).send({ error: 'forbidden', detail: 'Insufficient permissions.' });\n }\n };\n },\n );\n};\n\nexport const caronteFastifyPlugin = fp(carontePlugin, {\n name: 'caronte',\n fastify: '>=4',\n});\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n caronteGuard: (\n id: string,\n level: string,\n method?: string,\n ) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;\n }\n}"]}
1
+ {"version":3,"sources":["../../src/registry.ts","../../src/adapters/fastify.ts"],"names":["trace","fp"],"mappings":"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","file":"fastify.cjs","sourcesContent":["import type { OperationDescriptor } from './models.js';\n\nconst _registry: OperationDescriptor[] = [];\n\n/** Infer operation method from the last segment of the operation id.\n * e.g. \"tasks:list\" → \"read\", \"tasks:create\" → \"write\", \"tasks:delete\" → \"delete\"\n */\nexport function detectMethod(id: string): string {\n const last = (id.split(':').pop() ?? '').toLowerCase();\n if (/^(get|list|fetch|read)/.test(last)) return 'read';\n if (/^(delete|remove|destroy)/.test(last)) return 'delete';\n if (/(stream|subscribe|watch|listen)/.test(last)) return 'stream';\n return 'write';\n}\n\n/** Register an operation in the global registry (used by startup to sync). */\nexport function registerOperation(op: OperationDescriptor): void {\n const exists = _registry.some(\n o => o.identifier === op.identifier && o.method === op.method,\n );\n if (!exists) _registry.push(op);\n}\n\nexport function getRegistry(): OperationDescriptor[] {\n return [..._registry];\n}\n\nexport function clearRegistry(): void {\n _registry.length = 0;\n}","import type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\nimport fp from 'fastify-plugin';\nimport { trace } from '@opentelemetry/api';\nimport { CaronteClient } from '../client.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n caronteUser?: TokenClaims;\n }\n}\n\nexport interface CarontePluginOptions {\n client: CaronteClient;\n}\n\nconst carontePlugin: FastifyPluginAsync<CarontePluginOptions> = async (\n fastify: FastifyInstance,\n { client }: CarontePluginOptions,\n) => {\n // Decorate request with caronteUser\n fastify.decorateRequest<TokenClaims | undefined>('caronteUser', undefined);\n\n // Global hook — validates Bearer token on every request\n fastify.addHook('onRequest', async (request: FastifyRequest) => {\n const auth = request.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n request.caronteUser = await client.validateToken(token);\n trace.getActiveSpan()?.setAttribute('caronte.user', request.caronteUser.sub);\n } catch {\n // invalid token — caronteUser stays null; guard handles the 401\n }\n }\n });\n\n // Decorate fastify with a guard factory\n fastify.decorate(\n 'caronteGuard',\n (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n const span = trace.getActiveSpan();\n span?.setAttribute('caronte.operation_id', id);\n\n if (level === 'public') {\n span?.setAttribute('caronte.permission_result', 'public');\n return;\n }\n\n if (!request.caronteUser) {\n span?.setAttribute('caronte.permission_result', 'unauthorized');\n await reply.status(401).send({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(request.caronteUser, id, resolvedMethod);\n span?.setAttribute('caronte.permission_result', allowed ? 'allowed' : 'forbidden');\n if (!allowed) {\n await reply.status(403).send({ error: 'forbidden', detail: 'Insufficient permissions.' });\n }\n };\n },\n );\n};\n\nexport const caronteFastifyPlugin = fp(carontePlugin, {\n name: 'caronte',\n fastify: '>=4',\n});\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n caronteGuard: (\n id: string,\n level: string,\n method?: string,\n ) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;\n }\n}"]}
@@ -1,5 +1,5 @@
1
1
  import { FastifyReply, FastifyPluginAsync } from 'fastify';
2
- import { T as TokenClaims, C as CaronteClient } from '../client-TcOSwgrn.cjs';
2
+ import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.cjs';
3
3
 
4
4
  interface CarontePluginOptions {
5
5
  client: CaronteClient;
@@ -1,5 +1,5 @@
1
1
  import { FastifyReply, FastifyPluginAsync } from 'fastify';
2
- import { T as TokenClaims, C as CaronteClient } from '../client-TcOSwgrn.js';
2
+ import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.js';
3
3
 
4
4
  interface CarontePluginOptions {
5
5
  client: CaronteClient;
@@ -1,5 +1,6 @@
1
1
  import { detectMethod, registerOperation } from '../chunk-HV6X5RJY.js';
2
2
  import fp from 'fastify-plugin';
3
+ import { trace } from '@opentelemetry/api';
3
4
 
4
5
  var carontePlugin = async (fastify, { client }) => {
5
6
  fastify.decorateRequest("caronteUser", void 0);
@@ -9,6 +10,7 @@ var carontePlugin = async (fastify, { client }) => {
9
10
  if (scheme?.toLowerCase() === "bearer" && token) {
10
11
  try {
11
12
  request.caronteUser = await client.validateToken(token);
13
+ trace.getActiveSpan()?.setAttribute("caronte.user", request.caronteUser.sub);
12
14
  } catch {
13
15
  }
14
16
  }
@@ -19,12 +21,19 @@ var carontePlugin = async (fastify, { client }) => {
19
21
  const resolvedMethod = method ?? detectMethod(id);
20
22
  registerOperation({ identifier: id, method: resolvedMethod, level });
21
23
  return async (request, reply) => {
22
- if (level === "public") return;
24
+ const span = trace.getActiveSpan();
25
+ span?.setAttribute("caronte.operation_id", id);
26
+ if (level === "public") {
27
+ span?.setAttribute("caronte.permission_result", "public");
28
+ return;
29
+ }
23
30
  if (!request.caronteUser) {
31
+ span?.setAttribute("caronte.permission_result", "unauthorized");
24
32
  await reply.status(401).send({ error: "unauthorized", detail: "Bearer token required." });
25
33
  return;
26
34
  }
27
35
  const allowed = client.checkPermission(request.caronteUser, id, resolvedMethod);
36
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
28
37
  if (!allowed) {
29
38
  await reply.status(403).send({ error: "forbidden", detail: "Insufficient permissions." });
30
39
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/fastify.ts"],"names":[],"mappings":";;;AAgBA,IAAM,aAAA,GAA0D,OAC9D,OAAA,EACA,EAAE,QAAO,KACN;AAEH,EAAA,OAAA,CAAQ,eAAA,CAAyC,eAAe,MAAS,CAAA;AAGzE,EAAA,OAAA,CAAQ,OAAA,CAAQ,WAAA,EAAa,OAAO,OAAA,KAA4B;AAC9D,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAA,CAAQ,aAAA,IAAiB,EAAA;AAC9C,IAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,IAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,WAAA,GAAc,MAAM,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA;AAAA,MACxD,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,OAAA,CAAQ,QAAA;AAAA,IACN,cAAA;AAAA,IACA,CAAC,EAAA,EAAY,KAAA,EAAe,MAAA,KAAoB;AAC9C,MAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,MAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,MAAA,OAAO,OAAO,SAAyB,KAAA,KAAuC;AAC5E,QAAA,IAAI,UAAU,QAAA,EAAU;AAExB,QAAA,IAAI,CAAC,QAAQ,WAAA,EAAa;AACxB,UAAA,MAAM,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,MAAA,EAAQ,wBAAA,EAA0B,CAAA;AACxF,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,UAAU,MAAA,CAAO,eAAA,CAAgB,OAAA,CAAQ,WAAA,EAAa,IAAI,cAAc,CAAA;AAC9E,QAAA,IAAI,CAAC,OAAA,EAAS;AACZ,UAAA,MAAM,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,2BAAA,EAA6B,CAAA;AAAA,QAC1F;AAAA,MACF,CAAA;AAAA,IACF;AAAA,GACF;AACF,CAAA;AAEO,IAAM,oBAAA,GAAuB,GAAG,aAAA,EAAe;AAAA,EACpD,IAAA,EAAM,SAAA;AAAA,EACN,OAAA,EAAS;AACX,CAAC","file":"fastify.js","sourcesContent":["import type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\nimport fp from 'fastify-plugin';\nimport { CaronteClient } from '../client.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n caronteUser?: TokenClaims;\n }\n}\n\nexport interface CarontePluginOptions {\n client: CaronteClient;\n}\n\nconst carontePlugin: FastifyPluginAsync<CarontePluginOptions> = async (\n fastify: FastifyInstance,\n { client }: CarontePluginOptions,\n) => {\n // Decorate request with caronteUser\n fastify.decorateRequest<TokenClaims | undefined>('caronteUser', undefined);\n\n // Global hook — validates Bearer token on every request\n fastify.addHook('onRequest', async (request: FastifyRequest) => {\n const auth = request.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n request.caronteUser = await client.validateToken(token);\n } catch {\n // invalid token — caronteUser stays null; guard handles the 401\n }\n }\n });\n\n // Decorate fastify with a guard factory\n fastify.decorate(\n 'caronteGuard',\n (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n if (level === 'public') return;\n\n if (!request.caronteUser) {\n await reply.status(401).send({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(request.caronteUser, id, resolvedMethod);\n if (!allowed) {\n await reply.status(403).send({ error: 'forbidden', detail: 'Insufficient permissions.' });\n }\n };\n },\n );\n};\n\nexport const caronteFastifyPlugin = fp(carontePlugin, {\n name: 'caronte',\n fastify: '>=4',\n});\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n caronteGuard: (\n id: string,\n level: string,\n method?: string,\n ) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;\n }\n}"]}
1
+ {"version":3,"sources":["../../src/adapters/fastify.ts"],"names":[],"mappings":";;;;AAiBA,IAAM,aAAA,GAA0D,OAC9D,OAAA,EACA,EAAE,QAAO,KACN;AAEH,EAAA,OAAA,CAAQ,eAAA,CAAyC,eAAe,MAAS,CAAA;AAGzE,EAAA,OAAA,CAAQ,OAAA,CAAQ,WAAA,EAAa,OAAO,OAAA,KAA4B;AAC9D,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAA,CAAQ,aAAA,IAAiB,EAAA;AAC9C,IAAA,MAAM,CAAC,MAAA,EAAQ,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,IAAA,IAAI,MAAA,EAAQ,WAAA,EAAY,KAAM,QAAA,IAAY,KAAA,EAAO;AAC/C,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,WAAA,GAAc,MAAM,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA;AACtD,QAAA,KAAA,CAAM,eAAc,EAAG,YAAA,CAAa,cAAA,EAAgB,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,MAC7E,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,OAAA,CAAQ,QAAA;AAAA,IACN,cAAA;AAAA,IACA,CAAC,EAAA,EAAY,KAAA,EAAe,MAAA,KAAoB;AAC9C,MAAA,MAAM,cAAA,GAAiB,MAAA,IAAU,YAAA,CAAa,EAAE,CAAA;AAChD,MAAA,iBAAA,CAAkB,EAAE,UAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,cAAA,EAAgB,OAAO,CAAA;AAEnE,MAAA,OAAO,OAAO,SAAyB,KAAA,KAAuC;AAC5E,QAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc;AACjC,QAAA,IAAA,EAAM,YAAA,CAAa,wBAAwB,EAAE,CAAA;AAE7C,QAAA,IAAI,UAAU,QAAA,EAAU;AACtB,UAAA,IAAA,EAAM,YAAA,CAAa,6BAA6B,QAAQ,CAAA;AACxD,UAAA;AAAA,QACF;AAEA,QAAA,IAAI,CAAC,QAAQ,WAAA,EAAa;AACxB,UAAA,IAAA,EAAM,YAAA,CAAa,6BAA6B,cAAc,CAAA;AAC9D,UAAA,MAAM,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,MAAA,EAAQ,wBAAA,EAA0B,CAAA;AACxF,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,UAAU,MAAA,CAAO,eAAA,CAAgB,OAAA,CAAQ,WAAA,EAAa,IAAI,cAAc,CAAA;AAC9E,QAAA,IAAA,EAAM,YAAA,CAAa,2BAAA,EAA6B,OAAA,GAAU,SAAA,GAAY,WAAW,CAAA;AACjF,QAAA,IAAI,CAAC,OAAA,EAAS;AACZ,UAAA,MAAM,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,2BAAA,EAA6B,CAAA;AAAA,QAC1F;AAAA,MACF,CAAA;AAAA,IACF;AAAA,GACF;AACF,CAAA;AAEO,IAAM,oBAAA,GAAuB,GAAG,aAAA,EAAe;AAAA,EACpD,IAAA,EAAM,SAAA;AAAA,EACN,OAAA,EAAS;AACX,CAAC","file":"fastify.js","sourcesContent":["import type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\nimport fp from 'fastify-plugin';\nimport { trace } from '@opentelemetry/api';\nimport { CaronteClient } from '../client.js';\nimport type { TokenClaims } from '../models.js';\nimport { detectMethod, registerOperation } from '../registry.js';\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n caronteUser?: TokenClaims;\n }\n}\n\nexport interface CarontePluginOptions {\n client: CaronteClient;\n}\n\nconst carontePlugin: FastifyPluginAsync<CarontePluginOptions> = async (\n fastify: FastifyInstance,\n { client }: CarontePluginOptions,\n) => {\n // Decorate request with caronteUser\n fastify.decorateRequest<TokenClaims | undefined>('caronteUser', undefined);\n\n // Global hook — validates Bearer token on every request\n fastify.addHook('onRequest', async (request: FastifyRequest) => {\n const auth = request.headers.authorization ?? '';\n const [scheme, token] = auth.split(' ');\n if (scheme?.toLowerCase() === 'bearer' && token) {\n try {\n request.caronteUser = await client.validateToken(token);\n trace.getActiveSpan()?.setAttribute('caronte.user', request.caronteUser.sub);\n } catch {\n // invalid token — caronteUser stays null; guard handles the 401\n }\n }\n });\n\n // Decorate fastify with a guard factory\n fastify.decorate(\n 'caronteGuard',\n (id: string, level: string, method?: string) => {\n const resolvedMethod = method ?? detectMethod(id);\n registerOperation({ identifier: id, method: resolvedMethod, level });\n\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n const span = trace.getActiveSpan();\n span?.setAttribute('caronte.operation_id', id);\n\n if (level === 'public') {\n span?.setAttribute('caronte.permission_result', 'public');\n return;\n }\n\n if (!request.caronteUser) {\n span?.setAttribute('caronte.permission_result', 'unauthorized');\n await reply.status(401).send({ error: 'unauthorized', detail: 'Bearer token required.' });\n return;\n }\n\n const allowed = client.checkPermission(request.caronteUser, id, resolvedMethod);\n span?.setAttribute('caronte.permission_result', allowed ? 'allowed' : 'forbidden');\n if (!allowed) {\n await reply.status(403).send({ error: 'forbidden', detail: 'Insufficient permissions.' });\n }\n };\n },\n );\n};\n\nexport const caronteFastifyPlugin = fp(carontePlugin, {\n name: 'caronte',\n fastify: '>=4',\n});\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n caronteGuard: (\n id: string,\n level: string,\n method?: string,\n ) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;\n }\n}"]}