@classytic/arc 1.1.0 → 2.1.2
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 +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import "../elevation-B_2dRLVP.mjs";
|
|
2
|
+
import { S as RouteHandler } from "../interface-Ch8HU9uM.mjs";
|
|
3
|
+
import { i as UserBase } from "../types-aYB4V7uN.mjs";
|
|
4
|
+
import "../types/index.mjs";
|
|
5
|
+
import { InvitationAdapter, InvitationDoc, MemberDoc, OrgAdapter, OrgDoc, OrgPermissionStatement, OrgRole, OrganizationPluginOptions } from "./types.mjs";
|
|
6
|
+
import { FastifyPluginAsync, RouteHandlerMethod } from "fastify";
|
|
7
|
+
|
|
8
|
+
//#region src/org/orgGuard.d.ts
|
|
9
|
+
interface OrgGuardOptions {
|
|
10
|
+
/** Require organization context (default: true) */
|
|
11
|
+
requireOrgContext?: boolean;
|
|
12
|
+
/** Required org-level roles */
|
|
13
|
+
roles?: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create org guard middleware.
|
|
17
|
+
* Reads `request.scope` for org context and roles.
|
|
18
|
+
* Elevated scope always passes.
|
|
19
|
+
*/
|
|
20
|
+
declare function orgGuard(options?: OrgGuardOptions): RouteHandler;
|
|
21
|
+
/**
|
|
22
|
+
* Shorthand for requiring org context
|
|
23
|
+
*/
|
|
24
|
+
declare function requireOrg(): RouteHandler;
|
|
25
|
+
/**
|
|
26
|
+
* Require org context with specific roles
|
|
27
|
+
*/
|
|
28
|
+
declare function requireOrgRole(...roles: string[]): RouteHandler;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/org/orgMembership.d.ts
|
|
31
|
+
interface OrgMembershipOptions {
|
|
32
|
+
/** Path to user's organizations array */
|
|
33
|
+
userOrgsPath?: string;
|
|
34
|
+
/** Optional DB lookup function */
|
|
35
|
+
validateFromDb?: (userId: string, orgId: string) => Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
interface OrgRolesOptions {
|
|
38
|
+
/** Path to user's organizations array */
|
|
39
|
+
userOrgsPath?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if user is member of organization.
|
|
43
|
+
* This is a low-level utility for checking membership from user object data.
|
|
44
|
+
* For request-level checks, use `request.scope` (isMember/isElevated guards).
|
|
45
|
+
*/
|
|
46
|
+
declare function orgMembershipCheck(user: UserBase | undefined | null, orgId: string | undefined | null, options?: OrgMembershipOptions): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Get user's role in organization from user object data.
|
|
49
|
+
* For request-level role checks, use `request.scope.orgRoles` (when scope is 'member').
|
|
50
|
+
*/
|
|
51
|
+
declare function getUserOrgRoles(user: UserBase | undefined | null, orgId: string | undefined | null, options?: OrgRolesOptions): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Check if user has specific role in organization from user object data.
|
|
54
|
+
* For request-level role checks, use `requireOrgRole()` permission or `request.scope`.
|
|
55
|
+
*/
|
|
56
|
+
declare function hasOrgRole(user: UserBase | undefined | null, orgId: string | undefined | null, roles: string | string[], options?: OrgRolesOptions): boolean;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/org/organizationPlugin.d.ts
|
|
59
|
+
declare module 'fastify' {
|
|
60
|
+
interface FastifyInstance {
|
|
61
|
+
/** Middleware: require the caller to hold one of the listed org roles */
|
|
62
|
+
requireOrgRole: (roles: string[]) => RouteHandlerMethod;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
declare const organizationPlugin: FastifyPluginAsync<OrganizationPluginOptions>;
|
|
66
|
+
declare const _default: FastifyPluginAsync<OrganizationPluginOptions>;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { type InvitationAdapter, type InvitationDoc, type MemberDoc, type OrgAdapter, type OrgDoc, type OrgGuardOptions, type OrgMembershipOptions, type OrgPermissionStatement, type OrgRole, type OrgRolesOptions, type OrganizationPluginOptions, getUserOrgRoles, hasOrgRole, orgGuard, orgMembershipCheck, _default as organizationPlugin, organizationPlugin as organizationPluginFn, requireOrg, requireOrgRole };
|
|
69
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/org/orgGuard.ts","../../src/org/orgMembership.ts","../../src/org/organizationPlugin.ts"],"mappings":";;;;;;;;UAsBiB,eAAA;EAYQ;EAVvB,iBAAA;EAUmE;EARnE,KAAA;AAAA;;;;;AAkEF;iBA1DgB,QAAA,CAAS,OAAA,GAAS,eAAA,GAAuB,YAAA;;;;iBAmDzC,UAAA,CAAA,GAAc,YAAA;;;AC7E9B;iBDoFgB,cAAA,CAAA,GAAkB,KAAA,aAAkB,YAAA;;;UCpFnC,oBAAA;;EAEf,YAAA;EDY8B;ECV9B,cAAA,IAAkB,MAAA,UAAgB,KAAA,aAAkB,OAAA;AAAA;AAAA,UAGrC,eAAA;EDmBD;ECjBd,YAAA;AAAA;;;;;;iBAQoB,kBAAA,CACpB,IAAA,EAAM,QAAA,qBACN,KAAA,6BACA,OAAA,GAAS,oBAAA,GACR,OAAA;ADwDH;;;;AAAA,iBCxBgB,eAAA,CACd,IAAA,EAAM,QAAA,qBACN,KAAA,6BACA,OAAA,GAAS,eAAA;AD4BX;;;;AAAA,iBCRgB,UAAA,CACd,IAAA,EAAM,QAAA,qBACN,KAAA,6BACA,KAAA,qBACA,OAAA,GAAS,eAAA;;;;YC3CC,eAAA;IF+CI;IE7CZ,cAAA,GAAiB,KAAA,eAAoB,kBAAA;EAAA;AAAA;AAAA,cA0DnC,kBAAA,EAAoB,kBAAA,CAAmB,yBAAA;AAAA,cAAyB,QAAA"}
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { c as isElevated, i as getOrgRoles, l as isMember, n as PUBLIC_SCOPE, o as hasOrgAccess } from "../types-Beqn1Un7.mjs";
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
|
|
4
|
+
//#region src/org/orgGuard.ts
|
|
5
|
+
/**
|
|
6
|
+
* Create org guard middleware.
|
|
7
|
+
* Reads `request.scope` for org context and roles.
|
|
8
|
+
* Elevated scope always passes.
|
|
9
|
+
*/
|
|
10
|
+
function orgGuard(options = {}) {
|
|
11
|
+
const { requireOrgContext = true, roles = [] } = options;
|
|
12
|
+
return async function orgGuardMiddleware(request, reply) {
|
|
13
|
+
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
14
|
+
if (isElevated(scope)) return;
|
|
15
|
+
if (requireOrgContext && !hasOrgAccess(scope)) {
|
|
16
|
+
reply.code(403).send({
|
|
17
|
+
success: false,
|
|
18
|
+
error: "Organization context required",
|
|
19
|
+
code: "ORG_CONTEXT_REQUIRED",
|
|
20
|
+
message: "This endpoint requires an organization context. Please specify organization via x-organization-id header."
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (roles.length > 0 && isMember(scope)) {
|
|
25
|
+
const userOrgRoles = getOrgRoles(scope);
|
|
26
|
+
if (!roles.some((role) => userOrgRoles.includes(role))) {
|
|
27
|
+
reply.code(403).send({
|
|
28
|
+
success: false,
|
|
29
|
+
error: "Insufficient organization permissions",
|
|
30
|
+
code: "ORG_ROLE_REQUIRED",
|
|
31
|
+
message: `This action requires one of these organization roles: ${roles.join(", ")}`,
|
|
32
|
+
required: roles,
|
|
33
|
+
current: userOrgRoles
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Shorthand for requiring org context
|
|
42
|
+
*/
|
|
43
|
+
function requireOrg() {
|
|
44
|
+
return orgGuard({ requireOrgContext: true });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Require org context with specific roles
|
|
48
|
+
*/
|
|
49
|
+
function requireOrgRole(...roles) {
|
|
50
|
+
return orgGuard({
|
|
51
|
+
requireOrgContext: true,
|
|
52
|
+
roles
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/org/orgMembership.ts
|
|
58
|
+
/**
|
|
59
|
+
* Check if user is member of organization.
|
|
60
|
+
* This is a low-level utility for checking membership from user object data.
|
|
61
|
+
* For request-level checks, use `request.scope` (isMember/isElevated guards).
|
|
62
|
+
*/
|
|
63
|
+
async function orgMembershipCheck(user, orgId, options = {}) {
|
|
64
|
+
const { userOrgsPath = "organizations", validateFromDb } = options;
|
|
65
|
+
if (!user || !orgId) return false;
|
|
66
|
+
if ((user[userOrgsPath] ?? []).some((o) => {
|
|
67
|
+
return (o.organizationId?.toString() ?? String(o)) === orgId.toString();
|
|
68
|
+
})) return true;
|
|
69
|
+
if (validateFromDb) {
|
|
70
|
+
const userId = (user._id ?? user.id)?.toString();
|
|
71
|
+
if (userId) return validateFromDb(userId, orgId);
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get user's role in organization from user object data.
|
|
77
|
+
* For request-level role checks, use `request.scope.orgRoles` (when scope is 'member').
|
|
78
|
+
*/
|
|
79
|
+
function getUserOrgRoles(user, orgId, options = {}) {
|
|
80
|
+
const { userOrgsPath = "organizations" } = options;
|
|
81
|
+
if (!user || !orgId) return [];
|
|
82
|
+
return (user[userOrgsPath] ?? []).find((o) => {
|
|
83
|
+
return (o.organizationId?.toString() ?? String(o)) === orgId.toString();
|
|
84
|
+
})?.roles ?? [];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if user has specific role in organization from user object data.
|
|
88
|
+
* For request-level role checks, use `requireOrgRole()` permission or `request.scope`.
|
|
89
|
+
*/
|
|
90
|
+
function hasOrgRole(user, orgId, roles, options = {}) {
|
|
91
|
+
const userOrgRoles = getUserOrgRoles(user, orgId, options);
|
|
92
|
+
return (Array.isArray(roles) ? roles : [roles]).some((role) => userOrgRoles.includes(role));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/org/organizationPlugin.ts
|
|
97
|
+
/**
|
|
98
|
+
* Organization Plugin -- Full org management with REST endpoints
|
|
99
|
+
*
|
|
100
|
+
* Creates these routes:
|
|
101
|
+
* - POST /api/organizations -- Create org
|
|
102
|
+
* - GET /api/organizations -- List user's orgs
|
|
103
|
+
* - GET /api/organizations/:orgId -- Get org
|
|
104
|
+
* - PATCH /api/organizations/:orgId -- Update org
|
|
105
|
+
* - DELETE /api/organizations/:orgId -- Delete org
|
|
106
|
+
* - GET /api/organizations/:orgId/members -- List members
|
|
107
|
+
* - POST /api/organizations/:orgId/members -- Add member
|
|
108
|
+
* - PATCH /api/organizations/:orgId/members/:userId -- Update role
|
|
109
|
+
* - DELETE /api/organizations/:orgId/members/:userId -- Remove member
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* import { organizationPlugin } from '@classytic/arc/org';
|
|
113
|
+
*
|
|
114
|
+
* await fastify.register(organizationPlugin, {
|
|
115
|
+
* adapter: myMongooseOrgAdapter,
|
|
116
|
+
* basePath: '/api/organizations',
|
|
117
|
+
* enableInvitations: false,
|
|
118
|
+
* });
|
|
119
|
+
*/
|
|
120
|
+
const DEFAULT_ROLES = [
|
|
121
|
+
{
|
|
122
|
+
name: "owner",
|
|
123
|
+
permissions: [{
|
|
124
|
+
resource: "*",
|
|
125
|
+
action: ["*"]
|
|
126
|
+
}]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "admin",
|
|
130
|
+
permissions: [{
|
|
131
|
+
resource: "org",
|
|
132
|
+
action: ["read", "update"]
|
|
133
|
+
}, {
|
|
134
|
+
resource: "members",
|
|
135
|
+
action: ["*"]
|
|
136
|
+
}]
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "member",
|
|
140
|
+
permissions: [{
|
|
141
|
+
resource: "org",
|
|
142
|
+
action: ["read"]
|
|
143
|
+
}, {
|
|
144
|
+
resource: "members",
|
|
145
|
+
action: ["read"]
|
|
146
|
+
}]
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
/** Extract a UserBase from the request (set by auth plugin). */
|
|
150
|
+
function getUser(request) {
|
|
151
|
+
return request.user;
|
|
152
|
+
}
|
|
153
|
+
/** Get user id (supports both `id` and `_id`). */
|
|
154
|
+
function getUserId(user) {
|
|
155
|
+
const raw = user.id ?? user._id;
|
|
156
|
+
return raw ? String(raw) : void 0;
|
|
157
|
+
}
|
|
158
|
+
/** Standard JSON error reply. */
|
|
159
|
+
function sendError(reply, statusCode, code, message) {
|
|
160
|
+
reply.code(statusCode).send({
|
|
161
|
+
success: false,
|
|
162
|
+
code,
|
|
163
|
+
error: message
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const organizationPlugin = async (fastify, opts) => {
|
|
167
|
+
const { adapter, roles = DEFAULT_ROLES, basePath = "/api/organizations", enableInvitations = false } = opts;
|
|
168
|
+
const validRoleNames = new Set(roles.map((r) => r.name));
|
|
169
|
+
/**
|
|
170
|
+
* Create a preHandler that:
|
|
171
|
+
* 1. Ensures the request is authenticated
|
|
172
|
+
* 2. Looks up the caller's membership in the org identified by `:orgId`
|
|
173
|
+
* 3. Verifies the caller holds one of the required roles
|
|
174
|
+
*/
|
|
175
|
+
fastify.decorate("requireOrgRole", function requireOrgRole(requiredRoles) {
|
|
176
|
+
return async function requireOrgRoleHandler(request, reply) {
|
|
177
|
+
const user = getUser(request);
|
|
178
|
+
if (!user) {
|
|
179
|
+
sendError(reply, 401, "UNAUTHORIZED", "Authentication required");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const userId = getUserId(user);
|
|
183
|
+
if (!userId) {
|
|
184
|
+
sendError(reply, 401, "UNAUTHORIZED", "Unable to determine user identity");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const { orgId } = request.params;
|
|
188
|
+
if (!orgId) {
|
|
189
|
+
sendError(reply, 400, "MISSING_ORG_ID", "Organization ID is required");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const member = await adapter.getMember(orgId, userId);
|
|
193
|
+
if (!member) {
|
|
194
|
+
sendError(reply, 403, "NOT_A_MEMBER", "You are not a member of this organization");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!requiredRoles.includes(member.role)) {
|
|
198
|
+
sendError(reply, 403, "INSUFFICIENT_ROLE", `This action requires one of these roles: ${requiredRoles.join(", ")}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
/** Wrap preHandlers so that authenticate is called first (if available). */
|
|
204
|
+
function withAuth(...extra) {
|
|
205
|
+
const handlers = [];
|
|
206
|
+
const inst = fastify;
|
|
207
|
+
if (typeof inst.authenticate === "function") handlers.push(inst.authenticate);
|
|
208
|
+
handlers.push(...extra);
|
|
209
|
+
return handlers;
|
|
210
|
+
}
|
|
211
|
+
function generateSlug(name) {
|
|
212
|
+
return name.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* POST / -- Create organization
|
|
216
|
+
*
|
|
217
|
+
* Body: { name: string; slug?: string; [key: string]: unknown }
|
|
218
|
+
* The authenticated user becomes the owner.
|
|
219
|
+
*/
|
|
220
|
+
fastify.post(basePath, { preHandler: withAuth() }, async (request, reply) => {
|
|
221
|
+
const user = getUser(request);
|
|
222
|
+
if (!user) {
|
|
223
|
+
sendError(reply, 401, "UNAUTHORIZED", "Authentication required");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const userId = getUserId(user);
|
|
227
|
+
if (!userId) {
|
|
228
|
+
sendError(reply, 401, "UNAUTHORIZED", "Unable to determine user identity");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const body = request.body;
|
|
232
|
+
if (!body?.name) {
|
|
233
|
+
sendError(reply, 400, "VALIDATION_ERROR", "Organization name is required");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const slug = body.slug ?? generateSlug(body.name);
|
|
237
|
+
if (await adapter.getOrgBySlug(slug)) {
|
|
238
|
+
sendError(reply, 409, "SLUG_TAKEN", `An organization with slug '${slug}' already exists`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const org = await adapter.createOrg({
|
|
242
|
+
...body,
|
|
243
|
+
name: body.name,
|
|
244
|
+
slug,
|
|
245
|
+
ownerId: userId
|
|
246
|
+
});
|
|
247
|
+
await adapter.addMember(org.id, userId, "owner");
|
|
248
|
+
reply.code(201).send({
|
|
249
|
+
success: true,
|
|
250
|
+
data: org
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
/**
|
|
254
|
+
* GET / -- List the authenticated user's organizations
|
|
255
|
+
*/
|
|
256
|
+
fastify.get(basePath, { preHandler: withAuth() }, async (request, reply) => {
|
|
257
|
+
const user = getUser(request);
|
|
258
|
+
if (!user) {
|
|
259
|
+
sendError(reply, 401, "UNAUTHORIZED", "Authentication required");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const userId = getUserId(user);
|
|
263
|
+
if (!userId) {
|
|
264
|
+
sendError(reply, 401, "UNAUTHORIZED", "Unable to determine user identity");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const orgs = await adapter.listUserOrgs(userId);
|
|
268
|
+
reply.send({
|
|
269
|
+
success: true,
|
|
270
|
+
data: orgs
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
/**
|
|
274
|
+
* GET /:orgId -- Get a single organization
|
|
275
|
+
*/
|
|
276
|
+
fastify.get(`${basePath}/:orgId`, { preHandler: withAuth(fastify.requireOrgRole([
|
|
277
|
+
"owner",
|
|
278
|
+
"admin",
|
|
279
|
+
"member"
|
|
280
|
+
])) }, async (request, reply) => {
|
|
281
|
+
const { orgId } = request.params;
|
|
282
|
+
const org = await adapter.getOrg(orgId);
|
|
283
|
+
if (!org) {
|
|
284
|
+
sendError(reply, 404, "NOT_FOUND", "Organization not found");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
reply.send({
|
|
288
|
+
success: true,
|
|
289
|
+
data: org
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
/**
|
|
293
|
+
* PATCH /:orgId -- Update organization
|
|
294
|
+
*/
|
|
295
|
+
fastify.patch(`${basePath}/:orgId`, { preHandler: withAuth(fastify.requireOrgRole(["owner", "admin"])) }, async (request, reply) => {
|
|
296
|
+
const { orgId } = request.params;
|
|
297
|
+
const body = request.body;
|
|
298
|
+
if (!body || Object.keys(body).length === 0) {
|
|
299
|
+
sendError(reply, 400, "VALIDATION_ERROR", "Request body must not be empty");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const { ownerId: _ownerId, id: _id, ...updates } = body;
|
|
303
|
+
const org = await adapter.updateOrg(orgId, updates);
|
|
304
|
+
if (!org) {
|
|
305
|
+
sendError(reply, 404, "NOT_FOUND", "Organization not found");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
reply.send({
|
|
309
|
+
success: true,
|
|
310
|
+
data: org
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
/**
|
|
314
|
+
* DELETE /:orgId -- Delete organization (owner only)
|
|
315
|
+
*/
|
|
316
|
+
fastify.delete(`${basePath}/:orgId`, { preHandler: withAuth(fastify.requireOrgRole(["owner"])) }, async (request, reply) => {
|
|
317
|
+
const { orgId } = request.params;
|
|
318
|
+
if (!await adapter.getOrg(orgId)) {
|
|
319
|
+
sendError(reply, 404, "NOT_FOUND", "Organization not found");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
await adapter.deleteOrg(orgId);
|
|
323
|
+
reply.send({
|
|
324
|
+
success: true,
|
|
325
|
+
message: "Organization deleted"
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
/**
|
|
329
|
+
* GET /:orgId/members -- List members
|
|
330
|
+
*/
|
|
331
|
+
fastify.get(`${basePath}/:orgId/members`, { preHandler: withAuth(fastify.requireOrgRole([
|
|
332
|
+
"owner",
|
|
333
|
+
"admin",
|
|
334
|
+
"member"
|
|
335
|
+
])) }, async (request, reply) => {
|
|
336
|
+
const { orgId } = request.params;
|
|
337
|
+
const members = await adapter.listMembers(orgId);
|
|
338
|
+
reply.send({
|
|
339
|
+
success: true,
|
|
340
|
+
data: members
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
/**
|
|
344
|
+
* POST /:orgId/members -- Add a member
|
|
345
|
+
*
|
|
346
|
+
* Body: { userId: string; role: string }
|
|
347
|
+
*/
|
|
348
|
+
fastify.post(`${basePath}/:orgId/members`, { preHandler: withAuth(fastify.requireOrgRole(["owner", "admin"])) }, async (request, reply) => {
|
|
349
|
+
const { orgId } = request.params;
|
|
350
|
+
const body = request.body;
|
|
351
|
+
if (!body?.userId || !body.role) {
|
|
352
|
+
sendError(reply, 400, "VALIDATION_ERROR", "userId and role are required");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!validRoleNames.has(body.role)) {
|
|
356
|
+
sendError(reply, 400, "INVALID_ROLE", `Invalid role '${body.role}'. Valid roles: ${[...validRoleNames].join(", ")}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (await adapter.getMember(orgId, body.userId)) {
|
|
360
|
+
sendError(reply, 409, "ALREADY_MEMBER", "User is already a member of this organization");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const member = await adapter.addMember(orgId, body.userId, body.role);
|
|
364
|
+
reply.code(201).send({
|
|
365
|
+
success: true,
|
|
366
|
+
data: member
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
/**
|
|
370
|
+
* PATCH /:orgId/members/:userId -- Update a member's role
|
|
371
|
+
*
|
|
372
|
+
* Body: { role: string }
|
|
373
|
+
*/
|
|
374
|
+
fastify.patch(`${basePath}/:orgId/members/:userId`, { preHandler: withAuth(fastify.requireOrgRole(["owner", "admin"])) }, async (request, reply) => {
|
|
375
|
+
const { orgId, userId } = request.params;
|
|
376
|
+
const body = request.body;
|
|
377
|
+
if (!body?.role) {
|
|
378
|
+
sendError(reply, 400, "VALIDATION_ERROR", "role is required");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!validRoleNames.has(body.role)) {
|
|
382
|
+
sendError(reply, 400, "INVALID_ROLE", `Invalid role '${body.role}'. Valid roles: ${[...validRoleNames].join(", ")}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const currentMember = await adapter.getMember(orgId, userId);
|
|
386
|
+
if (!currentMember) {
|
|
387
|
+
sendError(reply, 404, "NOT_FOUND", "Member not found");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (currentMember.role === "owner" && body.role !== "owner") {
|
|
391
|
+
if ((await adapter.listMembers(orgId)).filter((m) => m.role === "owner").length <= 1) {
|
|
392
|
+
sendError(reply, 400, "LAST_OWNER", "Cannot change the role of the last owner. Transfer ownership first.");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const member = await adapter.updateMemberRole(orgId, userId, body.role);
|
|
397
|
+
if (!member) {
|
|
398
|
+
sendError(reply, 404, "NOT_FOUND", "Member not found");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
reply.send({
|
|
402
|
+
success: true,
|
|
403
|
+
data: member
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
/**
|
|
407
|
+
* DELETE /:orgId/members/:userId -- Remove a member
|
|
408
|
+
*/
|
|
409
|
+
fastify.delete(`${basePath}/:orgId/members/:userId`, { preHandler: withAuth(fastify.requireOrgRole(["owner", "admin"])) }, async (request, reply) => {
|
|
410
|
+
const { orgId, userId } = request.params;
|
|
411
|
+
const member = await adapter.getMember(orgId, userId);
|
|
412
|
+
if (!member) {
|
|
413
|
+
sendError(reply, 404, "NOT_FOUND", "Member not found");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (member.role === "owner") {
|
|
417
|
+
if ((await adapter.listMembers(orgId)).filter((m) => m.role === "owner").length <= 1) {
|
|
418
|
+
sendError(reply, 400, "LAST_OWNER", "Cannot remove the last owner. Transfer ownership or delete the organization.");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
await adapter.removeMember(orgId, userId);
|
|
423
|
+
reply.send({
|
|
424
|
+
success: true,
|
|
425
|
+
message: "Member removed"
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
if (enableInvitations && adapter.invitations) {
|
|
429
|
+
const inv = adapter.invitations;
|
|
430
|
+
/**
|
|
431
|
+
* POST /:orgId/invitations -- Create invitation
|
|
432
|
+
*
|
|
433
|
+
* Body: { email: string; role: string; expiresAt?: string }
|
|
434
|
+
*/
|
|
435
|
+
fastify.post(`${basePath}/:orgId/invitations`, { preHandler: withAuth(fastify.requireOrgRole(["owner", "admin"])) }, async (request, reply) => {
|
|
436
|
+
const user = getUser(request);
|
|
437
|
+
const userId = user ? getUserId(user) : void 0;
|
|
438
|
+
if (!userId) {
|
|
439
|
+
sendError(reply, 401, "UNAUTHORIZED", "Authentication required");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const { orgId } = request.params;
|
|
443
|
+
const body = request.body;
|
|
444
|
+
if (!body?.email || !body.role) {
|
|
445
|
+
sendError(reply, 400, "VALIDATION_ERROR", "email and role are required");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!validRoleNames.has(body.role)) {
|
|
449
|
+
sendError(reply, 400, "INVALID_ROLE", `Invalid role '${body.role}'. Valid roles: ${[...validRoleNames].join(", ")}`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const defaultExpiry = new Date(Date.now() + 10080 * 60 * 1e3);
|
|
453
|
+
const expiresAt = body.expiresAt ? new Date(body.expiresAt) : defaultExpiry;
|
|
454
|
+
const invitation = await inv.create({
|
|
455
|
+
orgId,
|
|
456
|
+
email: body.email,
|
|
457
|
+
role: body.role,
|
|
458
|
+
invitedBy: userId,
|
|
459
|
+
status: "pending",
|
|
460
|
+
expiresAt
|
|
461
|
+
});
|
|
462
|
+
reply.code(201).send({
|
|
463
|
+
success: true,
|
|
464
|
+
data: invitation
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
/**
|
|
468
|
+
* GET /:orgId/invitations -- List pending invitations
|
|
469
|
+
*/
|
|
470
|
+
fastify.get(`${basePath}/:orgId/invitations`, { preHandler: withAuth(fastify.requireOrgRole(["owner", "admin"])) }, async (request, reply) => {
|
|
471
|
+
const { orgId } = request.params;
|
|
472
|
+
const invitations = await inv.listPending(orgId);
|
|
473
|
+
reply.send({
|
|
474
|
+
success: true,
|
|
475
|
+
data: invitations
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
/**
|
|
479
|
+
* POST /invitations/:invitationId/accept -- Accept invitation
|
|
480
|
+
*/
|
|
481
|
+
fastify.post(`${basePath}/invitations/:invitationId/accept`, { preHandler: withAuth() }, async (request, reply) => {
|
|
482
|
+
const { invitationId } = request.params;
|
|
483
|
+
await inv.accept(invitationId);
|
|
484
|
+
reply.send({
|
|
485
|
+
success: true,
|
|
486
|
+
message: "Invitation accepted"
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
/**
|
|
490
|
+
* POST /invitations/:invitationId/reject -- Reject invitation
|
|
491
|
+
*/
|
|
492
|
+
fastify.post(`${basePath}/invitations/:invitationId/reject`, { preHandler: withAuth() }, async (request, reply) => {
|
|
493
|
+
const { invitationId } = request.params;
|
|
494
|
+
await inv.reject(invitationId);
|
|
495
|
+
reply.send({
|
|
496
|
+
success: true,
|
|
497
|
+
message: "Invitation rejected"
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
fastify.log?.debug?.({
|
|
502
|
+
basePath,
|
|
503
|
+
roles: [...validRoleNames],
|
|
504
|
+
invitations: enableInvitations
|
|
505
|
+
}, "Organization plugin registered");
|
|
506
|
+
};
|
|
507
|
+
var organizationPlugin_default = fp(organizationPlugin, {
|
|
508
|
+
name: "arc-organization",
|
|
509
|
+
fastify: "5.x"
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
export { getUserOrgRoles, hasOrgRole, orgGuard, orgMembershipCheck, organizationPlugin_default as organizationPlugin, organizationPlugin as organizationPluginFn, requireOrg, requireOrgRole };
|
|
514
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/org/orgGuard.ts","../../src/org/orgMembership.ts","../../src/org/organizationPlugin.ts"],"sourcesContent":["/**\n * Organization Guard Middleware\n *\n * Ensures organization context is present before handler execution.\n *\n * @example\n * // Require org context\n * fastify.get('/invoices', {\n * preHandler: [fastify.authenticate, orgGuard()]\n * }, handler);\n *\n * // Require specific org roles\n * fastify.post('/invoices', {\n * preHandler: [fastify.authenticate, orgGuard({ roles: ['admin', 'accountant'] })]\n * }, handler);\n */\n\nimport type { FastifyReply } from 'fastify';\nimport type { RequestWithExtras, RouteHandler } from '../types/index.js';\nimport type { RequestScope } from '../scope/types.js';\nimport { isMember, isElevated, hasOrgAccess, getOrgRoles, PUBLIC_SCOPE } from '../scope/types.js';\n\nexport interface OrgGuardOptions {\n /** Require organization context (default: true) */\n requireOrgContext?: boolean;\n /** Required org-level roles */\n roles?: string[];\n}\n\n/**\n * Create org guard middleware.\n * Reads `request.scope` for org context and roles.\n * Elevated scope always passes.\n */\nexport function orgGuard(options: OrgGuardOptions = {}): RouteHandler {\n const {\n requireOrgContext = true,\n roles = [],\n } = options;\n\n return async function orgGuardMiddleware(\n request: RequestWithExtras,\n reply: FastifyReply\n ): Promise<void> {\n const scope = request.scope ?? PUBLIC_SCOPE;\n\n // Elevated scope always passes\n if (isElevated(scope)) return;\n\n // Check org context exists\n if (requireOrgContext && !hasOrgAccess(scope)) {\n reply.code(403).send({\n success: false,\n error: 'Organization context required',\n code: 'ORG_CONTEXT_REQUIRED',\n message:\n 'This endpoint requires an organization context. ' +\n 'Please specify organization via x-organization-id header.',\n });\n return;\n }\n\n // Check org-level roles if specified\n if (roles.length > 0 && isMember(scope)) {\n const userOrgRoles = getOrgRoles(scope);\n const hasRequiredRole = roles.some((role) => userOrgRoles.includes(role));\n\n if (!hasRequiredRole) {\n reply.code(403).send({\n success: false,\n error: 'Insufficient organization permissions',\n code: 'ORG_ROLE_REQUIRED',\n message: `This action requires one of these organization roles: ${roles.join(', ')}`,\n required: roles,\n current: userOrgRoles,\n });\n return;\n }\n }\n };\n}\n\n/**\n * Shorthand for requiring org context\n */\nexport function requireOrg(): RouteHandler {\n return orgGuard({ requireOrgContext: true });\n}\n\n/**\n * Require org context with specific roles\n */\nexport function requireOrgRole(...roles: string[]): RouteHandler {\n return orgGuard({ requireOrgContext: true, roles });\n}\n\nexport default orgGuard;\n","/**\n * Organization Membership Utilities\n *\n * Server-side membership validation.\n */\n\nimport type { UserBase, UserOrganization } from '../types/index.js';\n\nexport interface OrgMembershipOptions {\n /** Path to user's organizations array */\n userOrgsPath?: string;\n /** Optional DB lookup function */\n validateFromDb?: (userId: string, orgId: string) => Promise<boolean>;\n}\n\nexport interface OrgRolesOptions {\n /** Path to user's organizations array */\n userOrgsPath?: string;\n}\n\n/**\n * Check if user is member of organization.\n * This is a low-level utility for checking membership from user object data.\n * For request-level checks, use `request.scope` (isMember/isElevated guards).\n */\nexport async function orgMembershipCheck(\n user: UserBase | undefined | null,\n orgId: string | undefined | null,\n options: OrgMembershipOptions = {}\n): Promise<boolean> {\n const {\n userOrgsPath = 'organizations',\n validateFromDb,\n } = options;\n\n if (!user || !orgId) return false;\n\n // Check from user object\n const userOrgs = ((user as UserBase & { [key: string]: unknown })[userOrgsPath] ?? []) as UserOrganization[];\n const isMemberFromUser = userOrgs.some((o) => {\n const memberOrgId = o.organizationId?.toString() ?? String(o);\n return memberOrgId === orgId.toString();\n });\n\n if (isMemberFromUser) return true;\n\n // Optional: validate from database\n if (validateFromDb) {\n const userId = (user._id ?? user.id)?.toString();\n if (userId) {\n return validateFromDb(userId, orgId);\n }\n }\n\n return false;\n}\n\n/**\n * Get user's role in organization from user object data.\n * For request-level role checks, use `request.scope.orgRoles` (when scope is 'member').\n */\nexport function getUserOrgRoles(\n user: UserBase | undefined | null,\n orgId: string | undefined | null,\n options: OrgRolesOptions = {}\n): string[] {\n const { userOrgsPath = 'organizations' } = options;\n\n if (!user || !orgId) return [];\n\n const userOrgs = ((user as UserBase & { [key: string]: unknown })[userOrgsPath] ?? []) as UserOrganization[];\n const membership = userOrgs.find((o) => {\n const memberOrgId = o.organizationId?.toString() ?? String(o);\n return memberOrgId === orgId.toString();\n });\n\n const membershipRoles = membership as { roles?: string[] } | undefined;\n return membershipRoles?.roles ?? [];\n}\n\n/**\n * Check if user has specific role in organization from user object data.\n * For request-level role checks, use `requireOrgRole()` permission or `request.scope`.\n */\nexport function hasOrgRole(\n user: UserBase | undefined | null,\n orgId: string | undefined | null,\n roles: string | string[],\n options: OrgRolesOptions = {}\n): boolean {\n const userOrgRoles = getUserOrgRoles(user, orgId, options);\n const requiredRoles = Array.isArray(roles) ? roles : [roles];\n\n return requiredRoles.some((role) => userOrgRoles.includes(role));\n}\n\nexport default { orgMembershipCheck, getUserOrgRoles, hasOrgRole };\n","/**\n * Organization Plugin -- Full org management with REST endpoints\n *\n * Creates these routes:\n * - POST /api/organizations -- Create org\n * - GET /api/organizations -- List user's orgs\n * - GET /api/organizations/:orgId -- Get org\n * - PATCH /api/organizations/:orgId -- Update org\n * - DELETE /api/organizations/:orgId -- Delete org\n * - GET /api/organizations/:orgId/members -- List members\n * - POST /api/organizations/:orgId/members -- Add member\n * - PATCH /api/organizations/:orgId/members/:userId -- Update role\n * - DELETE /api/organizations/:orgId/members/:userId -- Remove member\n *\n * @example\n * import { organizationPlugin } from '@classytic/arc/org';\n *\n * await fastify.register(organizationPlugin, {\n * adapter: myMongooseOrgAdapter,\n * basePath: '/api/organizations',\n * enableInvitations: false,\n * });\n */\n\nimport fp from 'fastify-plugin';\nimport type {\n FastifyInstance,\n FastifyPluginAsync,\n FastifyReply,\n FastifyRequest,\n RouteHandlerMethod,\n} from 'fastify';\nimport type {\n OrgAdapter,\n OrgRole,\n OrganizationPluginOptions,\n MemberDoc,\n} from './types.js';\nimport type { UserBase } from '../permissions/types.js';\n\n// ---------------------------------------------------------------------------\n// Fastify type augmentations\n// ---------------------------------------------------------------------------\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n /** Middleware: require the caller to hold one of the listed org roles */\n requireOrgRole: (roles: string[]) => RouteHandlerMethod;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Default roles\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_ROLES: OrgRole[] = [\n {\n name: 'owner',\n permissions: [{ resource: '*', action: ['*'] }],\n },\n {\n name: 'admin',\n permissions: [\n { resource: 'org', action: ['read', 'update'] },\n { resource: 'members', action: ['*'] },\n ],\n },\n {\n name: 'member',\n permissions: [\n { resource: 'org', action: ['read'] },\n { resource: 'members', action: ['read'] },\n ],\n },\n];\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract a UserBase from the request (set by auth plugin). */\nfunction getUser(request: FastifyRequest): UserBase | undefined {\n return (request as FastifyRequest & { user?: UserBase }).user;\n}\n\n/** Get user id (supports both `id` and `_id`). */\nfunction getUserId(user: UserBase): string | undefined {\n const raw = user.id ?? user._id;\n return raw ? String(raw) : undefined;\n}\n\n/** Standard JSON error reply. */\nfunction sendError(\n reply: FastifyReply,\n statusCode: number,\n code: string,\n message: string,\n): void {\n void reply.code(statusCode).send({ success: false, code, error: message });\n}\n\n// ---------------------------------------------------------------------------\n// Plugin implementation\n// ---------------------------------------------------------------------------\n\nconst organizationPlugin: FastifyPluginAsync<OrganizationPluginOptions> = async (\n fastify: FastifyInstance,\n opts: OrganizationPluginOptions,\n) => {\n const {\n adapter,\n roles = DEFAULT_ROLES,\n basePath = '/api/organizations',\n enableInvitations = false,\n } = opts;\n\n // Collect valid role names for quick validation\n const validRoleNames = new Set(roles.map((r) => r.name));\n\n // --------------------------------------------------\n // requireOrgRole decorator\n // --------------------------------------------------\n\n /**\n * Create a preHandler that:\n * 1. Ensures the request is authenticated\n * 2. Looks up the caller's membership in the org identified by `:orgId`\n * 3. Verifies the caller holds one of the required roles\n */\n fastify.decorate(\n 'requireOrgRole',\n function requireOrgRole(requiredRoles: string[]): RouteHandlerMethod {\n return async function requireOrgRoleHandler(\n request: FastifyRequest,\n reply: FastifyReply,\n ): Promise<void> {\n const user = getUser(request);\n if (!user) {\n sendError(reply, 401, 'UNAUTHORIZED', 'Authentication required');\n return;\n }\n\n const userId = getUserId(user);\n if (!userId) {\n sendError(reply, 401, 'UNAUTHORIZED', 'Unable to determine user identity');\n return;\n }\n\n const { orgId } = request.params as { orgId?: string };\n if (!orgId) {\n sendError(reply, 400, 'MISSING_ORG_ID', 'Organization ID is required');\n return;\n }\n\n const member = await adapter.getMember(orgId, userId);\n if (!member) {\n sendError(reply, 403, 'NOT_A_MEMBER', 'You are not a member of this organization');\n return;\n }\n\n const hasRole = requiredRoles.includes(member.role);\n if (!hasRole) {\n sendError(\n reply,\n 403,\n 'INSUFFICIENT_ROLE',\n `This action requires one of these roles: ${requiredRoles.join(', ')}`,\n );\n return;\n }\n };\n },\n );\n\n // --------------------------------------------------\n // Auth helper -- optional authenticate decorator\n // --------------------------------------------------\n\n /** Wrap preHandlers so that authenticate is called first (if available). */\n function withAuth(...extra: RouteHandlerMethod[]): RouteHandlerMethod[] {\n const handlers: RouteHandlerMethod[] = [];\n const inst = fastify as FastifyInstance & { authenticate?: RouteHandlerMethod };\n if (typeof inst.authenticate === 'function') {\n handlers.push(inst.authenticate);\n }\n handlers.push(...extra);\n return handlers;\n }\n\n // --------------------------------------------------\n // Slug helper\n // --------------------------------------------------\n\n function generateSlug(name: string): string {\n return name\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/[\\s_]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n }\n\n // --------------------------------------------------\n // Organization routes\n // --------------------------------------------------\n\n /**\n * POST / -- Create organization\n *\n * Body: { name: string; slug?: string; [key: string]: unknown }\n * The authenticated user becomes the owner.\n */\n fastify.post(\n basePath,\n { preHandler: withAuth() },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const user = getUser(request);\n if (!user) {\n sendError(reply, 401, 'UNAUTHORIZED', 'Authentication required');\n return;\n }\n\n const userId = getUserId(user);\n if (!userId) {\n sendError(reply, 401, 'UNAUTHORIZED', 'Unable to determine user identity');\n return;\n }\n\n const body = request.body as { name?: string; slug?: string; [key: string]: unknown } | undefined;\n if (!body?.name) {\n sendError(reply, 400, 'VALIDATION_ERROR', 'Organization name is required');\n return;\n }\n\n const slug = body.slug ?? generateSlug(body.name);\n\n // Check slug uniqueness\n const existing = await adapter.getOrgBySlug(slug);\n if (existing) {\n sendError(reply, 409, 'SLUG_TAKEN', `An organization with slug '${slug}' already exists`);\n return;\n }\n\n const org = await adapter.createOrg({ ...body, name: body.name, slug, ownerId: userId });\n\n // Auto-add creator as owner\n await adapter.addMember(org.id, userId, 'owner');\n\n void reply.code(201).send({ success: true, data: org });\n },\n );\n\n /**\n * GET / -- List the authenticated user's organizations\n */\n fastify.get(\n basePath,\n { preHandler: withAuth() },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const user = getUser(request);\n if (!user) {\n sendError(reply, 401, 'UNAUTHORIZED', 'Authentication required');\n return;\n }\n\n const userId = getUserId(user);\n if (!userId) {\n sendError(reply, 401, 'UNAUTHORIZED', 'Unable to determine user identity');\n return;\n }\n\n const orgs = await adapter.listUserOrgs(userId);\n void reply.send({ success: true, data: orgs });\n },\n );\n\n /**\n * GET /:orgId -- Get a single organization\n */\n fastify.get(\n `${basePath}/:orgId`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin', 'member'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId } = request.params as { orgId: string };\n const org = await adapter.getOrg(orgId);\n\n if (!org) {\n sendError(reply, 404, 'NOT_FOUND', 'Organization not found');\n return;\n }\n\n void reply.send({ success: true, data: org });\n },\n );\n\n /**\n * PATCH /:orgId -- Update organization\n */\n fastify.patch(\n `${basePath}/:orgId`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId } = request.params as { orgId: string };\n const body = request.body as Partial<Record<string, unknown>> | undefined;\n\n if (!body || Object.keys(body).length === 0) {\n sendError(reply, 400, 'VALIDATION_ERROR', 'Request body must not be empty');\n return;\n }\n\n // Prevent changing ownerId or id through PATCH\n const { ownerId: _ownerId, id: _id, ...updates } = body;\n\n const org = await adapter.updateOrg(orgId, updates);\n if (!org) {\n sendError(reply, 404, 'NOT_FOUND', 'Organization not found');\n return;\n }\n\n void reply.send({ success: true, data: org });\n },\n );\n\n /**\n * DELETE /:orgId -- Delete organization (owner only)\n */\n fastify.delete(\n `${basePath}/:orgId`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId } = request.params as { orgId: string };\n\n const org = await adapter.getOrg(orgId);\n if (!org) {\n sendError(reply, 404, 'NOT_FOUND', 'Organization not found');\n return;\n }\n\n await adapter.deleteOrg(orgId);\n void reply.send({ success: true, message: 'Organization deleted' });\n },\n );\n\n // --------------------------------------------------\n // Member routes\n // --------------------------------------------------\n\n /**\n * GET /:orgId/members -- List members\n */\n fastify.get(\n `${basePath}/:orgId/members`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin', 'member'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId } = request.params as { orgId: string };\n const members = await adapter.listMembers(orgId);\n void reply.send({ success: true, data: members });\n },\n );\n\n /**\n * POST /:orgId/members -- Add a member\n *\n * Body: { userId: string; role: string }\n */\n fastify.post(\n `${basePath}/:orgId/members`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId } = request.params as { orgId: string };\n const body = request.body as { userId?: string; role?: string } | undefined;\n\n if (!body?.userId || !body.role) {\n sendError(reply, 400, 'VALIDATION_ERROR', 'userId and role are required');\n return;\n }\n\n if (!validRoleNames.has(body.role)) {\n sendError(\n reply,\n 400,\n 'INVALID_ROLE',\n `Invalid role '${body.role}'. Valid roles: ${[...validRoleNames].join(', ')}`,\n );\n return;\n }\n\n // Prevent duplicate membership\n const existing = await adapter.getMember(orgId, body.userId);\n if (existing) {\n sendError(reply, 409, 'ALREADY_MEMBER', 'User is already a member of this organization');\n return;\n }\n\n const member = await adapter.addMember(orgId, body.userId, body.role);\n void reply.code(201).send({ success: true, data: member });\n },\n );\n\n /**\n * PATCH /:orgId/members/:userId -- Update a member's role\n *\n * Body: { role: string }\n */\n fastify.patch(\n `${basePath}/:orgId/members/:userId`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId, userId } = request.params as { orgId: string; userId: string };\n const body = request.body as { role?: string } | undefined;\n\n if (!body?.role) {\n sendError(reply, 400, 'VALIDATION_ERROR', 'role is required');\n return;\n }\n\n if (!validRoleNames.has(body.role)) {\n sendError(\n reply,\n 400,\n 'INVALID_ROLE',\n `Invalid role '${body.role}'. Valid roles: ${[...validRoleNames].join(', ')}`,\n );\n return;\n }\n\n // Prevent demoting the last owner\n const currentMember = await adapter.getMember(orgId, userId);\n if (!currentMember) {\n sendError(reply, 404, 'NOT_FOUND', 'Member not found');\n return;\n }\n\n if (currentMember.role === 'owner' && body.role !== 'owner') {\n const members = await adapter.listMembers(orgId);\n const ownerCount = members.filter((m: MemberDoc) => m.role === 'owner').length;\n if (ownerCount <= 1) {\n sendError(\n reply,\n 400,\n 'LAST_OWNER',\n 'Cannot change the role of the last owner. Transfer ownership first.',\n );\n return;\n }\n }\n\n const member = await adapter.updateMemberRole(orgId, userId, body.role);\n if (!member) {\n sendError(reply, 404, 'NOT_FOUND', 'Member not found');\n return;\n }\n\n void reply.send({ success: true, data: member });\n },\n );\n\n /**\n * DELETE /:orgId/members/:userId -- Remove a member\n */\n fastify.delete(\n `${basePath}/:orgId/members/:userId`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId, userId } = request.params as { orgId: string; userId: string };\n\n const member = await adapter.getMember(orgId, userId);\n if (!member) {\n sendError(reply, 404, 'NOT_FOUND', 'Member not found');\n return;\n }\n\n // Prevent removing the last owner\n if (member.role === 'owner') {\n const members = await adapter.listMembers(orgId);\n const ownerCount = members.filter((m: MemberDoc) => m.role === 'owner').length;\n if (ownerCount <= 1) {\n sendError(\n reply,\n 400,\n 'LAST_OWNER',\n 'Cannot remove the last owner. Transfer ownership or delete the organization.',\n );\n return;\n }\n }\n\n await adapter.removeMember(orgId, userId);\n void reply.send({ success: true, message: 'Member removed' });\n },\n );\n\n // --------------------------------------------------\n // Invitation routes (optional)\n // --------------------------------------------------\n\n if (enableInvitations && adapter.invitations) {\n const inv = adapter.invitations;\n\n /**\n * POST /:orgId/invitations -- Create invitation\n *\n * Body: { email: string; role: string; expiresAt?: string }\n */\n fastify.post(\n `${basePath}/:orgId/invitations`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const user = getUser(request);\n const userId = user ? getUserId(user) : undefined;\n if (!userId) {\n sendError(reply, 401, 'UNAUTHORIZED', 'Authentication required');\n return;\n }\n\n const { orgId } = request.params as { orgId: string };\n const body = request.body as { email?: string; role?: string; expiresAt?: string } | undefined;\n\n if (!body?.email || !body.role) {\n sendError(reply, 400, 'VALIDATION_ERROR', 'email and role are required');\n return;\n }\n\n if (!validRoleNames.has(body.role)) {\n sendError(\n reply,\n 400,\n 'INVALID_ROLE',\n `Invalid role '${body.role}'. Valid roles: ${[...validRoleNames].join(', ')}`,\n );\n return;\n }\n\n const defaultExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : defaultExpiry;\n\n const invitation = await inv.create({\n orgId,\n email: body.email,\n role: body.role,\n invitedBy: userId,\n status: 'pending',\n expiresAt,\n });\n\n void reply.code(201).send({ success: true, data: invitation });\n },\n );\n\n /**\n * GET /:orgId/invitations -- List pending invitations\n */\n fastify.get(\n `${basePath}/:orgId/invitations`,\n { preHandler: withAuth(fastify.requireOrgRole(['owner', 'admin'])) },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { orgId } = request.params as { orgId: string };\n const invitations = await inv.listPending(orgId);\n void reply.send({ success: true, data: invitations });\n },\n );\n\n /**\n * POST /invitations/:invitationId/accept -- Accept invitation\n */\n fastify.post(\n `${basePath}/invitations/:invitationId/accept`,\n { preHandler: withAuth() },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { invitationId } = request.params as { invitationId: string };\n await inv.accept(invitationId);\n void reply.send({ success: true, message: 'Invitation accepted' });\n },\n );\n\n /**\n * POST /invitations/:invitationId/reject -- Reject invitation\n */\n fastify.post(\n `${basePath}/invitations/:invitationId/reject`,\n { preHandler: withAuth() },\n async (request: FastifyRequest, reply: FastifyReply) => {\n const { invitationId } = request.params as { invitationId: string };\n await inv.reject(invitationId);\n void reply.send({ success: true, message: 'Invitation rejected' });\n },\n );\n }\n\n fastify.log?.debug?.(\n { basePath, roles: [...validRoleNames], invitations: enableInvitations },\n 'Organization plugin registered',\n );\n};\n\nexport default fp(organizationPlugin, {\n name: 'arc-organization',\n fastify: '5.x',\n});\n\nexport { organizationPlugin };\n"],"mappings":"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"}
|