@shipfox/api-auth-context 18.0.0 → 19.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +9 -5
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled: 1 file with swc (
|
|
2
|
+
Successfully compiled: 1 file with swc (228.72ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @shipfox/api-auth-context
|
|
2
2
|
|
|
3
|
+
## 19.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b416c4c: Preserves existing package behavior while simplifying internal control flow.
|
|
8
|
+
- Updated dependencies [621108c]
|
|
9
|
+
- @shipfox/api-auth-dto@19.0.0
|
|
10
|
+
- @shipfox/api-workspaces-dto@15.0.0
|
|
11
|
+
- @shipfox/node-fastify@0.4.3
|
|
12
|
+
|
|
3
13
|
## 18.0.0
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,uBAAuB,CAAC;AACzF,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EACL,WAAW,EAEX,KAAK,WAAW,EAEjB,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,SAAS,SAAS,CAAC;AAChC,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAC1E,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,2BAA2B,2BAA2B,CAAC;AACpE,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,aAAa,CAAC;IACpB,eAAe,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAClD,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;CAC5D;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,WAAW,CAAC,EAAE,aAAa,CAAC,qBAAqB,CAAC,GAAG,SAAS,CAAC;IAC/D,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,WAAW,CAe5E;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAC,GACnD;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,CAAC;AAE1E,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AACnD,MAAM,MAAM,oBAAoB,GAAG,wBAAwB,CAAC;AAC5D,MAAM,WAAW,2BAA2B;IAC1C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,kBAAkB,GAAG,MAAM,CAAC;AAUjC,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAEtF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,GAAG,IAAI,CAI9E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAM3E;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAS3E;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,kBAAkB,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,aAAa,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,4BAA4B,CAmB9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAK5E;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -121,6 +121,13 @@ function isAdministrationPrefix(prefix) {
|
|
|
121
121
|
}
|
|
122
122
|
return `${parent.replace(TRAILING_SLASHES, '')}/${child.replace(LEADING_SLASHES, '')}`;
|
|
123
123
|
}
|
|
124
|
+
function routePreHandlers(preHandler) {
|
|
125
|
+
if (preHandler === undefined) return [];
|
|
126
|
+
if (Array.isArray(preHandler)) return preHandler;
|
|
127
|
+
return [
|
|
128
|
+
preHandler
|
|
129
|
+
];
|
|
130
|
+
}
|
|
124
131
|
function adoptAdministrationGuardIn(route, parentPrefix) {
|
|
125
132
|
if (isRouteGroup(route)) {
|
|
126
133
|
return {
|
|
@@ -137,9 +144,7 @@ function adoptAdministrationGuardIn(route, parentPrefix) {
|
|
|
137
144
|
requireAdministrationActor(request);
|
|
138
145
|
return undefined;
|
|
139
146
|
},
|
|
140
|
-
...
|
|
141
|
-
route.preHandler
|
|
142
|
-
]
|
|
147
|
+
...routePreHandlers(route.preHandler)
|
|
143
148
|
];
|
|
144
149
|
return {
|
|
145
150
|
...route,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {JobLeaseTokenClaims, RunnerSessionTokenClaims} from '@shipfox/api-auth-dto';\nimport type {WorkspaceRole} from '@shipfox/api-workspaces-dto';\nimport {\n ClientError,\n isRouteGroup,\n type RouteExport,\n type RoutePreHandler,\n} from '@shipfox/node-fastify';\n\nexport const AUTH_USER = 'user';\nexport const AUTH_RUNNER_REGISTRATION_TOKEN = 'runner-registration-token';\nexport const AUTH_RUNNER_SESSION = 'runner-session';\nexport const AUTH_RUNNER_CONTROL_SESSION = 'runner-control-session';\nexport const AUTH_LEASED_JOB = 'leased-job';\nexport const AUTH_PROVISIONER_TOKEN = 'provisioner-token';\n\nexport type WorkspaceStatus = 'active' | 'suspended' | 'deleted';\n\nexport interface UserContextMembership {\n workspaceId: string;\n role: WorkspaceRole;\n workspaceStatus: WorkspaceStatus;\n}\n\nexport interface UserContext {\n userId: string;\n email: string;\n name: string | null;\n memberships: ReadonlyArray<UserContextMembership>;\n impersonatorId?: string | undefined;\n canAccess(workspaceId: string): boolean;\n hasRole(workspaceId: string, role: WorkspaceRole): boolean;\n}\n\nexport interface BuildUserContextParams {\n userId: string;\n email: string;\n name?: string | null | undefined;\n memberships?: ReadonlyArray<UserContextMembership> | undefined;\n impersonatorId?: string | undefined;\n}\n\nexport function buildUserContext(params: BuildUserContextParams): UserContext {\n const memberships = params.memberships ?? [];\n return {\n userId: params.userId,\n email: params.email,\n name: params.name ?? null,\n memberships,\n impersonatorId: params.impersonatorId,\n canAccess: (workspaceId) =>\n memberships.some((m) => m.workspaceId === workspaceId && m.workspaceStatus === 'active'),\n hasRole: (workspaceId, role) =>\n memberships.some(\n (m) => m.workspaceId === workspaceId && m.workspaceStatus === 'active' && m.role === role,\n ),\n };\n}\n\nexport type ProvisionerContext =\n | {provisionerTokenId: string; scope: 'installation'}\n | {provisionerTokenId: string; scope: 'workspace'; workspaceId: string};\n\nexport type LeasedJobContext = JobLeaseTokenClaims;\nexport type RunnerSessionContext = RunnerSessionTokenClaims;\nexport interface RunnerControlSessionContext {\n runnerControlSessionId: string;\n runnerInstanceId: string;\n provisionerId: string;\n}\n\ntype RequestWithContext = object;\n\nconst USER_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/user');\nconst LEASED_JOB_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/leased-job');\nconst PROVISIONER_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/provisioner');\nconst RUNNER_SESSION_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/runner-session');\nconst RUNNER_CONTROL_SESSION_CONTEXT_KEY = Symbol.for(\n '@shipfox/api-auth-context/runner-control-session',\n);\n\nexport function setUserContext(request: RequestWithContext, context: UserContext): void {\n (request as Record<symbol, unknown>)[USER_CONTEXT_KEY] = context;\n}\n\nexport function getUserContext(request: RequestWithContext): UserContext | null {\n return (\n ((request as Record<symbol, unknown>)[USER_CONTEXT_KEY] as UserContext | undefined) ?? null\n );\n}\n\nexport function requireUserContext(request: RequestWithContext): UserContext {\n const context = getUserContext(request);\n if (!context) {\n throw new Error('User context is not available on this request');\n }\n return context;\n}\n\n/**\n * Rejects a request whose user context carries an impersonator (the\n * durable-artefact deny-list). Routes that issue a credential or create a\n * durable grant call this so an impersonated session cannot leave anything\n * behind that outlives its bounded token window. Requires a user context:\n * callers must run it on routes whose auth method has set one.\n */\nexport function rejectImpersonatedSession(request: RequestWithContext): void {\n const context = requireUserContext(request);\n if (context.impersonatorId) {\n throw new ClientError(\n 'Impersonated sessions cannot issue credentials or create durable grants',\n 'impersonation-not-permitted',\n {status: 403},\n );\n }\n}\n\nexport interface RequireWorkspaceAccessParams {\n request: RequestWithContext;\n workspaceId: string;\n}\n\nexport interface RequireWorkspaceAccessResult {\n workspaceId: string;\n userId: string;\n role: WorkspaceRole;\n}\n\n/**\n * Authorizes the request's user for a workspace using only the verified session\n * token. Membership and role come from the token claims, so the check stays\n * stateless and does not read the database on the request path.\n */\nexport function requireWorkspaceAccess(\n params: RequireWorkspaceAccessParams,\n): RequireWorkspaceAccessResult {\n const context = getUserContext(params.request);\n if (!context) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const membership = context.memberships.find((m) => m.workspaceId === params.workspaceId);\n if (!membership) {\n throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});\n }\n\n if (membership.workspaceStatus === 'suspended') {\n throw new ClientError('Workspace is suspended', 'workspace-suspended', {status: 409});\n }\n if (membership.workspaceStatus === 'deleted') {\n throw new ClientError('Workspace is not active', 'workspace-inactive', {status: 403});\n }\n\n return {workspaceId: params.workspaceId, userId: context.userId, role: membership.role};\n}\n\n/**\n * Rejects an impersonated session before any administrator authority is\n * consulted. Every route under an `/admin` prefix runs this guard: a request\n * whose context carries `impersonatorId` receives the same `admin-role-required`\n * failure a role-less actor would, before roles are read, so a target that\n * gains a grant inside the token window still cannot act on it. The check is\n * request-scoped on purpose: the inter-module `requireAdminRole` contract only\n * carries `{userId, minimumRole}` and cannot transport the mark.\n */\nexport function requireAdministrationActor(request: RequestWithContext): void {\n const context = getUserContext(request);\n if (context?.impersonatorId != null) {\n throw new ClientError('Administrator role required', 'admin-role-required', {status: 403});\n }\n}\n\nconst ADMINISTRATION_PREFIX = '/admin';\nconst TRAILING_SLASHES = /\\/+$/;\nconst LEADING_SLASHES = /^\\/+/;\n\nfunction isAdministrationPrefix(prefix: string): boolean {\n return prefix === ADMINISTRATION_PREFIX || prefix.startsWith(`${ADMINISTRATION_PREFIX}/`);\n}\n\n/**\n * Joins a parent prefix and a child prefix or route path the way Fastify\n * resolves the mounted URL: surrounding slashes are trimmed and segments are\n * joined with a single `/`, and a leading slash is added when the joined path\n * is non-empty. Raw concatenation would diverge from Fastify's resolution for\n * slash-less prefixes (`/admin` + `things` -> `/adminthings` while Fastify\n * mounts `/admin/things`) and let a route escape the guard while still\n * mounting under `/admin`.\n */\nfunction joinRoutePath(parent: string, child: string): string {\n if (parent === '') {\n return child.startsWith('/') ? child : `/${child}`;\n }\n if (child === '') {\n return parent;\n }\n return `${parent.replace(TRAILING_SLASHES, '')}/${child.replace(LEADING_SLASHES, '')}`;\n}\n\nfunction adoptAdministrationGuardIn(route: RouteExport, parentPrefix: string): RouteExport {\n if (isRouteGroup(route)) {\n return {\n ...route,\n routes: route.routes.map((child) =>\n adoptAdministrationGuardIn(child, joinRoutePath(parentPrefix, route.prefix)),\n ),\n };\n }\n const effectivePath = joinRoutePath(parentPrefix, route.path);\n if (!isAdministrationPrefix(parentPrefix) && !isAdministrationPrefix(effectivePath)) {\n return route;\n }\n const preHandler: RoutePreHandler[] = [\n (request) => {\n requireAdministrationActor(request);\n return undefined;\n },\n ...(route.preHandler === undefined\n ? []\n : Array.isArray(route.preHandler)\n ? route.preHandler\n : [route.preHandler]),\n ];\n return {...route, preHandler};\n}\n\n/**\n * Positionally adopts the impersonated-session rejection for every route under\n * an `/admin` prefix in the given route tree: the rule is \"every `/admin`\n * route\", so an administration surface added later under the prefix inherits\n * the guard without anyone remembering to attach it. The guard runs before any\n * existing preHandler, so roles are never consulted for an impersonated\n * context — including on role-check-free routes such as the first-owner\n * bootstrap.\n */\nexport function adoptAdministrationActorGuard<T extends RouteExport | RouteExport[]>(routes: T): T {\n if (Array.isArray(routes)) {\n return routes.map((route) => adoptAdministrationGuardIn(route, '')) as T;\n }\n return adoptAdministrationGuardIn(routes, '') as T;\n}\n\n/**\n * Applies the workspace lifecycle gate to a resource that has already been loaded. Missing\n * membership remains resource-shaped 404 to avoid leaking the resource's existence, while\n * suspended and deleted claims retain their stable lifecycle errors.\n */\nexport function requireWorkspaceResourceAccess(params: {\n request: RequestWithContext;\n workspaceId: string;\n notFoundError: ClientError;\n}): RequireWorkspaceAccessResult {\n try {\n return requireWorkspaceAccess(params);\n } catch (error) {\n if (error instanceof ClientError && error.code === 'forbidden') {\n throw params.notFoundError;\n }\n throw error;\n }\n}\n\nexport function setProvisionerContext(\n request: RequestWithContext,\n context: ProvisionerContext,\n): void {\n (request as Record<symbol, unknown>)[PROVISIONER_CONTEXT_KEY] = context;\n}\n\nexport function getProvisionerContext(request: RequestWithContext): ProvisionerContext | null {\n return (\n ((request as Record<symbol, unknown>)[PROVISIONER_CONTEXT_KEY] as\n | ProvisionerContext\n | undefined) ?? null\n );\n}\n\nexport function requireProvisionerContext(request: RequestWithContext): ProvisionerContext {\n const context = getProvisionerContext(request);\n if (!context) {\n throw new Error('Provisioner context is not available on this request');\n }\n return context;\n}\n\nexport function requireWorkspaceProvisionerContext(\n request: RequestWithContext,\n): Extract<ProvisionerContext, {scope: 'workspace'}> {\n const context = requireProvisionerContext(request);\n if (context.scope !== 'workspace') {\n throw new ClientError('Workspace provisioner credential required', 'forbidden', {status: 403});\n }\n return context;\n}\n\nexport function requireInstallationProvisionerContext(\n request: RequestWithContext,\n): Extract<ProvisionerContext, {scope: 'installation'}> {\n const context = requireProvisionerContext(request);\n if (context.scope !== 'installation') {\n throw new ClientError('Installation provisioner credential required', 'forbidden', {\n status: 403,\n });\n }\n return context;\n}\n\nexport function setLeasedJobContext(request: RequestWithContext, context: LeasedJobContext): void {\n (request as Record<symbol, unknown>)[LEASED_JOB_CONTEXT_KEY] = context;\n}\n\nexport function getLeasedJobContext(request: RequestWithContext): LeasedJobContext | null {\n return (\n ((request as Record<symbol, unknown>)[LEASED_JOB_CONTEXT_KEY] as\n | LeasedJobContext\n | undefined) ?? null\n );\n}\n\nexport function requireLeasedJobContext(request: RequestWithContext): LeasedJobContext {\n const context = getLeasedJobContext(request);\n if (!context) {\n throw new Error('Leased job context is not available on this request');\n }\n return context;\n}\n\nexport function setRunnerSessionContext(\n request: RequestWithContext,\n context: RunnerSessionContext,\n): void {\n (request as Record<symbol, unknown>)[RUNNER_SESSION_CONTEXT_KEY] = context;\n}\n\nexport function getRunnerSessionContext(request: RequestWithContext): RunnerSessionContext | null {\n return (\n ((request as Record<symbol, unknown>)[RUNNER_SESSION_CONTEXT_KEY] as\n | RunnerSessionContext\n | undefined) ?? null\n );\n}\n\nexport function requireRunnerSessionContext(request: RequestWithContext): RunnerSessionContext {\n const context = getRunnerSessionContext(request);\n if (!context) {\n throw new Error('Runner session context is not available on this request');\n }\n return context;\n}\n\nexport function setRunnerControlSessionContext(\n request: RequestWithContext,\n context: RunnerControlSessionContext,\n): void {\n (request as Record<symbol, unknown>)[RUNNER_CONTROL_SESSION_CONTEXT_KEY] = context;\n}\n\nexport function requireRunnerControlSessionContext(\n request: RequestWithContext,\n): RunnerControlSessionContext {\n const context = (request as Record<symbol, unknown>)[RUNNER_CONTROL_SESSION_CONTEXT_KEY] as\n | RunnerControlSessionContext\n | undefined;\n if (!context) throw new Error('Runner control session context is not available on this request');\n return context;\n}\n"],"names":["ClientError","isRouteGroup","AUTH_USER","AUTH_RUNNER_REGISTRATION_TOKEN","AUTH_RUNNER_SESSION","AUTH_RUNNER_CONTROL_SESSION","AUTH_LEASED_JOB","AUTH_PROVISIONER_TOKEN","buildUserContext","params","memberships","userId","email","name","impersonatorId","canAccess","workspaceId","some","m","workspaceStatus","hasRole","role","USER_CONTEXT_KEY","Symbol","for","LEASED_JOB_CONTEXT_KEY","PROVISIONER_CONTEXT_KEY","RUNNER_SESSION_CONTEXT_KEY","RUNNER_CONTROL_SESSION_CONTEXT_KEY","setUserContext","request","context","getUserContext","requireUserContext","Error","rejectImpersonatedSession","status","requireWorkspaceAccess","membership","find","requireAdministrationActor","ADMINISTRATION_PREFIX","TRAILING_SLASHES","LEADING_SLASHES","isAdministrationPrefix","prefix","startsWith","joinRoutePath","parent","child","replace","adoptAdministrationGuardIn","route","parentPrefix","routes","map","effectivePath","path","preHandler","undefined","Array","isArray","adoptAdministrationActorGuard","requireWorkspaceResourceAccess","error","code","notFoundError","setProvisionerContext","getProvisionerContext","requireProvisionerContext","requireWorkspaceProvisionerContext","scope","requireInstallationProvisionerContext","setLeasedJobContext","getLeasedJobContext","requireLeasedJobContext","setRunnerSessionContext","getRunnerSessionContext","requireRunnerSessionContext","setRunnerControlSessionContext","requireRunnerControlSessionContext"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {JobLeaseTokenClaims, RunnerSessionTokenClaims} from '@shipfox/api-auth-dto';\nimport type {WorkspaceRole} from '@shipfox/api-workspaces-dto';\nimport {\n ClientError,\n isRouteGroup,\n type RouteExport,\n type RoutePreHandler,\n} from '@shipfox/node-fastify';\n\nexport const AUTH_USER = 'user';\nexport const AUTH_RUNNER_REGISTRATION_TOKEN = 'runner-registration-token';\nexport const AUTH_RUNNER_SESSION = 'runner-session';\nexport const AUTH_RUNNER_CONTROL_SESSION = 'runner-control-session';\nexport const AUTH_LEASED_JOB = 'leased-job';\nexport const AUTH_PROVISIONER_TOKEN = 'provisioner-token';\n\nexport type WorkspaceStatus = 'active' | 'suspended' | 'deleted';\n\nexport interface UserContextMembership {\n workspaceId: string;\n role: WorkspaceRole;\n workspaceStatus: WorkspaceStatus;\n}\n\nexport interface UserContext {\n userId: string;\n email: string;\n name: string | null;\n memberships: ReadonlyArray<UserContextMembership>;\n impersonatorId?: string | undefined;\n canAccess(workspaceId: string): boolean;\n hasRole(workspaceId: string, role: WorkspaceRole): boolean;\n}\n\nexport interface BuildUserContextParams {\n userId: string;\n email: string;\n name?: string | null | undefined;\n memberships?: ReadonlyArray<UserContextMembership> | undefined;\n impersonatorId?: string | undefined;\n}\n\nexport function buildUserContext(params: BuildUserContextParams): UserContext {\n const memberships = params.memberships ?? [];\n return {\n userId: params.userId,\n email: params.email,\n name: params.name ?? null,\n memberships,\n impersonatorId: params.impersonatorId,\n canAccess: (workspaceId) =>\n memberships.some((m) => m.workspaceId === workspaceId && m.workspaceStatus === 'active'),\n hasRole: (workspaceId, role) =>\n memberships.some(\n (m) => m.workspaceId === workspaceId && m.workspaceStatus === 'active' && m.role === role,\n ),\n };\n}\n\nexport type ProvisionerContext =\n | {provisionerTokenId: string; scope: 'installation'}\n | {provisionerTokenId: string; scope: 'workspace'; workspaceId: string};\n\nexport type LeasedJobContext = JobLeaseTokenClaims;\nexport type RunnerSessionContext = RunnerSessionTokenClaims;\nexport interface RunnerControlSessionContext {\n runnerControlSessionId: string;\n runnerInstanceId: string;\n provisionerId: string;\n}\n\ntype RequestWithContext = object;\n\nconst USER_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/user');\nconst LEASED_JOB_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/leased-job');\nconst PROVISIONER_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/provisioner');\nconst RUNNER_SESSION_CONTEXT_KEY = Symbol.for('@shipfox/api-auth-context/runner-session');\nconst RUNNER_CONTROL_SESSION_CONTEXT_KEY = Symbol.for(\n '@shipfox/api-auth-context/runner-control-session',\n);\n\nexport function setUserContext(request: RequestWithContext, context: UserContext): void {\n (request as Record<symbol, unknown>)[USER_CONTEXT_KEY] = context;\n}\n\nexport function getUserContext(request: RequestWithContext): UserContext | null {\n return (\n ((request as Record<symbol, unknown>)[USER_CONTEXT_KEY] as UserContext | undefined) ?? null\n );\n}\n\nexport function requireUserContext(request: RequestWithContext): UserContext {\n const context = getUserContext(request);\n if (!context) {\n throw new Error('User context is not available on this request');\n }\n return context;\n}\n\n/**\n * Rejects a request whose user context carries an impersonator (the\n * durable-artefact deny-list). Routes that issue a credential or create a\n * durable grant call this so an impersonated session cannot leave anything\n * behind that outlives its bounded token window. Requires a user context:\n * callers must run it on routes whose auth method has set one.\n */\nexport function rejectImpersonatedSession(request: RequestWithContext): void {\n const context = requireUserContext(request);\n if (context.impersonatorId) {\n throw new ClientError(\n 'Impersonated sessions cannot issue credentials or create durable grants',\n 'impersonation-not-permitted',\n {status: 403},\n );\n }\n}\n\nexport interface RequireWorkspaceAccessParams {\n request: RequestWithContext;\n workspaceId: string;\n}\n\nexport interface RequireWorkspaceAccessResult {\n workspaceId: string;\n userId: string;\n role: WorkspaceRole;\n}\n\n/**\n * Authorizes the request's user for a workspace using only the verified session\n * token. Membership and role come from the token claims, so the check stays\n * stateless and does not read the database on the request path.\n */\nexport function requireWorkspaceAccess(\n params: RequireWorkspaceAccessParams,\n): RequireWorkspaceAccessResult {\n const context = getUserContext(params.request);\n if (!context) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const membership = context.memberships.find((m) => m.workspaceId === params.workspaceId);\n if (!membership) {\n throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});\n }\n\n if (membership.workspaceStatus === 'suspended') {\n throw new ClientError('Workspace is suspended', 'workspace-suspended', {status: 409});\n }\n if (membership.workspaceStatus === 'deleted') {\n throw new ClientError('Workspace is not active', 'workspace-inactive', {status: 403});\n }\n\n return {workspaceId: params.workspaceId, userId: context.userId, role: membership.role};\n}\n\n/**\n * Rejects an impersonated session before any administrator authority is\n * consulted. Every route under an `/admin` prefix runs this guard: a request\n * whose context carries `impersonatorId` receives the same `admin-role-required`\n * failure a role-less actor would, before roles are read, so a target that\n * gains a grant inside the token window still cannot act on it. The check is\n * request-scoped on purpose: the inter-module `requireAdminRole` contract only\n * carries `{userId, minimumRole}` and cannot transport the mark.\n */\nexport function requireAdministrationActor(request: RequestWithContext): void {\n const context = getUserContext(request);\n if (context?.impersonatorId != null) {\n throw new ClientError('Administrator role required', 'admin-role-required', {status: 403});\n }\n}\n\nconst ADMINISTRATION_PREFIX = '/admin';\nconst TRAILING_SLASHES = /\\/+$/;\nconst LEADING_SLASHES = /^\\/+/;\n\nfunction isAdministrationPrefix(prefix: string): boolean {\n return prefix === ADMINISTRATION_PREFIX || prefix.startsWith(`${ADMINISTRATION_PREFIX}/`);\n}\n\n/**\n * Joins a parent prefix and a child prefix or route path the way Fastify\n * resolves the mounted URL: surrounding slashes are trimmed and segments are\n * joined with a single `/`, and a leading slash is added when the joined path\n * is non-empty. Raw concatenation would diverge from Fastify's resolution for\n * slash-less prefixes (`/admin` + `things` -> `/adminthings` while Fastify\n * mounts `/admin/things`) and let a route escape the guard while still\n * mounting under `/admin`.\n */\nfunction joinRoutePath(parent: string, child: string): string {\n if (parent === '') {\n return child.startsWith('/') ? child : `/${child}`;\n }\n if (child === '') {\n return parent;\n }\n return `${parent.replace(TRAILING_SLASHES, '')}/${child.replace(LEADING_SLASHES, '')}`;\n}\n\nfunction routePreHandlers(\n preHandler: RoutePreHandler | RoutePreHandler[] | undefined,\n): RoutePreHandler[] {\n if (preHandler === undefined) return [];\n if (Array.isArray(preHandler)) return preHandler;\n return [preHandler];\n}\n\nfunction adoptAdministrationGuardIn(route: RouteExport, parentPrefix: string): RouteExport {\n if (isRouteGroup(route)) {\n return {\n ...route,\n routes: route.routes.map((child) =>\n adoptAdministrationGuardIn(child, joinRoutePath(parentPrefix, route.prefix)),\n ),\n };\n }\n const effectivePath = joinRoutePath(parentPrefix, route.path);\n if (!isAdministrationPrefix(parentPrefix) && !isAdministrationPrefix(effectivePath)) {\n return route;\n }\n const preHandler: RoutePreHandler[] = [\n (request) => {\n requireAdministrationActor(request);\n return undefined;\n },\n ...routePreHandlers(route.preHandler),\n ];\n return {...route, preHandler};\n}\n\n/**\n * Positionally adopts the impersonated-session rejection for every route under\n * an `/admin` prefix in the given route tree: the rule is \"every `/admin`\n * route\", so an administration surface added later under the prefix inherits\n * the guard without anyone remembering to attach it. The guard runs before any\n * existing preHandler, so roles are never consulted for an impersonated\n * context — including on role-check-free routes such as the first-owner\n * bootstrap.\n */\nexport function adoptAdministrationActorGuard<T extends RouteExport | RouteExport[]>(routes: T): T {\n if (Array.isArray(routes)) {\n return routes.map((route) => adoptAdministrationGuardIn(route, '')) as T;\n }\n return adoptAdministrationGuardIn(routes, '') as T;\n}\n\n/**\n * Applies the workspace lifecycle gate to a resource that has already been loaded. Missing\n * membership remains resource-shaped 404 to avoid leaking the resource's existence, while\n * suspended and deleted claims retain their stable lifecycle errors.\n */\nexport function requireWorkspaceResourceAccess(params: {\n request: RequestWithContext;\n workspaceId: string;\n notFoundError: ClientError;\n}): RequireWorkspaceAccessResult {\n try {\n return requireWorkspaceAccess(params);\n } catch (error) {\n if (error instanceof ClientError && error.code === 'forbidden') {\n throw params.notFoundError;\n }\n throw error;\n }\n}\n\nexport function setProvisionerContext(\n request: RequestWithContext,\n context: ProvisionerContext,\n): void {\n (request as Record<symbol, unknown>)[PROVISIONER_CONTEXT_KEY] = context;\n}\n\nexport function getProvisionerContext(request: RequestWithContext): ProvisionerContext | null {\n return (\n ((request as Record<symbol, unknown>)[PROVISIONER_CONTEXT_KEY] as\n | ProvisionerContext\n | undefined) ?? null\n );\n}\n\nexport function requireProvisionerContext(request: RequestWithContext): ProvisionerContext {\n const context = getProvisionerContext(request);\n if (!context) {\n throw new Error('Provisioner context is not available on this request');\n }\n return context;\n}\n\nexport function requireWorkspaceProvisionerContext(\n request: RequestWithContext,\n): Extract<ProvisionerContext, {scope: 'workspace'}> {\n const context = requireProvisionerContext(request);\n if (context.scope !== 'workspace') {\n throw new ClientError('Workspace provisioner credential required', 'forbidden', {status: 403});\n }\n return context;\n}\n\nexport function requireInstallationProvisionerContext(\n request: RequestWithContext,\n): Extract<ProvisionerContext, {scope: 'installation'}> {\n const context = requireProvisionerContext(request);\n if (context.scope !== 'installation') {\n throw new ClientError('Installation provisioner credential required', 'forbidden', {\n status: 403,\n });\n }\n return context;\n}\n\nexport function setLeasedJobContext(request: RequestWithContext, context: LeasedJobContext): void {\n (request as Record<symbol, unknown>)[LEASED_JOB_CONTEXT_KEY] = context;\n}\n\nexport function getLeasedJobContext(request: RequestWithContext): LeasedJobContext | null {\n return (\n ((request as Record<symbol, unknown>)[LEASED_JOB_CONTEXT_KEY] as\n | LeasedJobContext\n | undefined) ?? null\n );\n}\n\nexport function requireLeasedJobContext(request: RequestWithContext): LeasedJobContext {\n const context = getLeasedJobContext(request);\n if (!context) {\n throw new Error('Leased job context is not available on this request');\n }\n return context;\n}\n\nexport function setRunnerSessionContext(\n request: RequestWithContext,\n context: RunnerSessionContext,\n): void {\n (request as Record<symbol, unknown>)[RUNNER_SESSION_CONTEXT_KEY] = context;\n}\n\nexport function getRunnerSessionContext(request: RequestWithContext): RunnerSessionContext | null {\n return (\n ((request as Record<symbol, unknown>)[RUNNER_SESSION_CONTEXT_KEY] as\n | RunnerSessionContext\n | undefined) ?? null\n );\n}\n\nexport function requireRunnerSessionContext(request: RequestWithContext): RunnerSessionContext {\n const context = getRunnerSessionContext(request);\n if (!context) {\n throw new Error('Runner session context is not available on this request');\n }\n return context;\n}\n\nexport function setRunnerControlSessionContext(\n request: RequestWithContext,\n context: RunnerControlSessionContext,\n): void {\n (request as Record<symbol, unknown>)[RUNNER_CONTROL_SESSION_CONTEXT_KEY] = context;\n}\n\nexport function requireRunnerControlSessionContext(\n request: RequestWithContext,\n): RunnerControlSessionContext {\n const context = (request as Record<symbol, unknown>)[RUNNER_CONTROL_SESSION_CONTEXT_KEY] as\n | RunnerControlSessionContext\n | undefined;\n if (!context) throw new Error('Runner control session context is not available on this request');\n return context;\n}\n"],"names":["ClientError","isRouteGroup","AUTH_USER","AUTH_RUNNER_REGISTRATION_TOKEN","AUTH_RUNNER_SESSION","AUTH_RUNNER_CONTROL_SESSION","AUTH_LEASED_JOB","AUTH_PROVISIONER_TOKEN","buildUserContext","params","memberships","userId","email","name","impersonatorId","canAccess","workspaceId","some","m","workspaceStatus","hasRole","role","USER_CONTEXT_KEY","Symbol","for","LEASED_JOB_CONTEXT_KEY","PROVISIONER_CONTEXT_KEY","RUNNER_SESSION_CONTEXT_KEY","RUNNER_CONTROL_SESSION_CONTEXT_KEY","setUserContext","request","context","getUserContext","requireUserContext","Error","rejectImpersonatedSession","status","requireWorkspaceAccess","membership","find","requireAdministrationActor","ADMINISTRATION_PREFIX","TRAILING_SLASHES","LEADING_SLASHES","isAdministrationPrefix","prefix","startsWith","joinRoutePath","parent","child","replace","routePreHandlers","preHandler","undefined","Array","isArray","adoptAdministrationGuardIn","route","parentPrefix","routes","map","effectivePath","path","adoptAdministrationActorGuard","requireWorkspaceResourceAccess","error","code","notFoundError","setProvisionerContext","getProvisionerContext","requireProvisionerContext","requireWorkspaceProvisionerContext","scope","requireInstallationProvisionerContext","setLeasedJobContext","getLeasedJobContext","requireLeasedJobContext","setRunnerSessionContext","getRunnerSessionContext","requireRunnerSessionContext","setRunnerControlSessionContext","requireRunnerControlSessionContext"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-auth-context",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "19.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@shipfox/api-auth-dto": "
|
|
21
|
+
"@shipfox/api-auth-dto": "19.0.0",
|
|
22
22
|
"@shipfox/api-workspaces-dto": "15.0.0",
|
|
23
23
|
"@shipfox/node-fastify": "0.4.3"
|
|
24
24
|
},
|
package/src/index.ts
CHANGED
|
@@ -197,6 +197,14 @@ function joinRoutePath(parent: string, child: string): string {
|
|
|
197
197
|
return `${parent.replace(TRAILING_SLASHES, '')}/${child.replace(LEADING_SLASHES, '')}`;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
function routePreHandlers(
|
|
201
|
+
preHandler: RoutePreHandler | RoutePreHandler[] | undefined,
|
|
202
|
+
): RoutePreHandler[] {
|
|
203
|
+
if (preHandler === undefined) return [];
|
|
204
|
+
if (Array.isArray(preHandler)) return preHandler;
|
|
205
|
+
return [preHandler];
|
|
206
|
+
}
|
|
207
|
+
|
|
200
208
|
function adoptAdministrationGuardIn(route: RouteExport, parentPrefix: string): RouteExport {
|
|
201
209
|
if (isRouteGroup(route)) {
|
|
202
210
|
return {
|
|
@@ -215,11 +223,7 @@ function adoptAdministrationGuardIn(route: RouteExport, parentPrefix: string): R
|
|
|
215
223
|
requireAdministrationActor(request);
|
|
216
224
|
return undefined;
|
|
217
225
|
},
|
|
218
|
-
...(route.preHandler
|
|
219
|
-
? []
|
|
220
|
-
: Array.isArray(route.preHandler)
|
|
221
|
-
? route.preHandler
|
|
222
|
-
: [route.preHandler]),
|
|
226
|
+
...routePreHandlers(route.preHandler),
|
|
223
227
|
];
|
|
224
228
|
return {...route, preHandler};
|
|
225
229
|
}
|