@jskit-ai/auth-core 0.1.51 → 0.1.53

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  "packageVersion": 1,
3
3
  "packageId": "@jskit-ai/auth-core",
4
- "version": "0.1.51",
4
+ "version": "0.1.53",
5
5
  "kind": "runtime",
6
6
  "dependsOn": [
7
7
  "@jskit-ai/value-app-config-shared"
@@ -69,7 +69,7 @@ export default Object.freeze({
69
69
  "mutations": {
70
70
  "dependencies": {
71
71
  "runtime": {
72
- "@jskit-ai/kernel": "0.1.52",
72
+ "@jskit-ai/kernel": "0.1.54",
73
73
  "@fastify/cookie": "^11.0.2",
74
74
  "@fastify/csrf-protection": "^7.1.0",
75
75
  "@fastify/rate-limit": "^10.3.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/auth-core",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -8,6 +8,8 @@
8
8
  "exports": {
9
9
  "./server/providers/AccessCoreServiceProvider": "./src/server/providers/AccessCoreServiceProvider.js",
10
10
  "./server/providers/FastifyAuthPolicyServiceProvider": "./src/server/providers/FastifyAuthPolicyServiceProvider.js",
11
+ "./server/authPolicyContextResolverRegistry": "./src/server/authPolicyContextResolverRegistry.js",
12
+ "./server/authServiceDecoratorRegistry": "./src/server/authServiceDecoratorRegistry.js",
11
13
  "./server/utils": "./src/server/utils.js",
12
14
  "./server/validators": "./src/server/validators.js",
13
15
  "./server/inviteTokens": "./src/server/inviteTokens.js",
@@ -44,7 +46,7 @@
44
46
  "./shared/commands/authSessionReadCommand": "./src/shared/commands/authSessionReadCommand.js"
45
47
  },
46
48
  "dependencies": {
47
- "@jskit-ai/kernel": "0.1.52",
49
+ "@jskit-ai/kernel": "0.1.54",
48
50
  "@fastify/cookie": "^11.0.2",
49
51
  "@fastify/csrf-protection": "^7.1.0",
50
52
  "@fastify/rate-limit": "^10.3.0",
@@ -0,0 +1,125 @@
1
+ import { normalizePermissionList } from "@jskit-ai/kernel/shared/support/permissions";
2
+ import { registerTaggedSingleton, resolveTaggedEntries } from "@jskit-ai/kernel/server/registries";
3
+
4
+ const AUTH_POLICY_CONTEXT_RESOLVER_TAG = "jskit.auth.policy.context.resolvers";
5
+
6
+ function normalizeAuthPolicyContextResolver(entry) {
7
+ if (typeof entry === "function") {
8
+ return Object.freeze({
9
+ resolverId: String(entry.name || "anonymous"),
10
+ order: 0,
11
+ resolveAuthPolicyContext: entry
12
+ });
13
+ }
14
+
15
+ if (!entry || typeof entry !== "object" || typeof entry.resolveAuthPolicyContext !== "function") {
16
+ return null;
17
+ }
18
+
19
+ const resolverId = String(entry.resolverId || "anonymous");
20
+ const order = Number.isFinite(entry.order) ? Number(entry.order) : 0;
21
+
22
+ return Object.freeze({
23
+ ...entry,
24
+ resolverId,
25
+ order,
26
+ resolveAuthPolicyContext: entry.resolveAuthPolicyContext
27
+ });
28
+ }
29
+
30
+ function registerAuthPolicyContextResolver(app, token, factory) {
31
+ registerTaggedSingleton(app, token, factory, AUTH_POLICY_CONTEXT_RESOLVER_TAG, {
32
+ context: "registerAuthPolicyContextResolver"
33
+ });
34
+ }
35
+
36
+ function resolveAuthPolicyContextResolvers(scope) {
37
+ return resolveTaggedEntries(scope, AUTH_POLICY_CONTEXT_RESOLVER_TAG)
38
+ .map((entry, index) => ({
39
+ resolver: normalizeAuthPolicyContextResolver(entry),
40
+ index
41
+ }))
42
+ .filter((entry) => Boolean(entry.resolver))
43
+ .sort((left, right) => {
44
+ if (left.resolver.order !== right.resolver.order) {
45
+ return left.resolver.order - right.resolver.order;
46
+ }
47
+
48
+ return left.index - right.index;
49
+ })
50
+ .map((entry) => entry.resolver);
51
+ }
52
+
53
+ function mergeAuthPolicyContexts(contexts = []) {
54
+ const merged = {};
55
+ let hasValues = false;
56
+ const permissions = new Set();
57
+
58
+ for (const context of Array.isArray(contexts) ? contexts : [contexts]) {
59
+ if (!context || typeof context !== "object") {
60
+ continue;
61
+ }
62
+
63
+ for (const [key, value] of Object.entries(context)) {
64
+ if (key === "permissions") {
65
+ for (const permission of normalizePermissionList(value)) {
66
+ permissions.add(permission);
67
+ }
68
+ continue;
69
+ }
70
+
71
+ if (value === undefined) {
72
+ continue;
73
+ }
74
+
75
+ merged[key] = value;
76
+ hasValues = true;
77
+ }
78
+ }
79
+
80
+ if (permissions.size > 0) {
81
+ merged.permissions = Object.freeze([...permissions]);
82
+ hasValues = true;
83
+ }
84
+
85
+ return hasValues ? Object.freeze(merged) : null;
86
+ }
87
+
88
+ function composeAuthPolicyContextResolvers(resolvers = []) {
89
+ const normalizedResolvers = (Array.isArray(resolvers) ? resolvers : [resolvers])
90
+ .map((entry, index) => ({
91
+ resolver: normalizeAuthPolicyContextResolver(entry),
92
+ index
93
+ }))
94
+ .filter((entry) => Boolean(entry.resolver))
95
+ .sort((left, right) => {
96
+ if (left.resolver.order !== right.resolver.order) {
97
+ return left.resolver.order - right.resolver.order;
98
+ }
99
+
100
+ return left.index - right.index;
101
+ })
102
+ .map((entry) => entry.resolver);
103
+
104
+ if (normalizedResolvers.length < 1) {
105
+ return null;
106
+ }
107
+
108
+ return async function resolveComposedAuthPolicyContext(input = {}) {
109
+ const contexts = [];
110
+
111
+ for (const resolver of normalizedResolvers) {
112
+ contexts.push(await resolver.resolveAuthPolicyContext(input));
113
+ }
114
+
115
+ return mergeAuthPolicyContexts(contexts);
116
+ };
117
+ }
118
+
119
+ export {
120
+ AUTH_POLICY_CONTEXT_RESOLVER_TAG,
121
+ registerAuthPolicyContextResolver,
122
+ resolveAuthPolicyContextResolvers,
123
+ mergeAuthPolicyContexts,
124
+ composeAuthPolicyContextResolvers
125
+ };
@@ -0,0 +1,56 @@
1
+ import { registerTaggedSingleton, resolveTaggedEntries } from "@jskit-ai/kernel/server/registries";
2
+
3
+ const AUTH_SERVICE_DECORATOR_TAG = "jskit.auth.service.decorators";
4
+
5
+ function normalizeAuthServiceDecorator(entry) {
6
+ if (typeof entry === "function") {
7
+ return Object.freeze({
8
+ decoratorId: String(entry.name || "anonymous"),
9
+ order: 0,
10
+ decorateAuthService: entry
11
+ });
12
+ }
13
+
14
+ if (!entry || typeof entry !== "object" || typeof entry.decorateAuthService !== "function") {
15
+ return null;
16
+ }
17
+
18
+ const decoratorId = String(entry.decoratorId || "anonymous");
19
+ const order = Number.isFinite(entry.order) ? Number(entry.order) : 0;
20
+
21
+ return Object.freeze({
22
+ ...entry,
23
+ decoratorId,
24
+ order,
25
+ decorateAuthService: entry.decorateAuthService
26
+ });
27
+ }
28
+
29
+ function registerAuthServiceDecorator(app, token, factory) {
30
+ registerTaggedSingleton(app, token, factory, AUTH_SERVICE_DECORATOR_TAG, {
31
+ context: "registerAuthServiceDecorator"
32
+ });
33
+ }
34
+
35
+ function resolveAuthServiceDecorators(scope) {
36
+ return resolveTaggedEntries(scope, AUTH_SERVICE_DECORATOR_TAG)
37
+ .map((entry, index) => ({
38
+ decorator: normalizeAuthServiceDecorator(entry),
39
+ index
40
+ }))
41
+ .filter((entry) => Boolean(entry.decorator))
42
+ .sort((left, right) => {
43
+ if (left.decorator.order !== right.decorator.order) {
44
+ return left.decorator.order - right.decorator.order;
45
+ }
46
+
47
+ return left.index - right.index;
48
+ })
49
+ .map((entry) => entry.decorator);
50
+ }
51
+
52
+ export {
53
+ AUTH_SERVICE_DECORATOR_TAG,
54
+ registerAuthServiceDecorator,
55
+ resolveAuthServiceDecorators
56
+ };
@@ -1,5 +1,9 @@
1
1
  import { registerActionContextContributor } from "@jskit-ai/kernel/server/actions";
2
2
  import { registerRouteVisibilityResolver } from "@jskit-ai/kernel/server/http";
3
+ import {
4
+ composeAuthPolicyContextResolvers,
5
+ resolveAuthPolicyContextResolvers
6
+ } from "../authPolicyContextResolverRegistry.js";
3
7
  import { authPolicyPlugin } from "../lib/plugin.js";
4
8
  import { createAuthActionContextContributor } from "../lib/actionContextContributor.js";
5
9
  import { createAuthRouteVisibilityResolver } from "../lib/routeVisibilityResolver.js";
@@ -74,17 +78,30 @@ class FastifyAuthPolicyServiceProvider {
74
78
  const env = app.has("jskit.env") ? app.make("jskit.env") : {};
75
79
  const fastify = app.make("jskit.fastify");
76
80
  const authService = app.make("authService");
77
- const resolveContext =
81
+ const legacyResolveContext =
78
82
  typeof app.has === "function" && app.has("auth.policy.contextResolver")
79
83
  ? app.make("auth.policy.contextResolver")
80
84
  : null;
81
85
 
82
- if (resolveContext != null && typeof resolveContext !== "function") {
86
+ if (legacyResolveContext != null && typeof legacyResolveContext !== "function") {
83
87
  throw new Error(
84
88
  "FastifyAuthPolicyServiceProvider requires auth.policy.contextResolver to be a function when provided."
85
89
  );
86
90
  }
87
91
 
92
+ const resolveContext = composeAuthPolicyContextResolvers([
93
+ ...resolveAuthPolicyContextResolvers(app),
94
+ ...(legacyResolveContext
95
+ ? [
96
+ {
97
+ resolverId: "legacy.auth.policy.contextResolver",
98
+ order: 1000,
99
+ resolveAuthPolicyContext: legacyResolveContext
100
+ }
101
+ ]
102
+ : [])
103
+ ]);
104
+
88
105
  const pluginDeps = {
89
106
  resolveActor: async (request) => {
90
107
  if (authService && typeof authService.authenticateRequest === "function") {
@@ -204,6 +204,8 @@ const sessionResponseValidator = Object.freeze({
204
204
  {
205
205
  authenticated: Type.Boolean(),
206
206
  username: Type.Optional(Type.String({ minLength: 1, maxLength: 120 })),
207
+ email: Type.Optional(authEmailValidator.schema),
208
+ permissions: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 200 }))),
207
209
  csrfToken: Type.String({ minLength: 1 }),
208
210
  oauthProviders: Type.Array(oauthProviderCatalogEntryValidator.schema),
209
211
  oauthDefaultProvider: Type.Union([oauthProviderValidator.schema, Type.Null()])
@@ -21,6 +21,7 @@ test("authApi exposes the expected methods and request routes", async () => {
21
21
  "verifyOtp",
22
22
  "oauthStartUrl",
23
23
  "oauthComplete",
24
+ "devLoginAs",
24
25
  "requestPasswordReset",
25
26
  "completePasswordRecovery",
26
27
  "resetPassword",
@@ -30,6 +31,7 @@ test("authApi exposes the expected methods and request routes", async () => {
30
31
  await api.session();
31
32
  await api.resendRegisterConfirmation({ email: "x@example.com" });
32
33
  await api.login({ email: "x@example.com" });
34
+ await api.devLoginAs({ userId: "42" });
33
35
  await api.logout();
34
36
 
35
37
  assert.equal(calls[0].url, "/api/session");
@@ -37,8 +39,10 @@ test("authApi exposes the expected methods and request routes", async () => {
37
39
  assert.equal(calls[1].options.method, "POST");
38
40
  assert.equal(calls[2].url, "/api/login");
39
41
  assert.equal(calls[2].options.method, "POST");
40
- assert.equal(calls[3].url, "/api/logout");
42
+ assert.equal(calls[3].url, "/api/dev-auth/login-as");
41
43
  assert.equal(calls[3].options.method, "POST");
44
+ assert.equal(calls[4].url, "/api/logout");
45
+ assert.equal(calls[4].options.method, "POST");
42
46
  });
43
47
 
44
48
  test("authApi oauthStartUrl builds provider path with optional returnTo", () => {
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createApplication } from "@jskit-ai/kernel/_testable";
4
+ import {
5
+ AUTH_POLICY_CONTEXT_RESOLVER_TAG,
6
+ composeAuthPolicyContextResolvers,
7
+ registerAuthPolicyContextResolver,
8
+ resolveAuthPolicyContextResolvers
9
+ } from "../src/server/authPolicyContextResolverRegistry.js";
10
+
11
+ test("auth policy context resolver registry resolves resolvers in order", async () => {
12
+ const app = createApplication();
13
+
14
+ registerAuthPolicyContextResolver(app, "test.auth.policy.context.permissions", () => ({
15
+ resolverId: "permissions",
16
+ order: 20,
17
+ async resolveAuthPolicyContext() {
18
+ return {
19
+ permissions: ["alpha.read"]
20
+ };
21
+ }
22
+ }));
23
+
24
+ registerAuthPolicyContextResolver(app, "test.auth.policy.context.workspace", () => ({
25
+ resolverId: "workspace",
26
+ order: 10,
27
+ async resolveAuthPolicyContext() {
28
+ return {
29
+ workspace: { id: "11" },
30
+ membership: { roleSid: "member" },
31
+ permissions: ["workspace.read"]
32
+ };
33
+ }
34
+ }));
35
+
36
+ const resolvers = resolveAuthPolicyContextResolvers(app);
37
+ assert.deepEqual(
38
+ resolvers.map((entry) => entry.resolverId),
39
+ ["workspace", "permissions"]
40
+ );
41
+
42
+ const resolveContext = composeAuthPolicyContextResolvers(resolvers);
43
+ const context = await resolveContext({
44
+ actor: { id: "7" }
45
+ });
46
+
47
+ assert.deepEqual(context, {
48
+ workspace: { id: "11" },
49
+ membership: { roleSid: "member" },
50
+ permissions: ["workspace.read", "alpha.read"]
51
+ });
52
+ });
53
+
54
+ test("auth policy context resolver registry exports canonical tag", () => {
55
+ assert.equal(AUTH_POLICY_CONTEXT_RESOLVER_TAG, "jskit.auth.policy.context.resolvers");
56
+ });
@@ -0,0 +1,51 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createApplication } from "@jskit-ai/kernel/_testable";
4
+ import {
5
+ AUTH_SERVICE_DECORATOR_TAG,
6
+ registerAuthServiceDecorator,
7
+ resolveAuthServiceDecorators
8
+ } from "../src/server/authServiceDecoratorRegistry.js";
9
+
10
+ test("auth service decorator registry resolves decorators in order", () => {
11
+ const app = createApplication();
12
+
13
+ registerAuthServiceDecorator(app, "test.auth.decorator.zeta", () => ({
14
+ decoratorId: "zeta",
15
+ order: 50,
16
+ decorateAuthService(service) {
17
+ return {
18
+ ...service,
19
+ trace: [...service.trace, "zeta"]
20
+ };
21
+ }
22
+ }));
23
+
24
+ registerAuthServiceDecorator(app, "test.auth.decorator.alpha", () => ({
25
+ decoratorId: "alpha",
26
+ order: 10,
27
+ decorateAuthService(service) {
28
+ return {
29
+ ...service,
30
+ trace: [...service.trace, "alpha"]
31
+ };
32
+ }
33
+ }));
34
+
35
+ const decorators = resolveAuthServiceDecorators(app);
36
+ assert.equal(decorators.length, 2);
37
+ assert.deepEqual(
38
+ decorators.map((entry) => entry.decoratorId),
39
+ ["alpha", "zeta"]
40
+ );
41
+
42
+ const decorated = decorators.reduce(
43
+ (service, decorator) => decorator.decorateAuthService(service),
44
+ { trace: [] }
45
+ );
46
+ assert.deepEqual(decorated.trace, ["alpha", "zeta"]);
47
+ });
48
+
49
+ test("auth service decorator registry exports canonical tag", () => {
50
+ assert.equal(AUTH_SERVICE_DECORATOR_TAG, "jskit.auth.service.decorators");
51
+ });
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { FastifyAuthPolicyServiceProvider } from "../src/server/providers/FastifyAuthPolicyServiceProvider.js";
4
+ import { AUTH_POLICY_CONTEXT_RESOLVER_TAG } from "../src/server/authPolicyContextResolverRegistry.js";
4
5
  import { createFakeFastifyPolicyRuntime } from "../../../tooling/testUtils/fakeFastify.mjs";
5
6
 
6
7
  test("FastifyAuthPolicyServiceProvider registers auth policy plugin through provider boot", async () => {
@@ -84,6 +85,17 @@ test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolv
84
85
  throw new Error(`Missing token ${String(token)}`);
85
86
  }
86
87
  return bag.get(token);
88
+ },
89
+ resolveTag(tag) {
90
+ if (tag !== AUTH_POLICY_CONTEXT_RESOLVER_TAG) {
91
+ return [];
92
+ }
93
+
94
+ return [
95
+ async () => ({
96
+ permissions: ["settings.manage"]
97
+ })
98
+ ];
87
99
  }
88
100
  };
89
101
 
@@ -108,5 +120,5 @@ test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolv
108
120
  assert.equal(request.workspace?.id, 11);
109
121
  assert.equal(request.workspace?.slug, "acme");
110
122
  assert.equal(request.membership?.roleSid, "member");
111
- assert.deepEqual(request.permissions, ["projects.read"]);
123
+ assert.deepEqual(request.permissions, ["settings.manage", "projects.read"]);
112
124
  });