@jskit-ai/auth-core 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/package.descriptor.mjs +95 -0
  2. package/package.json +51 -0
  3. package/src/client/authApi.js +1 -0
  4. package/src/client/index.js +2 -0
  5. package/src/client/providers/AccessCoreClientProvider.js +23 -0
  6. package/src/client/providers/FastifyAuthPolicyClientProvider.js +13 -0
  7. package/src/client/signOutFlow.js +1 -0
  8. package/src/server/inviteTokens.js +41 -0
  9. package/src/server/lib/actionContextContributor.js +36 -0
  10. package/src/server/lib/authPolicySupport.js +38 -0
  11. package/src/server/lib/errors.js +20 -0
  12. package/src/server/lib/index.js +3 -0
  13. package/src/server/lib/objectUtils.js +5 -0
  14. package/src/server/lib/plugin.js +247 -0
  15. package/src/server/lib/routeMeta.js +64 -0
  16. package/src/server/lib/routeVisibilityResolver.js +25 -0
  17. package/src/server/lib/tokens.js +3 -0
  18. package/src/server/membershipAccess.js +67 -0
  19. package/src/server/providers/AccessCoreServiceProvider.js +35 -0
  20. package/src/server/providers/FastifyAuthPolicyServiceProvider.js +124 -0
  21. package/src/server/utils.js +26 -0
  22. package/src/server/validators.js +183 -0
  23. package/src/shared/authApi.js +50 -0
  24. package/src/shared/authConstraints.js +13 -0
  25. package/src/shared/authMethods.js +170 -0
  26. package/src/shared/authPaths.js +24 -0
  27. package/src/shared/commands/authCommandValidators.js +255 -0
  28. package/src/shared/commands/authLoginOAuthCompleteCommand.js +68 -0
  29. package/src/shared/commands/authLoginOAuthStartCommand.js +72 -0
  30. package/src/shared/commands/authLoginOtpRequestCommand.js +56 -0
  31. package/src/shared/commands/authLoginOtpVerifyCommand.js +64 -0
  32. package/src/shared/commands/authLoginPasswordCommand.js +57 -0
  33. package/src/shared/commands/authLogoutCommand.js +23 -0
  34. package/src/shared/commands/authPasswordRecoveryCompleteCommand.js +67 -0
  35. package/src/shared/commands/authPasswordResetCommand.js +49 -0
  36. package/src/shared/commands/authPasswordResetRequestCommand.js +50 -0
  37. package/src/shared/commands/authRegisterCommand.js +57 -0
  38. package/src/shared/commands/authSessionReadCommand.js +26 -0
  39. package/src/shared/index.js +3 -0
  40. package/src/shared/inputNormalization.js +1 -0
  41. package/src/shared/inviteTokens.js +38 -0
  42. package/src/shared/oauthCallbackParams.js +5 -0
  43. package/src/shared/oauthProviders.js +66 -0
  44. package/src/shared/signOutFlow.js +28 -0
  45. package/test/actionContextContributor.test.js +44 -0
  46. package/test/authApi.test.js +47 -0
  47. package/test/authMethods.test.js +95 -0
  48. package/test/authPaths.test.js +17 -0
  49. package/test/commandValidators.test.js +33 -0
  50. package/test/plugin.test.js +250 -0
  51. package/test/providerRuntime.test.js +114 -0
  52. package/test/routeMeta.test.js +95 -0
  53. package/test/routeVisibilityResolver.test.js +34 -0
  54. package/test/serverUtils.test.js +28 -0
  55. package/test/signOutFlow.test.js +67 -0
