@jskit-ai/auth-core 0.1.157 → 0.1.159

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,70 +1,15 @@
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;
1
+ function registerAuthServiceDecorator(extensions, decorator) {
2
+ if (!extensions || typeof extensions.registerServiceDecorator !== "function") {
3
+ throw new TypeError("registerAuthServiceDecorator requires auth.extensions.");
16
4
  }
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
- });
5
+ return extensions.registerServiceDecorator(decorator);
27
6
  }
28
7
 
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
- function applyAuthServiceDecorators(scope, authService) {
53
- let decoratedAuthService = authService;
54
-
55
- for (const decorator of resolveAuthServiceDecorators(scope)) {
56
- decoratedAuthService = decorator.decorateAuthService(decoratedAuthService);
57
- if (!decoratedAuthService || typeof decoratedAuthService !== "object") {
58
- throw new Error(`Auth service decorator "${decorator.decoratorId}" must return an auth service object.`);
59
- }
8
+ function applyAuthServiceDecorators(extensions, authService) {
9
+ if (!extensions || typeof extensions.decorateService !== "function") {
10
+ throw new TypeError("applyAuthServiceDecorators requires auth.extensions.");
60
11
  }
61
-
62
- return decoratedAuthService;
12
+ return extensions.decorateService(authService);
63
13
  }
64
14
 
65
- export {
66
- AUTH_SERVICE_DECORATOR_TAG,
67
- applyAuthServiceDecorators,
68
- registerAuthServiceDecorator,
69
- resolveAuthServiceDecorators
70
- };
15
+ export { applyAuthServiceDecorators, registerAuthServiceDecorator };
@@ -0,0 +1,32 @@
1
+ const attachByHandle = new WeakMap();
2
+
3
+ function createDeferredAuthService() {
4
+ let service = null;
5
+ const handle = new Proxy(Object.create(null), {
6
+ get(_target, property) {
7
+ if (property === Symbol.toStringTag) return "AuthService";
8
+ if (!service) throw new Error("Authentication service is not ready until runtime startup completes.");
9
+ const value = service[property];
10
+ return typeof value === "function" ? value.bind(service) : value;
11
+ },
12
+ has(_target, property) {
13
+ return service ? property in service : false;
14
+ }
15
+ });
16
+ attachByHandle.set(handle, (value) => {
17
+ if (service) throw new Error("Authentication service is already initialized.");
18
+ service = value;
19
+ });
20
+ return handle;
21
+ }
22
+
23
+ function attachDeferredAuthService(handle, service) {
24
+ const attach = attachByHandle.get(handle);
25
+ if (!attach) throw new TypeError("attachDeferredAuthService requires a deferred auth service handle.");
26
+ if (!service || typeof service !== "object") {
27
+ throw new TypeError("attachDeferredAuthService requires an auth service object.");
28
+ }
29
+ attach(service);
30
+ }
31
+
32
+ export { attachDeferredAuthService, createDeferredAuthService };
@@ -3,7 +3,8 @@ import { normalizePermissionList } from "@jskit-ai/kernel/shared/support/permiss
3
3
  function createAuthActionContextContributor() {
4
4
  return Object.freeze({
5
5
  contributorId: "auth.policy.request-context",
6
- contribute({ request } = {}) {
6
+ contribute({ context } = {}) {
7
+ const request = context?.requestMeta?.request || null;
7
8
  const contribution = {};
8
9
  const permissions = normalizePermissionList(request?.permissions);
9
10
 
@@ -1,3 +1,4 @@
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
1
2
  import * as authConstraints from "../../shared/authConstraints.js";
2
3
  import * as authMethods from "../../shared/authMethods.js";
3
4
  import * as oauthProviders from "../../shared/oauthProviders.js";
@@ -7,7 +8,7 @@ import * as inviteTokens from "../inviteTokens.js";
7
8
  import * as utils from "../utils.js";
8
9
  import * as validators from "../validators.js";
9
10
 
10
- const ACCESS_CORE_API = Object.freeze({
11
+ const access = Object.freeze({
11
12
  authConstraints,
12
13
  authMethods,
13
14
  oauthProviders,
@@ -18,18 +19,14 @@ const ACCESS_CORE_API = Object.freeze({
18
19
  validators
19
20
  });
20
21
 
21
- class AccessCoreServiceProvider {
22
- static id = "auth.access";
23
-
24
- register(app) {
25
- if (!app || typeof app.singleton !== "function") {
26
- throw new Error("AccessCoreServiceProvider requires application singleton().");
27
- }
28
-
29
- app.singleton("auth.access", () => ACCESS_CORE_API);
22
+ const AuthAccessProvider = defineProvider({
23
+ id: "auth.access",
24
+ provides: {
25
+ access: "auth.access"
26
+ },
27
+ setup() {
28
+ return { access };
30
29
  }
30
+ });
31
31
 
32
- boot() {}
33
- }
34
-
35
- export { AccessCoreServiceProvider };
32
+ export { AuthAccessProvider };
@@ -0,0 +1,14 @@
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
2
+ import { createAuthExtensions } from "../authExtensions.js";
3
+
4
+ const AuthExtensionsProvider = defineProvider({
5
+ id: "auth.extensions",
6
+ provides: {
7
+ extensions: "auth.extensions"
8
+ },
9
+ setup() {
10
+ return { extensions: createAuthExtensions() };
11
+ }
12
+ });
13
+
14
+ export { AuthExtensionsProvider };
@@ -0,0 +1,15 @@
1
+ import { defineFeature } from "@jskit-ai/kernel/server/features";
2
+ import { buildAuthActions } from "../actions/auth.contributor.js";
3
+
4
+ const AuthFeature = defineFeature({
5
+ id: "auth.actions",
6
+ domain: "auth",
7
+ optional: {
8
+ authService: "auth.service"
9
+ },
10
+ actions({ authService }) {
11
+ return authService ? buildAuthActions({ authService }) : [];
12
+ }
13
+ });
14
+
15
+ export { AuthFeature };
@@ -0,0 +1,79 @@
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
2
+ import { parseBooleanFlag } from "../booleanFlag.js";
3
+ import { authPolicyPlugin } from "../lib/plugin.js";
4
+ import { createAuthActionContextContributor } from "../lib/actionContextContributor.js";
5
+ import { createAuthRouteVisibilityResolver } from "../lib/routeVisibilityResolver.js";
6
+
7
+ function parseList(value) {
8
+ return String(value || "").split(",").map((entry) => entry.trim()).filter(Boolean);
9
+ }
10
+
11
+ function defaultHasPermission({ permission, permissions = [] } = {}) {
12
+ return !permission || (Array.isArray(permissions) && permissions.includes(permission));
13
+ }
14
+
15
+ function createPolicy({ authService, extensions, env }) {
16
+ return Object.freeze({
17
+ async resolveActor(request) {
18
+ if (authService && typeof authService.authenticateRequest === "function") {
19
+ return authService.authenticateRequest(request);
20
+ }
21
+ return { authenticated: false, actor: null, transientFailure: false };
22
+ },
23
+ hasPermission: defaultHasPermission,
24
+ resolveContext(input = {}) {
25
+ return extensions.resolvePolicyContext(input);
26
+ },
27
+ options: Object.freeze({
28
+ nodeEnv: String(env.NODE_ENV || "development").trim() || "development",
29
+ apiPrefix: String(env.AUTH_API_PREFIX || "/api/").trim() || "/api/",
30
+ unsafeMethods: Object.freeze(parseList(env.AUTH_CSRF_UNSAFE_METHODS)),
31
+ csrfCookieOpts: Object.freeze({
32
+ secure: parseBooleanFlag(env.AUTH_CSRF_COOKIE_SECURE, false)
33
+ })
34
+ })
35
+ });
36
+ }
37
+
38
+ const AuthPolicyProvider = defineProvider({
39
+ id: "auth.policy",
40
+ requires: {
41
+ actions: "runtime.actions",
42
+ extensions: "auth.extensions",
43
+ fastify: "runtime.fastify",
44
+ http: "runtime.http",
45
+ env: "runtime.env"
46
+ },
47
+ optional: {
48
+ authService: "auth.service"
49
+ },
50
+ provides: {
51
+ policy: "auth.policy"
52
+ },
53
+ setup({ actions, authService, extensions, http, env }) {
54
+ const actionContext = createAuthActionContextContributor();
55
+ actions.registerContextContributor({
56
+ id: actionContext.contributorId,
57
+ contribute: actionContext.contribute
58
+ });
59
+ const visibility = createAuthRouteVisibilityResolver();
60
+ http.registerVisibilityResolver({
61
+ id: visibility.resolverId,
62
+ resolve: visibility.resolve
63
+ });
64
+ return { policy: createPolicy({ authService, extensions, env }) };
65
+ },
66
+ async boot({ fastify }, { outputs }) {
67
+ const policy = outputs.policy;
68
+ await authPolicyPlugin(
69
+ {
70
+ resolveActor: policy.resolveActor,
71
+ resolveContext: policy.resolveContext,
72
+ hasPermission: policy.hasPermission
73
+ },
74
+ policy.options
75
+ )(fastify);
76
+ }
77
+ });
78
+
79
+ export { AuthPolicyProvider };
@@ -6,11 +6,15 @@ test("auth action context contributor skips empty placeholder values", () => {
6
6
  const contributor = createAuthActionContextContributor();
7
7
 
8
8
  const contribution = contributor.contribute({
9
- request: {
10
- user: null,
11
- workspace: null,
12
- membership: null,
13
- permissions: []
9
+ context: {
10
+ requestMeta: {
11
+ request: {
12
+ user: null,
13
+ workspace: null,
14
+ membership: null,
15
+ permissions: []
16
+ }
17
+ }
14
18
  }
15
19
  });
16
20
 
@@ -33,7 +37,11 @@ test("auth action context contributor contributes real request context values",
33
37
  permissions: ["workspace.settings.update", "", " "]
34
38
  };
35
39
 
36
- const contribution = contributor.contribute({ request });
40
+ const contribution = contributor.contribute({
41
+ context: {
42
+ requestMeta: { request }
43
+ }
44
+ });
37
45
 
38
46
  assert.deepEqual(contribution, {
39
47
  actor: request.user,
@@ -1,265 +1,88 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { createSchema } from "json-rest-schema";
4
- import { createApplication } from "@jskit-ai/kernel/_testable";
5
- import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
6
- import { AuthActionsServiceProvider } from "../src/server/providers/AuthActionsServiceProvider.js";
3
+ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
4
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
7
5
  import { buildAuthActions } from "../src/server/actions/auth.contributor.js";
6
+ import { AuthFeature } from "../src/server/providers/AuthFeature.js";
8
7
 
9
- function createAppConfigFixture() {
10
- return {
11
- surfaceModeAll: "all",
12
- surfaceDefaultId: "home",
13
- surfaceDefinitions: {
14
- home: { id: "home", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false },
15
- console: {
16
- id: "console",
17
- pagesRoot: "console",
18
- enabled: true,
19
- requiresAuth: true,
20
- requiresWorkspace: false
21
- }
22
- }
23
- };
24
- }
25
-
26
- test("auth logout action delegates to selected auth provider and notifies session changes", async () => {
27
- const action = buildAuthActions().find((definition) => definition.id === "auth.logout");
28
- const request = {
29
- id: "request-1"
30
- };
8
+ test("auth logout action delegates directly to the selected auth service", async () => {
31
9
  const calls = [];
32
-
33
- const result = await action.execute(
34
- {},
35
- {
36
- requestMeta: {
37
- request
38
- }
39
- },
40
- {
41
- authService: {
42
- async logout(receivedRequest) {
43
- calls.push({
44
- type: "logout",
45
- request: receivedRequest
46
- });
47
- return {
48
- ok: true,
49
- clearSession: true
50
- };
51
- }
52
- },
53
- authSessionEventsService: {
54
- async notifySessionChanged(payload) {
55
- calls.push({
56
- type: "notify",
57
- context: payload.context
58
- });
59
- }
60
- }
10
+ const authService = {
11
+ async logout(request) {
12
+ calls.push(request);
13
+ return { ok: true, clearSession: true };
61
14
  }
62
- );
63
-
64
- assert.deepEqual(result, {
15
+ };
16
+ const action = buildAuthActions({ authService }).find((definition) => definition.id === "auth.logout");
17
+ const request = { id: "request-1" };
18
+ assert.deepEqual(await action.execute({}, { requestMeta: { request } }), {
65
19
  ok: true,
66
20
  clearSession: true
67
21
  });
68
- assert.deepEqual(calls, [
69
- {
70
- type: "logout",
71
- request
72
- },
73
- {
74
- type: "notify",
75
- context: {
76
- requestMeta: {
77
- request
78
- }
79
- }
80
- }
81
- ]);
22
+ assert.deepEqual(calls, [request]);
82
23
  });
83
24
 
84
- test("shared dev login-as action passes the trusted request to the selected auth provider", async () => {
85
- const action = buildAuthActions().find((definition) => definition.id === "auth.dev.loginAs");
86
- const request = {
87
- headers: {
88
- "x-jskit-dev-auth-secret": "secret"
89
- }
90
- };
91
- const input = {
92
- email: "ada@example.com"
93
- };
25
+ test("shared dev login-as action passes the trusted request to auth.service", async () => {
26
+ const input = { email: "ada@example.com" };
27
+ const request = { headers: { "x-jskit-dev-auth-secret": "secret" } };
94
28
  let received = null;
95
-
96
- const result = await action.execute(input, {
97
- requestMeta: {
98
- request
99
- }
100
- }, {
29
+ const action = buildAuthActions({
101
30
  authService: {
102
31
  async devLoginAs(receivedRequest, receivedInput) {
103
- received = {
104
- input: receivedInput,
105
- request: receivedRequest
106
- };
107
- return {
108
- ok: true
109
- };
32
+ received = { input: receivedInput, request: receivedRequest };
33
+ return { ok: true };
110
34
  }
111
35
  }
112
- });
113
-
114
- assert.deepEqual(result, { ok: true });
36
+ }).find((definition) => definition.id === "auth.dev.loginAs");
37
+ assert.deepEqual(await action.execute(input, { requestMeta: { request } }), { ok: true });
115
38
  assert.deepEqual(received, { input, request });
116
39
  });
117
40
 
118
- test("AuthActionsServiceProvider registers shared auth actions against auth.provider", async () => {
119
- const app = createApplication();
120
- const logoutCalls = [];
121
- const published = [];
122
- class SelectedAuthProvider {
123
- static id = "auth.provider";
124
-
125
- register(targetApp) {
126
- targetApp.singleton("authService", () => ({
127
- async logout(request) {
128
- logoutCalls.push(request);
129
- return {
130
- ok: true,
131
- clearSession: true
132
- };
133
- },
134
- async authenticateRequest() {
135
- return {
136
- authenticated: false,
137
- actor: null,
138
- transientFailure: false
139
- };
41
+ test("AuthFeature contributes actions only when auth.service exists", async () => {
42
+ let actions = null;
43
+ const SelectedAuthProvider = defineProvider({
44
+ id: "test.auth.service",
45
+ provides: { service: "auth.service" },
46
+ setup() {
47
+ return {
48
+ service: {
49
+ async authenticateRequest() {
50
+ return { authenticated: false, actor: null, transientFailure: false };
51
+ },
52
+ async logout() {
53
+ return { ok: true, clearSession: true };
54
+ }
140
55
  }
141
- }));
142
- }
143
- }
144
-
145
- app.instance("appConfig", createAppConfigFixture());
146
- app.instance("domainEvents", {
147
- async publish(payload) {
148
- published.push(payload);
56
+ };
149
57
  }
150
58
  });
151
-
152
- await app.start({
153
- providers: [ActionRuntimeServiceProvider, SelectedAuthProvider, AuthActionsServiceProvider]
154
- });
155
-
156
- const actionExecutor = app.make("actionExecutor");
157
- const definitions = actionExecutor.listDefinitions();
158
- assert.equal(definitions.some((definition) => definition.id === "auth.login.password"), true);
159
- assert.equal(definitions.some((definition) => definition.id === "auth.register"), true);
160
- assert.equal(definitions.some((definition) => definition.id === "auth.dev.loginAs"), true);
161
- assert.deepEqual(definitions.find((definition) => definition.id === "auth.session.read")?.surfaces, [
162
- "home",
163
- "console"
164
- ]);
165
-
166
- const request = { id: "request-2" };
167
- const result = await actionExecutor.execute({
168
- actionId: "auth.logout",
169
- input: {},
170
- context: {
171
- channel: "internal",
172
- surface: "home",
173
- requestMeta: { request },
174
- actor: { id: 42 }
59
+ const ProbeProvider = defineProvider({
60
+ id: "test.auth.actions.probe",
61
+ requires: { catalogue: "runtime.actions" },
62
+ setup({ catalogue }) {
63
+ actions = catalogue;
64
+ return {};
175
65
  }
176
66
  });
177
-
178
- assert.deepEqual(result, {
179
- ok: true,
180
- clearSession: true
67
+ const runtime = createCapabilityRuntime({
68
+ providers: [createActionProvider(), SelectedAuthProvider, AuthFeature, ProbeProvider]
181
69
  });
182
- assert.deepEqual(logoutCalls, [request]);
183
- assert.equal(published.length, 2);
184
- assert.deepEqual(
185
- published.map((event) => ({
186
- source: event.source,
187
- entity: event.entity,
188
- operation: event.operation,
189
- entityId: event.entityId,
190
- realtimeEvent: event.meta?.realtime?.event
191
- })),
192
- [
193
- {
194
- source: "auth",
195
- entity: "session",
196
- operation: "updated",
197
- entityId: "42",
198
- realtimeEvent: "auth.session.changed"
199
- },
200
- {
201
- source: "users",
202
- entity: "bootstrap",
203
- operation: "updated",
204
- entityId: "42",
205
- realtimeEvent: "users.bootstrap.changed"
206
- }
207
- ]
208
- );
209
- });
210
-
211
- test("AuthActionsServiceProvider leaves unrelated actions usable when no auth provider is selected", async () => {
212
- const app = createApplication();
213
-
214
- class PublicActionProvider {
215
- static id = "public.actions";
216
-
217
- static startsAfter = ["runtime.actions"];
218
-
219
- register(targetApp) {
220
- targetApp.action({
221
- id: "public.ping",
222
- domain: "public",
223
- version: 1,
224
- kind: "query",
225
- channels: ["internal"],
226
- surfaces: ["home"],
227
- permission: { require: "none" },
228
- input: {
229
- schema: createSchema({}),
230
- mode: "patch"
231
- },
232
- output: null,
233
- idempotency: "none",
234
- audit: { actionName: "public.ping" },
235
- observability: {},
236
- async execute() {
237
- return { ok: true };
238
- }
239
- });
70
+ await runtime.start();
71
+ assert.equal(actions.listDefinitions().some((definition) => definition.id === "auth.login.password"), true);
72
+ assert.deepEqual(actions.getDefinition("auth.session.read").surfaces, ["*"]);
73
+
74
+ let publicActions = null;
75
+ const EmptyProbe = defineProvider({
76
+ id: "test.auth.empty-probe",
77
+ requires: { catalogue: "runtime.actions" },
78
+ setup({ catalogue }) {
79
+ publicActions = catalogue;
80
+ return {};
240
81
  }
241
- }
242
-
243
- app.instance("appConfig", createAppConfigFixture());
244
-
245
- await app.start({
246
- providers: [ActionRuntimeServiceProvider, AuthActionsServiceProvider, PublicActionProvider]
247
82
  });
248
-
249
- const actionExecutor = app.make("actionExecutor");
250
- assert.equal(
251
- actionExecutor.listDefinitions().some((definition) => definition.id.startsWith("auth.")),
252
- false
253
- );
254
- assert.deepEqual(
255
- await actionExecutor.execute({
256
- actionId: "public.ping",
257
- input: {},
258
- context: {
259
- channel: "internal",
260
- surface: "home"
261
- }
262
- }),
263
- { ok: true }
264
- );
83
+ const noAuthRuntime = createCapabilityRuntime({
84
+ providers: [createActionProvider(), AuthFeature, EmptyProbe]
85
+ });
86
+ await noAuthRuntime.start();
87
+ assert.deepEqual(publicActions.listDefinitions(), []);
265
88
  });