@caronte-sdk/node 0.2.2 → 0.2.4

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 +66 -0
  2. package/bin/caronte-node.js +62 -0
  3. package/dist/adapters/apollo.cjs +21 -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 +18 -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 +70 -1
  22. package/dist/adapters/nest/index.cjs.map +1 -1
  23. package/dist/adapters/nest/index.d.cts +13 -1
  24. package/dist/adapters/nest/index.d.ts +13 -1
  25. package/dist/adapters/nest/index.js +24 -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 +16 -2
  37. package/dist/chunk-XMA2NAR7.js.map +0 -1
package/README.md CHANGED
@@ -115,6 +115,55 @@ export class TasksController {
115
115
  }
116
116
  ```
117
117
 
118
+ #### Running with `otelCollectorUrl` on NestJS
119
+
120
+ `CaronteModule.forRootAsync()` alone is **not enough** to get request tracing
121
+ in a Nest app, even outside the ESM case above. `NestFactory.create()`
122
+ requires `express` (or `fastify`) internally to build the HTTP adapter
123
+ *before* any module's own providers — including `CaronteModule` — get a
124
+ chance to run. By the time `CaronteModule`'s factory calls `startup()` (and,
125
+ through it, `initOtel()`), the HTTP framework has already finished loading
126
+ unpatched, so auto-instrumentation never attaches to it. This is a Nest
127
+ bootstrap-ordering issue, distinct from the ESM loader-hook issue — it
128
+ affects CommonJS Nest apps too.
129
+
130
+ Fix: call `initOtel()` yourself, **before** `import { NestFactory } from
131
+ '@nestjs/core'`:
132
+
133
+ ```ts
134
+ // main.ts
135
+ import 'reflect-metadata';
136
+
137
+ // Must run before @nestjs/core is required — see explanation above.
138
+ // CaronteModule still calls startup()/initOtel() too (harmless no-op,
139
+ // idempotent), so an app that skips this still gets traces/metrics/logs
140
+ // for everything except the HTTP layer itself.
141
+ import { initOtel } from '@caronte-sdk/node';
142
+ if (process.env.OTEL_COLLECTOR_URL) {
143
+ initOtel({
144
+ otelCollectorUrl: process.env.OTEL_COLLECTOR_URL,
145
+ appId: process.env.APP_ID!,
146
+ realmId: process.env.REALM_ID!,
147
+ });
148
+ }
149
+
150
+ import { NestFactory } from '@nestjs/core';
151
+ import { AppModule } from './app.module';
152
+
153
+ async function bootstrap() {
154
+ const app = await NestFactory.create(AppModule);
155
+ await app.listen(3000);
156
+ }
157
+ bootstrap();
158
+ ```
159
+
160
+ This works because in CommonJS, `require()` calls run in the textual order
161
+ they appear — the same reason `reflect-metadata` is conventionally imported
162
+ first in this ecosystem. It only works if `main.ts` itself is CommonJS
163
+ (no `"type": "module"` in `package.json`, e.g. run via `ts-node`); an ESM
164
+ Nest app needs the `caronte-node` wrapper from the section above instead —
165
+ in that case skip this manual `initOtel()` call, the loader hook handles it.
166
+
118
167
  ### Apollo Server
119
168
 
120
169
  ```ts
@@ -179,6 +228,23 @@ You can always pass the method explicitly as the third argument to `guard()`.
179
228
  | `realmId` | Name or UUID of the realm (e.g. `purp`) |
180
229
  | `appId` | UUID of the app registered in `auth.apps` |
181
230
  | `secret` | Plain-text app secret — use env vars, never commit |
231
+ | `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()`. |
232
+
233
+ ### Running with `otelCollectorUrl` on an ESM app
234
+
235
+ 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).
236
+
237
+ `@caronte-sdk/node` ships a `caronte-node` bin that handles this for you — put it in front of whatever already runs your app:
238
+
239
+ ```jsonc
240
+ // package.json
241
+ "scripts": {
242
+ "dev": "caronte-node tsx watch src/main.ts",
243
+ "start": "caronte-node tsx src/main.ts"
244
+ }
245
+ ```
246
+
247
+ 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
248
 
183
249
  ## How it works
184
250
 
@@ -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,12 +43,16 @@ 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;
45
50
  }
46
51
  }
47
52
  }
53
+ const operationName = ctx.operationName ?? ctx.operation?.name?.value;
54
+ if (operationName) api.trace.getActiveSpan()?.setAttribute("graphql.operation.name", operationName);
55
+ if (ctx.operation?.operation) api.trace.getActiveSpan()?.setAttribute("graphql.operation.type", ctx.operation.operation);
48
56
  }