@@ -0,0 +1,95 @@
1
+ export default Object.freeze({
2
+ "packageVersion": 1,
3
+ "packageId": "@jskit-ai/auth-core",
4
+ "version": "0.1.4",
5
+ "dependsOn": [
6
+ "@jskit-ai/value-app-config-shared"
7
+ ],
8
+ "capabilities": {
9
+ "provides": [
10
+ "auth.access",
11
+ "auth.policy"
12
+ ],
13
+ "requires": []
14
+ },
15
+ "runtime": {
16
+ "server": {
17
+ "providerEntrypoint": "src/server/providers/AccessCoreServiceProvider.js",
18
+ "providers": [
19
+ {
20
+ "entrypoint": "src/server/providers/AccessCoreServiceProvider.js",
21
+ "export": "AccessCoreServiceProvider"
22
+ },
23
+ {
24
+ "entrypoint": "src/server/providers/FastifyAuthPolicyServiceProvider.js",
25
+ "export": "FastifyAuthPolicyServiceProvider"
26
+ }
27
+ ]
28
+ },
29
+ "client": {
30
+ "providers": [
31
+ {
32
+ "entrypoint": "src/client/providers/AccessCoreClientProvider.js",
33
+ "export": "AccessCoreClientProvider"
34
+ },
35
+ {
36
+ "entrypoint": "src/client/providers/FastifyAuthPolicyClientProvider.js",
37
+ "export": "FastifyAuthPolicyClientProvider"
38
+ }
39
+ ]
40
+ }
41
+ },
42
+ "metadata": {
43
+ "apiSummary": {
44
+ "surfaces": [
45
+ {
46
+ "subpath": "./client",
47
+ "summary": "Exports client auth access APIs plus AccessCoreClientProvider/FastifyAuthPolicyClientProvider."
48
+ },
49
+ {
50
+ "subpath": "./server",
51
+ "summary": "Exports server auth access and Fastify auth policy providers plus server auth utility modules."
52
+ },
53
+ {
54
+ "subpath": "./shared",
55
+ "summary": "Exports shared auth client helpers (createApi and runAuthSignOutFlow), with structured subpaths at ./shared/authApi and ./shared/signOutFlow."
56
+ }
57
+ ],
58
+ "containerTokens": {
59
+ "server": [
60
+ "auth.access"
61
+ ],
62
+ "client": [
63
+ "auth.access.client"
64
+ ]
65
+ }
66
+ }
67
+ },
68
+ "mutations": {
69
+ "dependencies": {
70
+ "runtime": {
71
+ "@jskit-ai/kernel": "0.1.4",
72
+ "@fastify/cookie": "^11.0.2",
73
+ "@fastify/csrf-protection": "^7.1.0",
74
+ "@fastify/rate-limit": "^10.3.0"
75
+ },
76
+ "dev": {}
77
+ },
78
+ "packageJson": {
79
+ "scripts": {}
80
+ },
81
+ "procfile": {},
82
+ "files": [],
83
+ "text": [
84
+ {
85
+ "file": ".env",
86
+ "op": "upsert-env",
87
+ "key": "AUTH_PROFILE_MODE",
88
+ "value": "standalone",
89
+ "reason": "Default auth profile mode to standalone when users-core is not installed.",
90
+ "category": "runtime-config",
91
+ "id": "auth-profile-mode"
92
+ }
93
+ ]
94
+ }
95
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@jskit-ai/auth-core",
3
+ "version": "0.1.4",
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ },
8
+ "exports": {
9
+ "./server/providers/AccessCoreServiceProvider": "./src/server/providers/AccessCoreServiceProvider.js",
10
+ "./server/providers/FastifyAuthPolicyServiceProvider": "./src/server/providers/FastifyAuthPolicyServiceProvider.js",
11
+ "./server/utils": "./src/server/utils.js",
12
+ "./server/validators": "./src/server/validators.js",
13
+ "./server/inviteTokens": "./src/server/inviteTokens.js",
14
+ "./server/membershipAccess": "./src/server/membershipAccess.js",
15
+ "./server/lib/index": "./src/server/lib/index.js",
16
+ "./server/lib/plugin": "./src/server/lib/plugin.js",
17
+ "./server/lib/routeMeta": "./src/server/lib/routeMeta.js",
18
+ "./server/lib/tokens": "./src/server/lib/tokens.js",
19
+ "./client": "./src/client/index.js",
20
+ "./client/authApi": "./src/client/authApi.js",
21
+ "./client/signOutFlow": "./src/client/signOutFlow.js",
22
+ "./shared": "./src/shared/index.js",
23
+ "./shared/authApi": "./src/shared/authApi.js",
24
+ "./shared/signOutFlow": "./src/shared/signOutFlow.js",
25
+ "./shared/authPaths": "./src/shared/authPaths.js",
26
+ "./shared/authConstraints": "./src/shared/authConstraints.js",
27
+ "./shared/authMethods": "./src/shared/authMethods.js",
28
+ "./shared/oauthProviders": "./src/shared/oauthProviders.js",
29
+ "./shared/oauthCallbackParams": "./src/shared/oauthCallbackParams.js",
30
+ "./shared/inputNormalization": "./src/shared/inputNormalization.js",
31
+ "./shared/inviteTokens": "./src/shared/inviteTokens.js",
32
+ "./shared/commands/authRegisterCommand": "./src/shared/commands/authRegisterCommand.js",
33
+ "./shared/commands/authLoginPasswordCommand": "./src/shared/commands/authLoginPasswordCommand.js",
34
+ "./shared/commands/authLoginOtpRequestCommand": "./src/shared/commands/authLoginOtpRequestCommand.js",
35
+ "./shared/commands/authLoginOtpVerifyCommand": "./src/shared/commands/authLoginOtpVerifyCommand.js",
36
+ "./shared/commands/authLoginOAuthStartCommand": "./src/shared/commands/authLoginOAuthStartCommand.js",
37
+ "./shared/commands/authLoginOAuthCompleteCommand": "./src/shared/commands/authLoginOAuthCompleteCommand.js",
38
+ "./shared/commands/authPasswordResetRequestCommand": "./src/shared/commands/authPasswordResetRequestCommand.js",
39
+ "./shared/commands/authPasswordRecoveryCompleteCommand": "./src/shared/commands/authPasswordRecoveryCompleteCommand.js",
40
+ "./shared/commands/authPasswordResetCommand": "./src/shared/commands/authPasswordResetCommand.js",
41
+ "./shared/commands/authLogoutCommand": "./src/shared/commands/authLogoutCommand.js",
42
+ "./shared/commands/authSessionReadCommand": "./src/shared/commands/authSessionReadCommand.js"
43
+ },
44
+ "dependencies": {
45
+ "@jskit-ai/kernel": "0.1.4",
46
+ "@fastify/cookie": "^11.0.2",
47
+ "@fastify/csrf-protection": "^7.1.0",
48
+ "@fastify/rate-limit": "^10.3.0",
49
+ "typebox": "^1.0.81"
50
+ }
51
+ }
@@ -0,0 +1 @@
1
+ export { createApi } from "../shared/authApi.js";
@@ -0,0 +1,2 @@
1
+ export { AccessCoreClientProvider } from "./providers/AccessCoreClientProvider.js";
2
+ export { FastifyAuthPolicyClientProvider } from "./providers/FastifyAuthPolicyClientProvider.js";
@@ -0,0 +1,23 @@
1
+ import { createApi as createAuthApi } from "../authApi.js";
2
+ import { runAuthSignOutFlow } from "../signOutFlow.js";
3
+
4
+ const CLIENT_API = Object.freeze({
5
+ createAuthApi,
6
+ runAuthSignOutFlow
7
+ });
8
+
9
+ class AccessCoreClientProvider {
10
+ static id = "auth.access.client";
11
+
12
+ register(app) {
13
+ if (!app || typeof app.singleton !== "function") {
14
+ throw new Error("AccessCoreClientProvider requires application singleton().");
15
+ }
16
+
17
+ app.singleton("auth.access.client", () => CLIENT_API);
18
+ }
19
+
20
+ boot() {}
21
+ }
22
+
23
+ export { AccessCoreClientProvider };
@@ -0,0 +1,13 @@
1
+ class FastifyAuthPolicyClientProvider {
2
+ static id = "auth.policy.client";
3
+
4
+ register(app) {
5
+ if (!app || typeof app.singleton !== "function") {
6
+ throw new Error("FastifyAuthPolicyClientProvider requires application singleton().");
7
+ }
8
+ }
9
+
10
+ boot() {}
11
+ }
12
+
13
+ export { FastifyAuthPolicyClientProvider };
@@ -0,0 +1 @@
1
+ export { runAuthSignOutFlow } from "../shared/signOutFlow.js";
@@ -0,0 +1,41 @@
1
+ import crypto from "node:crypto";
2
+ import {
3
+ OPAQUE_INVITE_TOKEN_HASH_PREFIX,
4
+ normalizeInviteToken,
5
+ isSha256Hex,
6
+ encodeInviteTokenHash,
7
+ decodeInviteTokenHash
8
+ } from "../shared/inviteTokens.js";
9
+
10
+ function buildInviteToken() {
11
+ return crypto.randomBytes(24).toString("hex");
12
+ }
13
+
14
+ function hashInviteToken(token) {
15
+ return crypto.createHash("sha256").update(normalizeInviteToken(token)).digest("hex");
16
+ }
17
+
18
+ function resolveInviteTokenHash(inviteToken) {
19
+ const normalizedToken = normalizeInviteToken(inviteToken);
20
+ if (!normalizedToken) {
21
+ return "";
22
+ }
23
+
24
+ const decodedTokenHash = decodeInviteTokenHash(normalizedToken);
25
+ if (decodedTokenHash) {
26
+ return decodedTokenHash;
27
+ }
28
+
29
+ return hashInviteToken(normalizedToken);
30
+ }
31
+
32
+ export {
33
+ OPAQUE_INVITE_TOKEN_HASH_PREFIX,
34
+ normalizeInviteToken,
35
+ isSha256Hex,
36
+ buildInviteToken,
37
+ hashInviteToken,
38
+ encodeInviteTokenHash,
39
+ decodeInviteTokenHash,
40
+ resolveInviteTokenHash
41
+ };
@@ -0,0 +1,36 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ function normalizePermissions(value) {
4
+ const source = Array.isArray(value) ? value : [];
5
+ return source.map((entry) => normalizeText(entry)).filter(Boolean);
6
+ }
7
+
8
+ function createAuthActionContextContributor() {
9
+ return Object.freeze({
10
+ contributorId: "auth.policy.request-context",
11
+ contribute({ request } = {}) {
12
+ const contribution = {};
13
+ const permissions = normalizePermissions(request?.permissions);
14
+
15
+ if (request?.user) {
16
+ contribution.actor = request.user;
17
+ }
18
+
19
+ if (request?.workspace) {
20
+ contribution.workspace = request.workspace;
21
+ }
22
+
23
+ if (request?.membership) {
24
+ contribution.membership = request.membership;
25
+ }
26
+
27
+ if (permissions.length > 0) {
28
+ contribution.permissions = permissions;
29
+ }
30
+
31
+ return contribution;
32
+ }
33
+ });
34
+ }
35
+
36
+ export { createAuthActionContextContributor };
@@ -0,0 +1,38 @@
1
+ import { asObject } from "./objectUtils.js";
2
+
3
+ function assertFunction(value, name) {
4
+ if (typeof value !== "function") {
5
+ throw new Error(`${name} is required.`);
6
+ }
7
+ return value;
8
+ }
9
+
10
+ function assertAuthPolicyDeps(deps = {}) {
11
+ const source = asObject(deps);
12
+ const resolveActor = assertFunction(source.resolveActor, "resolveActor");
13
+ const hasPermission = assertFunction(source.hasPermission, "hasPermission");
14
+
15
+ return {
16
+ resolveActor,
17
+ resolveContext: typeof source.resolveContext === "function" ? source.resolveContext : null,
18
+ hasPermission,
19
+ onPolicyDenied: typeof source.onPolicyDenied === "function" ? source.onPolicyDenied : null
20
+ };
21
+ }
22
+
23
+ function normalizeActorResolution(result) {
24
+ const source = asObject(result);
25
+ const actor = Object.hasOwn(source, "actor")
26
+ ? source.actor
27
+ : Object.hasOwn(source, "profile")
28
+ ? source.profile
29
+ : null;
30
+
31
+ return {
32
+ authenticated: Boolean(source.authenticated),
33
+ actor,
34
+ transientFailure: Boolean(source.transientFailure)
35
+ };
36
+ }
37
+
38
+ export { assertAuthPolicyDeps, normalizeActorResolution };
@@ -0,0 +1,20 @@
1
+ const AUTH_POLICY_DENY_REASONS = Object.freeze({
2
+ AUTH_UPSTREAM_UNAVAILABLE: "auth_upstream_unavailable",
3
+ UNAUTHENTICATED: "unauthenticated",
4
+ OWNER_UNRESOLVED: "owner_unresolved",
5
+ FORBIDDEN_OWNER_MISMATCH: "forbidden_owner_mismatch",
6
+ INVALID_AUTH_POLICY: "invalid_auth_policy",
7
+ FORBIDDEN_PERMISSION: "forbidden_permission"
8
+ });
9
+
10
+ function createAuthPolicyError(status, message, { code = "AUTH_POLICY_ERROR" } = {}) {
11
+ const normalizedStatus = Number(status) || 500;
12
+ const error = new Error(String(message || "Request failed."));
13
+ error.name = "AuthPolicyError";
14
+ error.status = normalizedStatus;
15
+ error.statusCode = normalizedStatus;
16
+ error.code = String(code || "AUTH_POLICY_ERROR");
17
+ return error;
18
+ }
19
+
20
+ export { AUTH_POLICY_DENY_REASONS, createAuthPolicyError };
@@ -0,0 +1,3 @@
1
+ export { authPolicyPlugin } from "./plugin.js";
2
+ export { withAuthPolicy, mergeAuthPolicy } from "./routeMeta.js";
3
+ export { AUTH_POLICY_CONTEXT_RESOLVER_TOKEN } from "./tokens.js";
@@ -0,0 +1,5 @@
1
+ function asObject(value) {
2
+ return value && typeof value === "object" ? value : {};
3
+ }
4
+
5
+ export { asObject };
@@ -0,0 +1,247 @@
1
+ import fastifyCookie from "@fastify/cookie";
2
+ import fastifyCsrfProtection from "@fastify/csrf-protection";
3
+ import fastifyRateLimit from "@fastify/rate-limit";
4
+ import { safeRequestUrl } from "@jskit-ai/kernel/server/runtime/requestUrl";
5
+
6
+ import { assertAuthPolicyDeps, normalizeActorResolution } from "./authPolicySupport.js";
7
+ import { AUTH_POLICY_DENY_REASONS, createAuthPolicyError } from "./errors.js";
8
+ import { AUTH_POLICIES, CONTEXT_POLICIES, resolveAuthPolicyMeta } from "./routeMeta.js";
9
+
10
+ const DEFAULT_API_PREFIX = "/api/";
11
+ const DEFAULT_RATE_LIMIT_PLUGIN_OPTIONS = Object.freeze({
12
+ global: false
13
+ });
14
+ const DEFAULT_UNSAFE_METHODS = Object.freeze(["POST", "PUT", "PATCH", "DELETE"]);
15
+
16
+ function normalizeUnsafeMethods(methodsValue) {
17
+ const source = Array.isArray(methodsValue) ? methodsValue : DEFAULT_UNSAFE_METHODS;
18
+ return new Set(source.map((method) => String(method || "").trim().toUpperCase()).filter(Boolean));
19
+ }
20
+
21
+ function normalizeApiPrefix(value) {
22
+ const normalized = String(value || "").trim();
23
+ return normalized || DEFAULT_API_PREFIX;
24
+ }
25
+
26
+ function normalizeRateLimitPluginOptions(value) {
27
+ if (!value || typeof value !== "object") {
28
+ return {
29
+ ...DEFAULT_RATE_LIMIT_PLUGIN_OPTIONS
30
+ };
31
+ }
32
+
33
+ return value;
34
+ }
35
+
36
+ function resolveRouteConfig(request) {
37
+ if (!request?.routeOptions || typeof request.routeOptions !== "object") {
38
+ return {};
39
+ }
40
+
41
+ const routeConfig = request.routeOptions.config;
42
+ if (!routeConfig || typeof routeConfig !== "object") {
43
+ return {};
44
+ }
45
+
46
+ return routeConfig;
47
+ }
48
+
49
+ function enforceCsrfProtection(fastify, request, reply) {
50
+ return new Promise((resolve, reject) => {
51
+ fastify.csrfProtection(request, reply, (error) => {
52
+ if (error) {
53
+ reject(error);
54
+ return;
55
+ }
56
+ resolve();
57
+ });
58
+ });
59
+ }
60
+
61
+ function resolveCsrfToken(request) {
62
+ return request.headers["csrf-token"] || request.headers["x-csrf-token"] || request.headers["x-xsrf-token"] || null;
63
+ }
64
+
65
+ function resolveOwnerValue(meta, request) {
66
+ if (typeof meta.ownerResolver === "function") {
67
+ return meta.ownerResolver({
68
+ req: request,
69
+ res: request.raw,
70
+ url: safeRequestUrl(request),
71
+ params: request.params || {},
72
+ user: request.user || null
73
+ });
74
+ }
75
+
76
+ if (typeof meta.ownerParam === "string" && meta.ownerParam) {
77
+ return request.params ? request.params[meta.ownerParam] : null;
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ function notifyPolicyDenied(onPolicyDenied, payload) {
84
+ if (typeof onPolicyDenied !== "function") {
85
+ return;
86
+ }
87
+ onPolicyDenied(payload);
88
+ }
89
+
90
+ function authPolicyPlugin(deps = {}, options = {}) {
91
+ const { resolveActor, resolveContext, hasPermission, onPolicyDenied } = assertAuthPolicyDeps(deps);
92
+ const apiPrefix = normalizeApiPrefix(options.apiPrefix);
93
+ const unsafeMethods = normalizeUnsafeMethods(options.unsafeMethods);
94
+ const rateLimitPluginOptions = normalizeRateLimitPluginOptions(options.rateLimitPluginOptions);
95
+ const nodeEnv = String(options.nodeEnv || "").trim();
96
+ const csrfCookieOpts =
97
+ options.csrfCookieOpts && typeof options.csrfCookieOpts === "object" ? options.csrfCookieOpts : {};
98
+ const resolveCsrfTokenFn = typeof options.resolveCsrfToken === "function" ? options.resolveCsrfToken : resolveCsrfToken;
99
+ const createError =
100
+ typeof options.createError === "function"
101
+ ? options.createError
102
+ : (status, message) => createAuthPolicyError(status, message);
103
+
104
+ return async function registerAuthPolicyPlugin(fastify) {
105
+ await fastify.register(fastifyCookie);
106
+ await fastify.register(fastifyRateLimit, rateLimitPluginOptions);
107
+ await fastify.register(fastifyCsrfProtection, {
108
+ getToken: resolveCsrfTokenFn,
109
+ cookieOpts: {
110
+ path: "/",
111
+ sameSite: "lax",
112
+ secure: nodeEnv === "production",
113
+ httpOnly: true,
114
+ ...csrfCookieOpts
115
+ }
116
+ });
117
+
118
+ fastify.decorateRequest("user", null);
119
+ fastify.decorateRequest("workspace", null);
120
+ fastify.decorateRequest("membership", null);
121
+ fastify.decorateRequest("permissions", null);
122
+
123
+ fastify.addHook("preHandler", async (request, reply) => {
124
+ const pathname = request?.raw?.url || request?.url || "/";
125
+ if (!String(pathname).startsWith(apiPrefix)) {
126
+ return;
127
+ }
128
+
129
+ const meta = resolveAuthPolicyMeta(resolveRouteConfig(request));
130
+ if (meta.csrfProtection && unsafeMethods.has(String(request?.method || "").toUpperCase())) {
131
+ await enforceCsrfProtection(fastify, request, reply);
132
+ }
133
+
134
+ if (meta.authPolicy === AUTH_POLICIES.PUBLIC) {
135
+ return;
136
+ }
137
+
138
+ const actorResolution = normalizeActorResolution(await resolveActor(request, reply, meta));
139
+ if (actorResolution.transientFailure) {
140
+ notifyPolicyDenied(onPolicyDenied, {
141
+ reason: AUTH_POLICY_DENY_REASONS.AUTH_UPSTREAM_UNAVAILABLE,
142
+ statusCode: 503,
143
+ request,
144
+ meta,
145
+ actor: actorResolution.actor,
146
+ context: null
147
+ });
148
+ throw createError(503, "Authentication service temporarily unavailable. Please retry.");
149
+ }
150
+
151
+ if (!actorResolution.authenticated) {
152
+ notifyPolicyDenied(onPolicyDenied, {
153
+ reason: AUTH_POLICY_DENY_REASONS.UNAUTHENTICATED,
154
+ statusCode: 401,
155
+ request,
156
+ meta,
157
+ actor: actorResolution.actor,
158
+ context: null
159
+ });
160
+ throw createError(401, "Authentication required.");
161
+ }
162
+
163
+ request.user = actorResolution.actor;
164
+ request.workspace = null;
165
+ request.membership = null;
166
+ request.permissions = [];
167
+
168
+ if (meta.authPolicy === AUTH_POLICIES.OWN) {
169
+ const ownerValue = await resolveOwnerValue(meta, request);
170
+ const userValue = request?.user ? request.user[meta.userField] : null;
171
+
172
+ if (ownerValue == null || userValue == null) {
173
+ notifyPolicyDenied(onPolicyDenied, {
174
+ reason: AUTH_POLICY_DENY_REASONS.OWNER_UNRESOLVED,
175
+ statusCode: 400,
176
+ request,
177
+ meta,
178
+ actor: actorResolution.actor,
179
+ context: null
180
+ });
181
+ throw createError(400, "Route owner could not be resolved.");
182
+ }
183
+
184
+ if (String(ownerValue) !== String(userValue)) {
185
+ notifyPolicyDenied(onPolicyDenied, {
186
+ reason: AUTH_POLICY_DENY_REASONS.FORBIDDEN_OWNER_MISMATCH,
187
+ statusCode: 403,
188
+ request,
189
+ meta,
190
+ actor: actorResolution.actor,
191
+ context: null
192
+ });
193
+ throw createError(403, "Forbidden.");
194
+ }
195
+ } else if (meta.authPolicy !== AUTH_POLICIES.REQUIRED) {
196
+ notifyPolicyDenied(onPolicyDenied, {
197
+ reason: AUTH_POLICY_DENY_REASONS.INVALID_AUTH_POLICY,
198
+ statusCode: 500,
199
+ request,
200
+ meta,
201
+ actor: actorResolution.actor,
202
+ context: null
203
+ });
204
+ throw createError(500, "Invalid route auth policy configuration.");
205
+ }
206
+
207
+ let context = null;
208
+ if (resolveContext && (meta.contextPolicy !== CONTEXT_POLICIES.NONE || meta.permission)) {
209
+ context = await resolveContext({
210
+ request,
211
+ actor: actorResolution.actor,
212
+ meta
213
+ });
214
+ const normalizedContext = context && typeof context === "object" ? context : {};
215
+ request.workspace = normalizedContext.workspace || null;
216
+ request.membership = normalizedContext.membership || null;
217
+ request.permissions = Array.isArray(normalizedContext.permissions) ? normalizedContext.permissions : [];
218
+ }
219
+
220
+ if (meta.permission) {
221
+ const allowed = await Promise.resolve(
222
+ hasPermission({
223
+ permission: meta.permission,
224
+ permissions: request.permissions,
225
+ request,
226
+ actor: actorResolution.actor,
227
+ context,
228
+ meta
229
+ })
230
+ );
231
+ if (!allowed) {
232
+ notifyPolicyDenied(onPolicyDenied, {
233
+ reason: AUTH_POLICY_DENY_REASONS.FORBIDDEN_PERMISSION,
234
+ statusCode: 403,
235
+ request,
236
+ meta,
237
+ actor: actorResolution.actor,
238
+ context
239
+ });
240
+ throw createError(403, "Forbidden.");
241
+ }
242
+ }
243
+ });
244
+ };
245
+ }
246
+
247
+ export { authPolicyPlugin };
@@ -0,0 +1,64 @@
1
+ import { asObject } from "./objectUtils.js";
2
+
3
+ const AUTH_POLICIES = Object.freeze({
4
+ PUBLIC: "public",
5
+ REQUIRED: "required",
6
+ OWN: "own"
7
+ });
8
+
9
+ const CONTEXT_POLICIES = Object.freeze({
10
+ NONE: "none",
11
+ OPTIONAL: "optional",
12
+ REQUIRED: "required"
13
+ });
14
+
15
+ const DEFAULT_AUTH_POLICY_META = Object.freeze({
16
+ authPolicy: AUTH_POLICIES.PUBLIC,
17
+ contextPolicy: CONTEXT_POLICIES.NONE,
18
+ surface: "",
19
+ permission: "",
20
+ ownerParam: null,
21
+ userField: "id",
22
+ ownerResolver: null,
23
+ csrfProtection: true
24
+ });
25
+
26
+ function resolveAuthPolicyMeta(input = {}) {
27
+ const source = asObject(input);
28
+ return {
29
+ authPolicy: source.authPolicy || AUTH_POLICIES.PUBLIC,
30
+ contextPolicy: source.contextPolicy || CONTEXT_POLICIES.NONE,
31
+ surface: String(source.surface || "").trim(),
32
+ permission: String(source.permission || "").trim(),
33
+ ownerParam: typeof source.ownerParam === "string" && source.ownerParam ? source.ownerParam : null,
34
+ userField: source.userField || "id",
35
+ ownerResolver: typeof source.ownerResolver === "function" ? source.ownerResolver : null,
36
+ csrfProtection: source.csrfProtection !== false
37
+ };
38
+ }
39
+
40
+ function withAuthPolicy(meta = {}) {
41
+ return {
42
+ config: resolveAuthPolicyMeta(meta)
43
+ };
44
+ }
45
+
46
+ function mergeAuthPolicy(routeOptions = {}, meta = {}) {
47
+ const sourceRouteOptions = asObject(routeOptions);
48
+ const sourceConfig = asObject(sourceRouteOptions.config);
49
+ const sourceMeta = asObject(meta);
50
+ const normalizedMeta = resolveAuthPolicyMeta({
51
+ ...sourceConfig,
52
+ ...sourceMeta
53
+ });
54
+
55
+ return {
56
+ ...sourceRouteOptions,
57
+ config: {
58
+ ...sourceConfig,
59
+ ...normalizedMeta
60
+ }
61
+ };
62
+ }
63
+
64
+ export { AUTH_POLICIES, CONTEXT_POLICIES, DEFAULT_AUTH_POLICY_META, resolveAuthPolicyMeta, withAuthPolicy, mergeAuthPolicy };
@@ -0,0 +1,25 @@
1
+ import { normalizeOpaqueId } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ function createAuthRouteVisibilityResolver() {
4
+ return Object.freeze({
5
+ resolverId: "auth.policy.visibility",
6
+ resolve({ visibility, context, request } = {}) {
7
+ if (visibility !== "user") {
8
+ return {};
9
+ }
10
+
11
+ const actor = context?.actor || request?.user || null;
12
+ const userOwnerId = normalizeOpaqueId(actor?.id);
13
+ if (userOwnerId == null) {
14
+ return {};
15
+ }
16
+
17
+ return {
18
+ userOwnerId,
19
+ requiresActorScope: true
20
+ };
21
+ }
22
+ });
23
+ }
24
+
25
+ export { createAuthRouteVisibilityResolver };