@classytic/arc 2.20.0 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseController-D-MHZ27_.mjs → BaseController-C6FlRWxF.mjs} +2 -2
- package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
- package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
- package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
- package/dist/audit/index.d.mts +9 -5
- package/dist/auth/audit.d.mts +7 -4
- package/dist/auth/audit.mjs +5 -0
- package/dist/auth/index.d.mts +35 -14
- package/dist/auth/index.mjs +6 -3
- package/dist/auth/redis-session.d.mts +1 -2
- package/dist/cache/index.d.mts +2 -3
- package/dist/cli/commands/describe.d.mts +1 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/context/index.d.mts +2 -2
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-CWhyGkiM.mjs → core-b6fgFgnd.mjs} +129 -17
- package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-h1cEEaEX.mjs} +10 -6
- package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
- package/dist/{createApp-D6uwzCKu.mjs → createApp-Ci7yG5gv.mjs} +390 -192
- package/dist/discovery/index.d.mts +0 -1
- package/dist/docs/index.d.mts +1 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
- package/dist/encryption/index.d.mts +2 -144
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
- package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -1
- package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
- package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
- package/dist/events/index.d.mts +9 -6
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -2
- package/dist/factory/index.d.mts +30 -9
- package/dist/factory/index.mjs +35 -4
- package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -3
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +4 -3
- package/dist/idempotency/index.mjs +2 -0
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{types-BHMKlMBU.d.mts → index-BEu4wjbL.d.mts} +241 -80
- package/dist/{index-DUa8WHz3.d.mts → index-D8MKYGDa.d.mts} +88 -7
- package/dist/{index-_--GyzcO.d.mts → index-DbtgCOCj.d.mts} +1324 -1228
- package/dist/{index-CdFkIMEd.d.mts → index-ogIHvOjo.d.mts} +36 -7
- package/dist/index.d.mts +8 -7
- package/dist/index.mjs +6 -6
- package/dist/{init-YH_nslQR.mjs → init-DleK3p9f.mjs} +272 -139
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.d.mts +0 -1
- package/dist/integrations/mcp/index.d.mts +6 -7
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -2
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +2 -2
- package/dist/integrations/webhooks.d.mts +8 -5
- package/dist/integrations/webhooks.mjs +9 -5
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
- package/dist/integrations/websocket-redis.d.mts +1 -2
- package/dist/integrations/websocket.d.mts +2 -2
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-ByA2tf6D.mjs} +8 -4
- package/dist/middleware/index.d.mts +1 -2
- package/dist/{module-JYH9V0I8.d.mts → module-B4u10iOJ.d.mts} +71 -1
- package/dist/{openapi-Dq6RbEGK.mjs → openapi-BRpMZD7C.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
- package/dist/pipeline/index.d.mts +1 -2
- package/dist/plugins/index.d.mts +96 -7
- package/dist/plugins/index.mjs +86 -268
- package/dist/plugins/response-cache.d.mts +5 -3
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +4 -5
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -2
- package/dist/presets/search.d.mts +2 -3
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
- package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
- package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
- package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
- package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-xHc42fwB.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
- package/dist/schemas/index.d.mts +0 -1
- package/dist/scim/index.d.mts +0 -1
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.d.mts +2 -3
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-k15RXfcq.mjs +64 -0
- package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
- package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
- package/dist/testing/index.d.mts +83 -4
- package/dist/testing/index.mjs +74 -4
- package/dist/testing/storageContract.d.mts +1 -2
- package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C9b2HR5W.d.mts → types-Bc42CUyk.d.mts} +1 -2
- package/dist/types-Cc91nXuA.d.mts +132 -0
- package/dist/{fields-C7ahdGYf.d.mts → types-Csxga7d3.d.mts} +2 -123
- package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +220 -144
- package/dist/utils/index.mjs +3 -3
- package/dist/{utils-ChjYQpjn.mjs → utils-FhJ88TJE.mjs} +129 -39
- package/dist/{versioning-D_eBCfNr.d.mts → versioning-DLwklPlG.d.mts} +1 -7
- package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
- package/llms.txt +39 -0
- package/package.json +453 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +44 -2
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +243 -0
- package/skills/arc-module-publishing/references/authoring-modules.md +327 -0
- package/skills/arc-module-publishing/references/package-manifest.md +157 -0
- package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { b as isElevated, n as PUBLIC_SCOPE } from "./types-bHblZ9YN.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { E as requireRoles, b as allowPublic, w as requireAuth } from "./permissions-BJMuKMl7.mjs";
|
|
3
3
|
import { multiTenantPreset } from "./presets/multiTenant.mjs";
|
|
4
4
|
//#region src/presets/ownedByUser.ts
|
|
5
5
|
/**
|
|
@@ -244,8 +244,10 @@ interface PushRefStore {
|
|
|
244
244
|
* pre-compute it without re-introducing the very race we're closing.
|
|
245
245
|
*/
|
|
246
246
|
stageIfInactive(args: {
|
|
247
|
-
pushRef: string;
|
|
248
|
-
|
|
247
|
+
pushRef: string;
|
|
248
|
+
/** Raw payload (NOT pre-wrapped). The store assigns seq + wraps atomically. */
|
|
249
|
+
payload: unknown;
|
|
250
|
+
/** Server timestamp (ms) — passed in so tests can pin the clock. */
|
|
249
251
|
timestamp: number;
|
|
250
252
|
expiresAt: number;
|
|
251
253
|
}): Promise<"ok" | "missing" | "active" | "raw">;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { r as CacheStore } from "./interface-C1_WLTMK.mjs";
|
|
2
|
-
import { i as QueryCache } from "./QueryCache-
|
|
2
|
+
import { i as QueryCache } from "./QueryCache-Z1B38koh.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
|
-
|
|
5
4
|
//#region src/cache/queryCachePlugin.d.ts
|
|
6
5
|
interface QueryCachePluginOptions {
|
|
7
6
|
/** CacheStore instance. Default: MemoryCacheStore with default options. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/scope/rateLimitKey.ts
|
|
2
|
+
function createTenantKeyGenerator(opts) {
|
|
3
|
+
if (opts?.strategy) return opts.strategy;
|
|
4
|
+
return (ctx) => {
|
|
5
|
+
const scope = ctx.scope;
|
|
6
|
+
if (!scope || scope.kind === "public") return ctx.ip;
|
|
7
|
+
if (scope.kind === "member") return scope.organizationId;
|
|
8
|
+
if (scope.kind === "service") return scope.organizationId;
|
|
9
|
+
if (scope.kind === "elevated") return scope.organizationId ?? scope.userId ?? ctx.ip;
|
|
10
|
+
return scope.userId ?? ctx.ip;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { createTenantKeyGenerator as t };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-
|
|
2
|
-
|
|
1
|
+
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-QmY7A258.mjs";
|
|
3
2
|
//#region src/events/transports/redis-stream.d.ts
|
|
4
3
|
interface RedisStreamLike {
|
|
5
4
|
xadd(key: string, id: string, ...fieldValues: string[]): Promise<string | null>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { f as createError } from "./errors-DkO8Nk6-.mjs";
|
|
2
2
|
import { n as requestContext } from "./requestContext-BZKOr13H.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { N as evaluateAndApplyPermission } from "./permissions-BJMuKMl7.mjs";
|
|
4
4
|
//#region src/core/middlewares/arcDecorator.ts
|
|
5
5
|
/**
|
|
6
6
|
* Build the `arcDecorator` preHandler for a resource.
|
|
@@ -81,24 +81,44 @@ function buildAuthMiddlewareForPermissions(fastify, permissions) {
|
|
|
81
81
|
//#endregion
|
|
82
82
|
//#region src/core/middlewares/chain.ts
|
|
83
83
|
/**
|
|
84
|
-
* Compose
|
|
85
|
-
*
|
|
86
|
-
* ingredients differently (regression risk:
|
|
87
|
-
* cache keys leak across users
|
|
84
|
+
* Compose the route-level `onRequest` + `preHandler` arrays in the canonical
|
|
85
|
+
* order. Every null/undefined entry is dropped. Keeps the three routers from
|
|
86
|
+
* accidentally ordering the same ingredients differently (regression risk:
|
|
87
|
+
* cache before auth → user-scoped cache keys leak across users; auth after
|
|
88
|
+
* body parse → anonymous schema probing).
|
|
88
89
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
90
|
+
* onRequest constraints (Fastify lifecycle): `request.body` is undefined —
|
|
91
|
+
* everything placed there must be header/cookie/param/query-driven. arc's
|
|
92
|
+
* `authenticate` / `optionalAuthenticate` qualify by contract (Bearer
|
|
93
|
+
* header, session cookie, api-key header); `preAuth` handlers exist
|
|
94
|
+
* precisely to promote query tokens into headers (SSE `?token=`); the arc
|
|
95
|
+
* decorator stamps static route metadata.
|
|
91
96
|
*/
|
|
92
|
-
function
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
function buildRouteHooks(parts) {
|
|
98
|
+
return {
|
|
99
|
+
onRequest: [
|
|
100
|
+
...parts.preAuth ?? [],
|
|
101
|
+
parts.arcDecorator,
|
|
102
|
+
parts.authMw ?? null
|
|
103
|
+
].filter(Boolean),
|
|
104
|
+
preHandler: [
|
|
105
|
+
parts.permissionMw ?? null,
|
|
106
|
+
parts.pluginMw ?? null,
|
|
107
|
+
...parts.routeGuards ?? [],
|
|
108
|
+
...parts.customMws ?? []
|
|
109
|
+
].filter(Boolean)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Spread helper — turns `RouteHookChains` into the two Fastify route-option
|
|
114
|
+
* fields, omitting empty arrays so routes without middleware stay
|
|
115
|
+
* option-free (Fastify skips absent hooks faster than empty arrays).
|
|
116
|
+
*/
|
|
117
|
+
function routeHookOptions(hooks) {
|
|
118
|
+
return {
|
|
119
|
+
...hooks.onRequest.length > 0 ? { onRequest: hooks.onRequest } : {},
|
|
120
|
+
...hooks.preHandler.length > 0 ? { preHandler: hooks.preHandler } : {}
|
|
121
|
+
};
|
|
102
122
|
}
|
|
103
123
|
/**
|
|
104
124
|
* `RouteDefinition.preHandler` accepts two shapes:
|
|
@@ -329,4 +349,4 @@ function tryRegisterRoute(fastify, opts, context) {
|
|
|
329
349
|
}
|
|
330
350
|
}
|
|
331
351
|
//#endregion
|
|
332
|
-
export { selectPluginMw as a,
|
|
352
|
+
export { selectPluginMw as a, buildRouteHooks as c, buildAuthMiddleware as d, buildAuthMiddlewareForPermissions as f, resolveRouterPluginMw as i, resolveRoutePreHandlers as l, buildRateLimitConfig as n, buildActionPermissionMw as o, buildArcDecorator as p, buildRouteConfig as r, buildCrudPermissionMw as s, tryRegisterRoute as t, routeHookOptions as u };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { K as ResolvedTenantPurge, R as RegisterOptions, V as ResourceDefinition, X as CrudRouteKey, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-DbtgCOCj.mjs";
|
|
2
|
+
import "../fastify-augmentation-9_fuetB4.mjs";
|
|
2
3
|
import { FastifyPluginAsync } from "fastify";
|
|
3
4
|
import { TenantPurgeProgress, TenantPurgeStrategy } from "@classytic/repo-core/repository";
|
|
4
|
-
|
|
5
5
|
//#region src/registry/assertNoTenantData.d.ts
|
|
6
6
|
interface AssertNoTenantDataOptions {
|
|
7
7
|
readonly organizationId: string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
//#region src/plugins/requestId.ts
|
|
4
|
+
/**
|
|
5
|
+
* Request ID Plugin
|
|
6
|
+
*
|
|
7
|
+
* Propagates request IDs for distributed tracing.
|
|
8
|
+
* - Accepts incoming x-request-id header (sanitized)
|
|
9
|
+
* - Generates UUID if not provided
|
|
10
|
+
* - Echoes the id on the response header + W3C trace-context propagation
|
|
11
|
+
*
|
|
12
|
+
* ID RESOLUTION LIVES AT THE SERVER LEVEL, NOT HERE. Fastify binds
|
|
13
|
+
* `request.log = logger.child({ reqId: request.id })` at request
|
|
14
|
+
* construction — before any hook runs — so an id assigned inside a hook
|
|
15
|
+
* never reaches the logs. `createApp` therefore wires
|
|
16
|
+
* {@link createRequestIdGenerator} into Fastify's `genReqId` option, and
|
|
17
|
+
* this plugin simply adopts `request.id`. Standalone registration on a
|
|
18
|
+
* bare Fastify instance falls back to the legacy in-hook resolution
|
|
19
|
+
* (header echo / UUID), with the documented limitation that
|
|
20
|
+
* `request.log`'s `reqId` binding predates the overwrite — pass
|
|
21
|
+
* `genReqId: createRequestIdGenerator()` to `Fastify()` to fix that.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* import { requestIdPlugin, createRequestIdGenerator } from '@classytic/arc';
|
|
25
|
+
*
|
|
26
|
+
* const fastify = Fastify({ genReqId: createRequestIdGenerator() });
|
|
27
|
+
* await fastify.register(requestIdPlugin);
|
|
28
|
+
*
|
|
29
|
+
* fastify.get('/', async (request) => {
|
|
30
|
+
* request.log.info('traced'); // carries the same reqId as the header
|
|
31
|
+
* return { id: request.id };
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Sanitize an incoming request-id header value: max 128 chars, alphanumeric
|
|
36
|
+
* plus dashes/underscores/dots/colons only. Returns `undefined` for anything
|
|
37
|
+
* else (crafted values could pollute logs or response headers). Array values
|
|
38
|
+
* (repeated headers) are rejected outright — a client sending multiple
|
|
39
|
+
* request-id headers is not tracing in good faith.
|
|
40
|
+
*/
|
|
41
|
+
function sanitizeRequestId(value) {
|
|
42
|
+
if (typeof value !== "string") return void 0;
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed.length > 0 && trimmed.length <= 128 && /^[\w.:-]+$/.test(trimmed) ? trimmed : void 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a Fastify `genReqId` function: adopt a sanitized incoming request-id
|
|
48
|
+
* header, or generate one. Wire this into the `Fastify()` constructor so
|
|
49
|
+
* `request.id`, `request.log`'s `reqId` binding, and the echoed response
|
|
50
|
+
* header all agree — `createApp` does this automatically.
|
|
51
|
+
*/
|
|
52
|
+
function createRequestIdGenerator(opts = {}) {
|
|
53
|
+
const { header = "x-request-id", generator = randomUUID } = opts;
|
|
54
|
+
const headerName = header.toLowerCase();
|
|
55
|
+
return (req) => sanitizeRequestId(req.headers[headerName]) ?? generator();
|
|
56
|
+
}
|
|
57
|
+
/** Fastify's default request ids are `req-<base36 counter>`. */
|
|
58
|
+
const FASTIFY_DEFAULT_ID = /^req-[0-9a-z]+$/;
|
|
59
|
+
/** W3C traceparent format: `version-traceid-parentid-flags` */
|
|
60
|
+
const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
|
|
61
|
+
/** W3C spec: version ff is reserved and invalid; trace/parent IDs must not be all zeros */
|
|
62
|
+
const TRACEID_ZEROS = "0".repeat(32);
|
|
63
|
+
const PARENTID_ZEROS = "0".repeat(16);
|
|
64
|
+
/** tracestate: printable ASCII, <= 512 bytes */
|
|
65
|
+
const TRACESTATE_MAX = 512;
|
|
66
|
+
function isValidTraceparent(tp) {
|
|
67
|
+
const m = TRACEPARENT_RE.exec(tp);
|
|
68
|
+
if (!m) return false;
|
|
69
|
+
const version = m[1] ?? "";
|
|
70
|
+
const traceId = m[2] ?? "";
|
|
71
|
+
const parentId = m[3] ?? "";
|
|
72
|
+
if (version.toLowerCase() === "ff") return false;
|
|
73
|
+
if (traceId.toLowerCase() === TRACEID_ZEROS) return false;
|
|
74
|
+
if (parentId.toLowerCase() === PARENTID_ZEROS) return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const requestIdPlugin = async (fastify, opts = {}) => {
|
|
78
|
+
const { header = "x-request-id", generator = randomUUID, setResponseHeader = true, propagateTraceContext = true } = opts;
|
|
79
|
+
if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
|
|
80
|
+
if (!fastify.hasRequestDecorator("traceContext")) fastify.decorateRequest("traceContext", void 0);
|
|
81
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
82
|
+
const incoming = sanitizeRequestId(request.headers[header]);
|
|
83
|
+
let requestId = String(request.id);
|
|
84
|
+
if (incoming && incoming !== requestId) requestId = incoming;
|
|
85
|
+
else if (!incoming && FASTIFY_DEFAULT_ID.test(requestId)) requestId = generator();
|
|
86
|
+
if (requestId !== request.id) request.id = requestId;
|
|
87
|
+
request.requestId = requestId;
|
|
88
|
+
if (setResponseHeader) reply.header(header, requestId);
|
|
89
|
+
if (propagateTraceContext) {
|
|
90
|
+
const rawTraceparent = request.headers["traceparent"];
|
|
91
|
+
const rawTracestate = request.headers["tracestate"];
|
|
92
|
+
if (typeof rawTraceparent === "string") {
|
|
93
|
+
const tp = rawTraceparent.trim();
|
|
94
|
+
if (isValidTraceparent(tp)) {
|
|
95
|
+
const ts = typeof rawTracestate === "string" && rawTracestate.length > 0 && rawTracestate.length <= TRACESTATE_MAX ? rawTracestate : void 0;
|
|
96
|
+
request.traceContext = {
|
|
97
|
+
traceparent: tp,
|
|
98
|
+
...ts ? { tracestate: ts } : {}
|
|
99
|
+
};
|
|
100
|
+
reply.header("traceparent", tp);
|
|
101
|
+
if (ts) reply.header("tracestate", ts);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
fastify.log?.debug?.("Request ID plugin registered");
|
|
107
|
+
};
|
|
108
|
+
var requestId_default = fp(requestIdPlugin, {
|
|
109
|
+
name: "arc-request-id",
|
|
110
|
+
fastify: "5.x"
|
|
111
|
+
});
|
|
112
|
+
//#endregion
|
|
113
|
+
export { sanitizeRequestId as i, requestIdPlugin as n, requestId_default as r, createRequestIdGenerator as t };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { arcLog } from "./logger/index.mjs";
|
|
2
|
+
//#region src/utils/schemaConverter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Schema Converter — Detect-First, Convert-Only-When-Needed
|
|
5
|
+
*
|
|
6
|
+
* Converts Zod v4 schemas to JSON Schema using Zod's native `z.toJSONSchema()`.
|
|
7
|
+
* Plain JSON Schema objects pass through with zero overhead.
|
|
8
|
+
*
|
|
9
|
+
* Zod is an **optional** peer dependency — loaded lazily at module init;
|
|
10
|
+
* `ensureSchemaConverter()` awaits readiness (arc's resource plugin does this
|
|
11
|
+
* before converting). When a Zod schema shows up and can't be converted
|
|
12
|
+
* (zod missing, zod v3, conversion throw), VALIDATION paths fail fast at
|
|
13
|
+
* boot with an actionable error; DOC paths warn and degrade — see
|
|
14
|
+
* {@link UnconvertibleMode}. Hosts that never pass Zod pay nothing.
|
|
15
|
+
*
|
|
16
|
+
* ## Targets
|
|
17
|
+
*
|
|
18
|
+
* Zod v4's `toJSONSchema` supports multiple output targets; arc picks per consumer:
|
|
19
|
+
*
|
|
20
|
+
* - **`draft-7`** (default) — for Fastify route schemas. Fastify v5 bundles AJV 8
|
|
21
|
+
* configured for draft-07, which uses **numeric** `exclusiveMinimum`/`exclusiveMaximum`.
|
|
22
|
+
* The `openapi-3.0` target emits the **boolean** form inherited from draft-04
|
|
23
|
+
* (`exclusiveMinimum: true` alongside `minimum`), which AJV rejects at route
|
|
24
|
+
* registration with `schema is invalid: data/properties/X/exclusiveMinimum must be number`.
|
|
25
|
+
* Using `draft-7` fixes `.positive() / .negative() / .gt() / .lt()` out of the box.
|
|
26
|
+
* - **`openapi-3.0`** — for OpenAPI doc generation (arc emits OpenAPI 3.0.3). Keeps
|
|
27
|
+
* the boolean exclusive form that 3.0 tooling expects.
|
|
28
|
+
*/
|
|
29
|
+
const log = arcLog("schema");
|
|
30
|
+
/** Default target for Fastify-consumed schemas (matches Fastify v5's default AJV draft). */
|
|
31
|
+
const DEFAULT_FASTIFY_TARGET = "draft-7";
|
|
32
|
+
/** Default target for OpenAPI document generation (matches arc's emitted OpenAPI version). */
|
|
33
|
+
const DEFAULT_OPENAPI_TARGET = "openapi-3.0";
|
|
34
|
+
let _toJSONSchema = null;
|
|
35
|
+
const zodReady = import("zod").then(({ z }) => {
|
|
36
|
+
if (typeof z?.toJSONSchema === "function") _toJSONSchema = (schema, opts) => z.toJSONSchema(schema, opts);
|
|
37
|
+
}).catch(() => {});
|
|
38
|
+
/**
|
|
39
|
+
* Await Zod-converter readiness. Arc's resource plugin awaits this once
|
|
40
|
+
* before any schema conversion, closing the (theoretical) race between the
|
|
41
|
+
* lazy `import("zod")` above and boot-time route registration. Resolves
|
|
42
|
+
* immediately when zod isn't installed — absence is handled per-schema.
|
|
43
|
+
*/
|
|
44
|
+
function ensureSchemaConverter() {
|
|
45
|
+
return zodReady;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if an object is already a plain JSON Schema.
|
|
49
|
+
* Returns true if it has JSON Schema markers (`type`, `properties`, `$ref`,
|
|
50
|
+
* `allOf`, `anyOf`, `oneOf`, `items`, `enum`) and does NOT have Zod markers.
|
|
51
|
+
*/
|
|
52
|
+
function isJsonSchema(input) {
|
|
53
|
+
if (input === null || typeof input !== "object") return false;
|
|
54
|
+
const obj = input;
|
|
55
|
+
if ("_def" in obj || "_zod" in obj) return false;
|
|
56
|
+
return "type" in obj || "properties" in obj || "$ref" in obj || "allOf" in obj || "anyOf" in obj || "oneOf" in obj || "items" in obj || "enum" in obj;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if an object is a Zod schema (has `_zod` marker from Zod v4).
|
|
60
|
+
*/
|
|
61
|
+
function isZodSchema(input) {
|
|
62
|
+
return input !== null && typeof input === "object" && "_zod" in input;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Zod v3 schemas carry `_def` but NOT v4's `_zod` marker. They can't be
|
|
66
|
+
* converted (`z.toJSONSchema()` is v4-only) — detect them so the failure is
|
|
67
|
+
* an actionable "upgrade to zod v4" instead of a cryptic AJV boot error
|
|
68
|
+
* three layers away.
|
|
69
|
+
*/
|
|
70
|
+
function isZodV3Schema(input) {
|
|
71
|
+
if (input === null || typeof input !== "object") return false;
|
|
72
|
+
const obj = input;
|
|
73
|
+
return "_def" in obj && !("_zod" in obj);
|
|
74
|
+
}
|
|
75
|
+
function unconvertible(mode, message, cause) {
|
|
76
|
+
if (mode === "throw") throw new Error(`[arc/schema] ${message}`, cause ? { cause } : void 0);
|
|
77
|
+
log.warn(`${message} — degrading to permissive { type: 'object' } (validation disabled for this slot)`);
|
|
78
|
+
return { type: "object" };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Convert any schema input to JSON Schema.
|
|
82
|
+
*
|
|
83
|
+
* Detection order:
|
|
84
|
+
* 1. `null`/`undefined` → `undefined`
|
|
85
|
+
* 2. Already JSON Schema → pass through as-is (zero overhead)
|
|
86
|
+
* 3. Zod v4 schema → `z.toJSONSchema(schema, { target })`
|
|
87
|
+
* 4. Zod v3 schema → unconvertible (actionable "upgrade to v4" error/warn)
|
|
88
|
+
* 5. Unrecognized object → return as-is (treat as opaque schema)
|
|
89
|
+
*
|
|
90
|
+
* @param input Schema (Zod, plain JSON Schema, or opaque object)
|
|
91
|
+
* @param target Output target — defaults to `draft-7` for Fastify compatibility.
|
|
92
|
+
* Pass `openapi-3.0`/`openapi-3.1` for OpenAPI document generation.
|
|
93
|
+
* @param mode Failure policy for unconvertible Zod schemas — see
|
|
94
|
+
* {@link UnconvertibleMode}. Defaults to `'warn'`.
|
|
95
|
+
*/
|
|
96
|
+
function toJsonSchema(input, target = DEFAULT_FASTIFY_TARGET, mode = "warn") {
|
|
97
|
+
if (input == null) return void 0;
|
|
98
|
+
if (typeof input !== "object") return void 0;
|
|
99
|
+
if (isJsonSchema(input)) return input;
|
|
100
|
+
if (isZodSchema(input)) {
|
|
101
|
+
if (!_toJSONSchema) return unconvertible(mode, "A Zod schema was passed but zod v4 is not installed (or not yet loaded). Install zod >=4 — arc converts via zod's native z.toJSONSchema().");
|
|
102
|
+
try {
|
|
103
|
+
const converted = _toJSONSchema(input, { target });
|
|
104
|
+
if ("$schema" in converted) delete converted.$schema;
|
|
105
|
+
return converted;
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
return unconvertible(mode, `z.toJSONSchema() failed for a Zod schema (target: ${target}): ${cause instanceof Error ? cause.message : String(cause)}. Zod-only features with no JSON Schema equivalent (.refine/.transform/custom checks) cannot express wire validation — enforce those in the handler or a pipeline step.`, cause);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (isZodV3Schema(input)) return unconvertible(mode, "A Zod v3 schema was passed (has `_def` but no v4 `_zod` marker). arc requires zod >=4 for schema conversion — upgrade zod, or pass plain JSON Schema.");
|
|
111
|
+
return input;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Convert all schema fields in an OpenApiSchemas object.
|
|
115
|
+
* JSON Schema values pass through unchanged. Only Zod schemas are converted.
|
|
116
|
+
*
|
|
117
|
+
* Defaults to the `openapi-3.0` target since this function feeds OpenAPI doc
|
|
118
|
+
* generation, not Fastify route validation.
|
|
119
|
+
*/
|
|
120
|
+
function convertOpenApiSchemas(schemas, target = DEFAULT_OPENAPI_TARGET) {
|
|
121
|
+
const result = {};
|
|
122
|
+
const schemaFields = [
|
|
123
|
+
"entity",
|
|
124
|
+
"createBody",
|
|
125
|
+
"updateBody",
|
|
126
|
+
"params",
|
|
127
|
+
"listQuery",
|
|
128
|
+
"response"
|
|
129
|
+
];
|
|
130
|
+
for (const field of schemaFields) {
|
|
131
|
+
const value = schemas[field];
|
|
132
|
+
if (value !== void 0) result[field] = toJsonSchema(value, target) ?? value;
|
|
133
|
+
}
|
|
134
|
+
for (const [key, value] of Object.entries(schemas)) if (!schemaFields.includes(key)) result[key] = value;
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Convert schema values in a Fastify route schema record.
|
|
139
|
+
*
|
|
140
|
+
* Handles `body`, `querystring`, `params`, `headers` (top-level conversion)
|
|
141
|
+
* and `response` (iterates by status code — each value converted individually).
|
|
142
|
+
*
|
|
143
|
+
* JSON Schema values pass through unchanged. Only Zod schemas are converted.
|
|
144
|
+
*
|
|
145
|
+
* Used for both custom routes and customSchemas (CRUD overrides).
|
|
146
|
+
*
|
|
147
|
+
* Defaults to `draft-7` so Fastify v5's bundled AJV 8 accepts the output.
|
|
148
|
+
* Pass `openapi-3.0` (or `openapi-3.1`) when generating OpenAPI documents.
|
|
149
|
+
*/
|
|
150
|
+
function convertRouteSchema(schema, target = DEFAULT_FASTIFY_TARGET) {
|
|
151
|
+
const result = { ...schema };
|
|
152
|
+
const mode = target === DEFAULT_FASTIFY_TARGET ? "throw" : "warn";
|
|
153
|
+
for (const field of [
|
|
154
|
+
"body",
|
|
155
|
+
"querystring",
|
|
156
|
+
"params",
|
|
157
|
+
"headers"
|
|
158
|
+
]) if (result[field] !== void 0) result[field] = toJsonSchema(result[field], target, mode) ?? result[field];
|
|
159
|
+
if (result.response !== void 0 && typeof result.response === "object" && result.response !== null) {
|
|
160
|
+
const responseObj = result.response;
|
|
161
|
+
const convertedResponse = {};
|
|
162
|
+
for (const [statusCode, responseSchema] of Object.entries(responseObj)) convertedResponse[statusCode] = toJsonSchema(responseSchema, target, mode) ?? responseSchema;
|
|
163
|
+
result.response = convertedResponse;
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
export { isZodSchema as a, isJsonSchema as i, convertRouteSchema as n, toJsonSchema as o, ensureSchemaConverter as r, convertOpenApiSchemas as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as toJsonSchema } from "./schemaConverter-Ds5reSGs.mjs";
|
|
2
2
|
//#region src/core/schemaIR.ts
|
|
3
3
|
/**
|
|
4
4
|
* Schema IR — one canonical representation, two adapters.
|
|
@@ -49,7 +49,7 @@ function normalizeSchemaIR(raw) {
|
|
|
49
49
|
properties: {},
|
|
50
50
|
required: []
|
|
51
51
|
};
|
|
52
|
-
const converted = toJsonSchema(raw);
|
|
52
|
+
const converted = toJsonSchema(raw, "draft-7", "throw");
|
|
53
53
|
if (!converted || typeof converted !== "object" || converted.type !== "object" && !("properties" in converted)) return {
|
|
54
54
|
properties: {},
|
|
55
55
|
required: []
|
package/dist/schemas/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ErrorContract, ErrorDetail } from "@classytic/repo-core/errors";
|
|
2
2
|
import { FastifyPluginAsyncTypebox, FastifyPluginCallbackTypebox, TypeBoxTypeProvider, TypeBoxValidatorCompiler } from "@fastify/type-provider-typebox";
|
|
3
3
|
import { Static, TObject, TSchema, TSchema as TSchema$1, Type } from "@sinclair/typebox";
|
|
4
|
-
|
|
5
4
|
//#region src/schemas/index.d.ts
|
|
6
5
|
/**
|
|
7
6
|
* Paginated list response — full union of every wire shape `toCanonicalList`
|
package/dist/scim/index.d.mts
CHANGED
package/dist/scim/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as parseJsonBody } from "../jsonBody-BlzAoGCe.mjs";
|
|
1
2
|
import fp from "fastify-plugin";
|
|
2
3
|
//#region src/scim/errors.ts
|
|
3
4
|
var ScimError = class extends Error {
|
|
@@ -93,18 +94,16 @@ function asRecord(doc) {
|
|
|
93
94
|
/**
|
|
94
95
|
* Register the `application/scim+json` Fastify content-type parser. Idempotent
|
|
95
96
|
* — second registration in the same scope is a no-op. Empty bodies (DELETE,
|
|
96
|
-
* GET) yield `undefined` rather than crashing on `JSON.parse("")`.
|
|
97
|
+
* GET) yield `undefined` rather than crashing on `JSON.parse("")`. Parsing
|
|
98
|
+
* flows through `parseJsonBody`, so malformed SCIM payloads surface as 400
|
|
99
|
+
* `arc.bad_request` (not 500) and prototype-poisoning bodies are rejected —
|
|
100
|
+
* same contract as arc's main `application/json` parser.
|
|
97
101
|
*/
|
|
98
102
|
function ensureScimContentTypeParser(fastify) {
|
|
99
103
|
if (fastify.hasContentTypeParser("application/scim+json")) return;
|
|
100
104
|
fastify.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_req, body, done) => {
|
|
101
|
-
const raw = body;
|
|
102
|
-
if (!raw || raw.length === 0) {
|
|
103
|
-
done(null, void 0);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
105
|
try {
|
|
107
|
-
done(null, JSON
|
|
106
|
+
done(null, parseJsonBody(body, "Invalid SCIM JSON payload"));
|
|
108
107
|
} catch (err) {
|
|
109
108
|
done(err, void 0);
|
|
110
109
|
}
|
package/dist/scope/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { C as isMember, D as requireOrgId, E as requireClientId, O as requireTeamId, S as isElevated, T as isService, _ as getTenantFromRequest, a as getAncestorOrgIds, b as hasOrgAccess, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, k as requireUserId, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserId, w as isOrgInScope, x as isAuthenticated, y as getUserRoles } from "../types-
|
|
2
|
-
import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-
|
|
1
|
+
import { C as isMember, D as requireOrgId, E as requireClientId, O as requireTeamId, S as isElevated, T as isService, _ as getTenantFromRequest, a as getAncestorOrgIds, b as hasOrgAccess, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, k as requireUserId, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserId, w as isOrgInScope, x as isAuthenticated, y as getUserRoles } from "../types-DH0GupI7.mjs";
|
|
2
|
+
import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-C9s5hFP_.mjs";
|
|
3
3
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
4
|
-
|
|
5
4
|
//#region src/scope/rateLimitKey.d.ts
|
|
6
5
|
interface RateLimitKeyContext {
|
|
7
6
|
ip: string;
|
package/dist/scope/index.mjs
CHANGED
|
@@ -1,79 +1,5 @@
|
|
|
1
1
|
import { C as isService, D as requireUserId, E as requireTeamId, S as isOrgInScope, T as requireOrgId, _ as getUserRoles, a as getDPoPJkt, b as isElevated, c as getOrgId, d as getScopeContext, f as getScopeContextMap, g as getUserId, h as getTenantFromRequest, i as getClientId, l as getOrgRoles, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, r as getAncestorOrgIds, s as getOrgContext, t as AUTHENTICATED_SCOPE, u as getRequestScope, v as hasOrgAccess, w as requireClientId, x as isMember, y as isAuthenticated } from "../types-bHblZ9YN.mjs";
|
|
2
|
-
import { n as normalizeRoles } from "../types-XTApa8b6.mjs";
|
|
3
2
|
import { n as elevation_default, t as elevationPlugin } from "../elevation-COpYKt2h.mjs";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (opts?.strategy) return opts.strategy;
|
|
7
|
-
return (ctx) => {
|
|
8
|
-
const scope = ctx.scope;
|
|
9
|
-
if (!scope || scope.kind === "public") return ctx.ip;
|
|
10
|
-
if (scope.kind === "member") return scope.organizationId;
|
|
11
|
-
if (scope.kind === "service") return scope.organizationId;
|
|
12
|
-
if (scope.kind === "elevated") return scope.organizationId ?? scope.userId ?? ctx.ip;
|
|
13
|
-
return scope.userId ?? ctx.ip;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region src/scope/resolveOrgFromHeader.ts
|
|
18
|
-
/**
|
|
19
|
-
* Create a preHandler hook that resolves org scope from a header.
|
|
20
|
-
* Must run AFTER authentication so `request.user` is populated.
|
|
21
|
-
* If the header is present and user is a member, sets `request.scope` to `member`.
|
|
22
|
-
* If the header is absent, scope stays as-is (typically `authenticated`).
|
|
23
|
-
*/
|
|
24
|
-
function resolveOrgFromHeader(options) {
|
|
25
|
-
const { header = "x-organization-id", resolveMembership } = options;
|
|
26
|
-
return async (request, reply) => {
|
|
27
|
-
const orgId = request.headers[header];
|
|
28
|
-
if (!orgId) return;
|
|
29
|
-
const scope = request.scope;
|
|
30
|
-
if (!scope || scope.kind === "public") {
|
|
31
|
-
reply.code(401).send({
|
|
32
|
-
success: false,
|
|
33
|
-
error: "Unauthorized",
|
|
34
|
-
message: "Authentication required for organization access",
|
|
35
|
-
code: "ORG_AUTH_REQUIRED"
|
|
36
|
-
});
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
if (scope.kind === "elevated") return;
|
|
40
|
-
const user = request.user;
|
|
41
|
-
if (!user) {
|
|
42
|
-
reply.code(401).send({
|
|
43
|
-
success: false,
|
|
44
|
-
error: "Unauthorized",
|
|
45
|
-
message: "Authentication required for organization access",
|
|
46
|
-
code: "ORG_AUTH_REQUIRED"
|
|
47
|
-
});
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const userId = String(user.id ?? user._id ?? "");
|
|
51
|
-
if (!userId) {
|
|
52
|
-
reply.code(401).send({
|
|
53
|
-
success: false,
|
|
54
|
-
error: "Unauthorized",
|
|
55
|
-
message: "User identity required for organization access"
|
|
56
|
-
});
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const membership = await resolveMembership(userId, orgId);
|
|
60
|
-
if (!membership) {
|
|
61
|
-
reply.code(403).send({
|
|
62
|
-
success: false,
|
|
63
|
-
error: "Forbidden",
|
|
64
|
-
message: "Not a member of this organization",
|
|
65
|
-
code: "ORG_ACCESS_DENIED"
|
|
66
|
-
});
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
request.scope = {
|
|
70
|
-
kind: "member",
|
|
71
|
-
userId: userId || void 0,
|
|
72
|
-
userRoles: normalizeRoles(user.role),
|
|
73
|
-
organizationId: orgId,
|
|
74
|
-
orgRoles: membership.roles
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
//#endregion
|
|
3
|
+
import { t as createTenantKeyGenerator } from "../rateLimitKey-BoWRH3e7.mjs";
|
|
4
|
+
import { t as resolveOrgFromHeader } from "../scope-k15RXfcq.mjs";
|
|
79
5
|
export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getAncestorOrgIds, getClientId, getDPoPJkt, getMandate, getOrgContext, getOrgId, getOrgRoles, getRequestScope, getScopeContext, getScopeContextMap, getServiceScopes, getTeamId, getTenantFromRequest, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, isOrgInScope, isService, requireClientId, requireOrgId, requireTeamId, requireUserId, resolveOrgFromHeader };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { n as normalizeRoles } from "./types-XTApa8b6.mjs";
|
|
2
|
+
//#region src/scope/resolveOrgFromHeader.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create a preHandler hook that resolves org scope from a header.
|
|
5
|
+
* Must run AFTER authentication so `request.user` is populated.
|
|
6
|
+
* If the header is present and user is a member, sets `request.scope` to `member`.
|
|
7
|
+
* If the header is absent, scope stays as-is (typically `authenticated`).
|
|
8
|
+
*/
|
|
9
|
+
function resolveOrgFromHeader(options) {
|
|
10
|
+
const { header = "x-organization-id", resolveMembership } = options;
|
|
11
|
+
return async (request, reply) => {
|
|
12
|
+
const orgId = request.headers[header];
|
|
13
|
+
if (!orgId) return;
|
|
14
|
+
const scope = request.scope;
|
|
15
|
+
if (!scope || scope.kind === "public") {
|
|
16
|
+
reply.code(401).send({
|
|
17
|
+
success: false,
|
|
18
|
+
error: "Unauthorized",
|
|
19
|
+
message: "Authentication required for organization access",
|
|
20
|
+
code: "ORG_AUTH_REQUIRED"
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (scope.kind === "elevated") return;
|
|
25
|
+
const user = request.user;
|
|
26
|
+
if (!user) {
|
|
27
|
+
reply.code(401).send({
|
|
28
|
+
success: false,
|
|
29
|
+
error: "Unauthorized",
|
|
30
|
+
message: "Authentication required for organization access",
|
|
31
|
+
code: "ORG_AUTH_REQUIRED"
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const userId = String(user.id ?? user._id ?? "");
|
|
36
|
+
if (!userId) {
|
|
37
|
+
reply.code(401).send({
|
|
38
|
+
success: false,
|
|
39
|
+
error: "Unauthorized",
|
|
40
|
+
message: "User identity required for organization access"
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const membership = await resolveMembership(userId, orgId);
|
|
45
|
+
if (!membership) {
|
|
46
|
+
reply.code(403).send({
|
|
47
|
+
success: false,
|
|
48
|
+
error: "Forbidden",
|
|
49
|
+
message: "Not a member of this organization",
|
|
50
|
+
code: "ORG_ACCESS_DENIED"
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
request.scope = {
|
|
55
|
+
kind: "member",
|
|
56
|
+
userId: userId || void 0,
|
|
57
|
+
userRoles: normalizeRoles(user.role),
|
|
58
|
+
organizationId: orgId,
|
|
59
|
+
orgRoles: membership.roles
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { resolveOrgFromHeader as t };
|