@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.
- package/README.md +17 -0
- package/bin/caronte-node.js +62 -0
- package/dist/adapters/apollo.cjs +18 -3
- package/dist/adapters/apollo.cjs.map +1 -1
- package/dist/adapters/apollo.d.cts +1 -1
- package/dist/adapters/apollo.d.ts +1 -1
- package/dist/adapters/apollo.js +15 -4
- package/dist/adapters/apollo.js.map +1 -1
- package/dist/adapters/express.cjs +10 -0
- package/dist/adapters/express.cjs.map +1 -1
- package/dist/adapters/express.d.cts +1 -1
- package/dist/adapters/express.d.ts +1 -1
- package/dist/adapters/express.js +7 -1
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fastify.cjs +10 -1
- package/dist/adapters/fastify.cjs.map +1 -1
- package/dist/adapters/fastify.d.cts +1 -1
- package/dist/adapters/fastify.d.ts +1 -1
- package/dist/adapters/fastify.js +10 -1
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/nest/index.cjs +58 -1
- package/dist/adapters/nest/index.cjs.map +1 -1
- package/dist/adapters/nest/index.d.cts +1 -1
- package/dist/adapters/nest/index.d.ts +1 -1
- package/dist/adapters/nest/index.js +12 -2
- package/dist/adapters/nest/index.js.map +1 -1
- package/dist/{chunk-XMA2NAR7.js → chunk-M46BRRWA.js} +50 -3
- package/dist/chunk-M46BRRWA.js.map +1 -0
- package/dist/{client-TcOSwgrn.d.cts → client-CSokWFYn.d.cts} +3 -0
- package/dist/{client-TcOSwgrn.d.ts → client-CSokWFYn.d.ts} +3 -0
- package/dist/index.cjs +48 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +1 -1
- package/package.json +18 -5
- 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
|
+
});
|
package/dist/adapters/apollo.cjs
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
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":"
|
|
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-
|
|
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-
|
|
2
|
+
import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.js';
|
|
3
3
|
|
|
4
4
|
interface CaronteContext extends BaseContext {
|
|
5
5
|
caronteUser?: TokenClaims;
|
package/dist/adapters/apollo.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
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":";;;;
|
|
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":"
|
|
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}"]}
|
package/dist/adapters/express.js
CHANGED
|
@@ -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":";;;
|
|
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
|
-
|
|
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":"
|
|
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-
|
|
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-
|
|
2
|
+
import { T as TokenClaims, C as CaronteClient } from '../client-CSokWFYn.js';
|
|
3
3
|
|
|
4
4
|
interface CarontePluginOptions {
|
|
5
5
|
client: CaronteClient;
|
package/dist/adapters/fastify.js
CHANGED
|
@@ -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
|
-
|
|
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":"
|
|
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}"]}
|