49
57
  };
50
58
  }
@@ -55,9 +63,19 @@ function createGuard(client) {
55
63
  const resolvedMethod = method ?? detectMethod(id);
56
64
  registerOperation({ identifier: id, method: resolvedMethod, level });
57
65
  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)) {
66
+ const span = api.trace.getActiveSpan();
67
+ span?.setAttribute("caronte.operation_id", id);
68
+ if (level === "public") {
69
+ span?.setAttribute("caronte.permission_result", "public");
70
+ return resolver(parent, args, context, info);
71
+ }
72
+ if (!context.caronteUser) {
73
+ span?.setAttribute("caronte.permission_result", "unauthorized");
74
+ throw new Error("Unauthorized: Bearer token required.");
75
+ }
76
+ const allowed = client.checkPermission(context.caronteUser, id, resolvedMethod);
77
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
78
+ if (!allowed) {
61
79
  throw new Error("Forbidden: Insufficient permissions.");
62
80
  }
63
81
  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 // Sem isso, toda requisição GraphQL aparece nas listas de trace\n // como \"POST /graphql\" genérico -- o nome/tipo da operação\n // (query/mutation) só existe aqui dentro do Apollo, nunca chega\n // no span HTTP. O span ativo neste ponto é o span do middleware\n // Express que envolve o handler do Apollo (não o span raiz\n // \"POST /graphql\" -- instrumentation-express cria um span por\n // camada de middleware, todos filhos diretos do span raiz), então\n // o backend precisa pedir mais spans por trace (spss) pra achar\n // esse atributo -- ver tempo.module.ts.\n const operationName = ctx.operationName ?? ctx.operation?.name?.value;\n if (operationName) trace.getActiveSpan()?.setAttribute('graphql.operation.name', operationName);\n if (ctx.operation?.operation) trace.getActiveSpan()?.setAttribute('graphql.operation.type', ctx.operation.operation);\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,12 +14,16 @@ 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;
20
21
  }
21
22
  }
22
23
  }
24
+ const operationName = ctx.operationName ?? ctx.operation?.name?.value;
25
+ if (operationName) trace.getActiveSpan()?.setAttribute("graphql.operation.name", operationName);
26
+ if (ctx.operation?.operation) trace.getActiveSpan()?.setAttribute("graphql.operation.type", ctx.operation.operation);
23
27
  }
24
28
  };
25
29
  }
@@ -30,9 +34,19 @@ function createGuard(client) {
30
34
  const resolvedMethod = method ?? detectMethod(id);
31
35
  registerOperation({ identifier: id, method: resolvedMethod, level });
32
36
  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)) {
37
+ const span = trace.getActiveSpan();
38
+ span?.setAttribute("caronte.operation_id", id);
39
+ if (level === "public") {
40
+ span?.setAttribute("caronte.permission_result", "public");
41
+ return resolver(parent, args, context, info);
42
+ }
43
+ if (!context.caronteUser) {
44
+ span?.setAttribute("caronte.permission_result", "unauthorized");
45
+ throw new Error("Unauthorized: Bearer token required.");
46
+ }
47
+ const allowed = client.checkPermission(context.caronteUser, id, resolvedMethod);
48
+ span?.setAttribute("caronte.permission_result", allowed ? "allowed" : "forbidden");
49
+ if (!allowed) {
36
50
  throw new Error("Forbidden: Insufficient permissions.");
37
51
  }
38
52
  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":"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","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 // Sem isso, toda requisição GraphQL aparece nas listas de trace\n // como \"POST /graphql\" genérico -- o nome/tipo da operação\n // (query/mutation) só existe aqui dentro do Apollo, nunca chega\n // no span HTTP. O span ativo neste ponto é o span do middleware\n // Express que envolve o handler do Apollo (não o span raiz\n // \"POST /graphql\" -- instrumentation-express cria um span por\n // camada de middleware, todos filhos diretos do span raiz), então\n // o backend precisa pedir mais spans por trace (spss) pra achar\n // esse atributo -- ver tempo.module.ts.\n const operationName = ctx.operationName ?? ctx.operation?.name?.value;\n if (operationName) trace.getActiveSpan()?.setAttribute('graphql.operation.name', operationName);\n if (ctx.operation?.operation) trace.getActiveSpan()?.setAttribute('graphql.operation.type', ctx.operation.operation);\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
  }