@jskit-ai/auth-core 0.1.99 → 0.1.101

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.99",
4
+ "version": "0.1.101",
5
5
  "kind": "runtime",
6
6
  "dependsOn": [
7
7
  "@jskit-ai/value-app-config-shared"
@@ -21,6 +21,10 @@ export default Object.freeze({
21
21
  "entrypoint": "src/server/providers/AccessCoreServiceProvider.js",
22
22
  "export": "AccessCoreServiceProvider"
23
23
  },
24
+ {
25
+ "entrypoint": "src/server/providers/AuthActionsServiceProvider.js",
26
+ "export": "AuthActionsServiceProvider"
27
+ },
24
28
  {
25
29
  "entrypoint": "src/server/providers/FastifyAuthPolicyServiceProvider.js",
26
30
  "export": "FastifyAuthPolicyServiceProvider"
@@ -49,7 +53,7 @@ export default Object.freeze({
49
53
  },
50
54
  {
51
55
  "subpath": "./server",
52
- "summary": "Exports server auth access and Fastify auth policy providers plus server auth utility modules."
56
+ "summary": "Exports server auth access, shared auth action, and Fastify auth policy providers plus server auth utility modules."
53
57
  },
54
58
  {
55
59
  "subpath": "./shared",
@@ -58,7 +62,8 @@ export default Object.freeze({
58
62
  ],
59
63
  "containerTokens": {
60
64
  "server": [
61
- "auth.access"
65
+ "auth.access",
66
+ "auth.session.events.service"
62
67
  ],
63
68
  "client": [
64
69
  "auth.access.client"
@@ -69,7 +74,7 @@ export default Object.freeze({
69
74
  "mutations": {
70
75
  "dependencies": {
71
76
  "runtime": {
72
- "@jskit-ai/kernel": "0.1.101",
77
+ "@jskit-ai/kernel": "0.1.103",
73
78
  "@fastify/cookie": "^11.0.2",
74
79
  "@fastify/csrf-protection": "^7.1.0",
75
80
  "@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.99",
3
+ "version": "0.1.101",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -8,8 +8,13 @@
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/providers/AuthActionsServiceProvider": "./src/server/providers/AuthActionsServiceProvider.js",
12
+ "./server/actions/auth.contributor": "./src/server/actions/auth.contributor.js",
13
+ "./server/services/authSessionEventsService": "./src/server/services/authSessionEventsService.js",
11
14
  "./server/authPolicyContextResolverRegistry": "./src/server/authPolicyContextResolverRegistry.js",
12
15
  "./server/authServiceDecoratorRegistry": "./src/server/authServiceDecoratorRegistry.js",
16
+ "./server/authActor": "./src/server/authActor.js",
17
+ "./server/unsupportedOperation": "./src/server/unsupportedOperation.js",
13
18
  "./server/utils": "./src/server/utils.js",
14
19
  "./server/validators": "./src/server/validators.js",
15
20
  "./server/inviteTokens": "./src/server/inviteTokens.js",
@@ -26,6 +31,8 @@
26
31
  "./shared/authDenied": "./src/shared/authDenied.js",
27
32
  "./shared/authPaths": "./src/shared/authPaths.js",
28
33
  "./shared/authConstraints": "./src/shared/authConstraints.js",
34
+ "./shared/authCapabilities": "./src/shared/authCapabilities.js",
35
+ "./shared/authSecurityStatus": "./src/shared/authSecurityStatus.js",
29
36
  "./shared/authMethods": "./src/shared/authMethods.js",
30
37
  "./shared/oauthProviders": "./src/shared/oauthProviders.js",
31
38
  "./shared/oauthCallbackParams": "./src/shared/oauthCallbackParams.js",
@@ -47,7 +54,7 @@
47
54
  "./shared/commands/authSessionReadCommand": "./src/shared/commands/authSessionReadCommand.js"
48
55
  },
49
56
  "dependencies": {
50
- "@jskit-ai/kernel": "0.1.101",
57
+ "@jskit-ai/kernel": "0.1.103",
51
58
  "@fastify/cookie": "^11.0.2",
52
59
  "@fastify/csrf-protection": "^7.1.0",
53
60
  "@fastify/rate-limit": "^10.3.0",
@@ -0,0 +1,263 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import {
3
+ emptyInputValidator
4
+ } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
5
+ import {
6
+ composeSchemaDefinitions
7
+ } from "@jskit-ai/kernel/shared/validators";
8
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
9
+ import {
10
+ authRegisterCommand,
11
+ authRegisterConfirmationResendCommand,
12
+ authLoginPasswordCommand,
13
+ authLoginOtpRequestCommand,
14
+ authLoginOtpVerifyCommand,
15
+ authLoginOAuthStartCommand,
16
+ authLoginOAuthCompleteCommand,
17
+ authPasswordResetRequestCommand,
18
+ authPasswordRecoveryCompleteCommand,
19
+ authPasswordResetCommand
20
+ } from "../../shared/commands/index.js";
21
+
22
+ const authLoginOAuthStartInput = composeSchemaDefinitions([
23
+ authLoginOAuthStartCommand.operation.params,
24
+ authLoginOAuthStartCommand.operation.query
25
+ ], {
26
+ mode: "patch",
27
+ context: "authContributor.authLoginOAuthStartInput"
28
+ });
29
+
30
+ const authLogoutOutput = deepFreeze({
31
+ schema: createSchema({
32
+ ok: { type: "boolean", required: true },
33
+ clearSession: { type: "boolean", required: true }
34
+ }),
35
+ mode: "replace"
36
+ });
37
+
38
+ function requireRequestContext(context, actionId) {
39
+ const request = context?.requestMeta?.request || null;
40
+ if (request) {
41
+ return request;
42
+ }
43
+
44
+ throw new Error(`${actionId} requires request context.`);
45
+ }
46
+
47
+ const baseAuthActions = Object.freeze([
48
+ {
49
+ id: "auth.register",
50
+ version: 1,
51
+ kind: "command",
52
+ channels: ["api", "internal"],
53
+ surfacesFrom: "enabled",
54
+ input: authRegisterCommand.operation.body,
55
+ idempotency: "none",
56
+ audit: {
57
+ actionName: "auth.register"
58
+ },
59
+ observability: {},
60
+ async execute(input, _context, deps) {
61
+ return deps.authService.register(input);
62
+ }
63
+ },
64
+ {
65
+ id: "auth.register.confirmation.resend",
66
+ version: 1,
67
+ kind: "command",
68
+ channels: ["api", "internal"],
69
+ surfacesFrom: "enabled",
70
+ input: authRegisterConfirmationResendCommand.operation.body,
71
+ idempotency: "none",
72
+ audit: {
73
+ actionName: "auth.register.confirmation.resend"
74
+ },
75
+ observability: {},
76
+ async execute(input, _context, deps) {
77
+ return deps.authService.resendRegisterConfirmation(input);
78
+ }
79
+ },
80
+ {
81
+ id: "auth.login.password",
82
+ version: 1,
83
+ kind: "command",
84
+ channels: ["api", "internal"],
85
+ surfacesFrom: "enabled",
86
+ input: authLoginPasswordCommand.operation.body,
87
+ idempotency: "none",
88
+ audit: {
89
+ actionName: "auth.login.password"
90
+ },
91
+ observability: {},
92
+ async execute(input, _context, deps) {
93
+ return deps.authService.login(input);
94
+ }
95
+ },
96
+ {
97
+ id: "auth.login.otp.request",
98
+ version: 1,
99
+ kind: "command",
100
+ channels: ["api", "internal"],
101
+ surfacesFrom: "enabled",
102
+ input: authLoginOtpRequestCommand.operation.body,
103
+ idempotency: "none",
104
+ audit: {
105
+ actionName: "auth.login.otp.request"
106
+ },
107
+ observability: {},
108
+ async execute(input, _context, deps) {
109
+ return deps.authService.requestOtpLogin(input);
110
+ }
111
+ },
112
+ {
113
+ id: "auth.login.otp.verify",
114
+ version: 1,
115
+ kind: "command",
116
+ channels: ["api", "internal"],
117
+ surfacesFrom: "enabled",
118
+ input: authLoginOtpVerifyCommand.operation.body,
119
+ idempotency: "none",
120
+ audit: {
121
+ actionName: "auth.login.otp.verify"
122
+ },
123
+ observability: {},
124
+ async execute(input, _context, deps) {
125
+ return deps.authService.verifyOtpLogin(input);
126
+ }
127
+ },
128
+ {
129
+ id: "auth.login.oauth.start",
130
+ version: 1,
131
+ kind: "command",
132
+ channels: ["api", "internal"],
133
+ surfacesFrom: "enabled",
134
+ input: authLoginOAuthStartInput,
135
+ idempotency: "none",
136
+ audit: {
137
+ actionName: "auth.login.oauth.start"
138
+ },
139
+ observability: {},
140
+ async execute(input, _context, deps) {
141
+ return deps.authService.oauthStart(input);
142
+ }
143
+ },
144
+ {
145
+ id: "auth.login.oauth.complete",
146
+ version: 1,
147
+ kind: "command",
148
+ channels: ["api", "internal"],
149
+ surfacesFrom: "enabled",
150
+ input: authLoginOAuthCompleteCommand.operation.body,
151
+ idempotency: "none",
152
+ audit: {
153
+ actionName: "auth.login.oauth.complete"
154
+ },
155
+ observability: {},
156
+ async execute(input, _context, deps) {
157
+ return deps.authService.oauthComplete(input);
158
+ }
159
+ },
160
+ {
161
+ id: "auth.password.reset.request",
162
+ version: 1,
163
+ kind: "command",
164
+ channels: ["api", "internal"],
165
+ surfacesFrom: "enabled",
166
+ input: authPasswordResetRequestCommand.operation.body,
167
+ idempotency: "none",
168
+ audit: {
169
+ actionName: "auth.password.reset.request"
170
+ },
171
+ observability: {},
172
+ async execute(input, _context, deps) {
173
+ return deps.authService.requestPasswordReset(input);
174
+ }
175
+ },
176
+ {
177
+ id: "auth.password.recovery.complete",
178
+ version: 1,
179
+ kind: "command",
180
+ channels: ["api", "internal"],
181
+ surfacesFrom: "enabled",
182
+ input: authPasswordRecoveryCompleteCommand.operation.body,
183
+ idempotency: "none",
184
+ audit: {
185
+ actionName: "auth.password.recovery.complete"
186
+ },
187
+ observability: {},
188
+ async execute(input, _context, deps) {
189
+ return deps.authService.completePasswordRecovery(input);
190
+ }
191
+ },
192
+ {
193
+ id: "auth.password.reset",
194
+ version: 1,
195
+ kind: "command",
196
+ channels: ["api", "internal"],
197
+ surfacesFrom: "enabled",
198
+ input: authPasswordResetCommand.operation.body,
199
+ idempotency: "none",
200
+ audit: {
201
+ actionName: "auth.password.reset"
202
+ },
203
+ observability: {},
204
+ async execute(input, context, deps) {
205
+ return deps.authService.resetPassword(requireRequestContext(context, "auth.password.reset"), input);
206
+ }
207
+ },
208
+ {
209
+ id: "auth.logout",
210
+ version: 1,
211
+ kind: "command",
212
+ channels: ["api", "automation", "internal"],
213
+ surfacesFrom: "enabled",
214
+ input: emptyInputValidator,
215
+ output: authLogoutOutput,
216
+ idempotency: "none",
217
+ audit: {
218
+ actionName: "auth.logout"
219
+ },
220
+ observability: {},
221
+ async execute(_input, context, deps) {
222
+ let logoutResult = {
223
+ ok: true,
224
+ clearSession: true
225
+ };
226
+ if (deps.authService && typeof deps.authService.logout === "function") {
227
+ logoutResult = await deps.authService.logout(requireRequestContext(context, "auth.logout"));
228
+ }
229
+
230
+ if (deps.authSessionEventsService && typeof deps.authSessionEventsService.notifySessionChanged === "function") {
231
+ await deps.authSessionEventsService.notifySessionChanged({
232
+ context
233
+ });
234
+ }
235
+ return {
236
+ ok: logoutResult?.ok !== false,
237
+ clearSession: logoutResult?.clearSession !== false
238
+ };
239
+ }
240
+ },
241
+ {
242
+ id: "auth.session.read",
243
+ version: 1,
244
+ kind: "query",
245
+ channels: ["api", "internal"],
246
+ surfacesFrom: "enabled",
247
+ input: emptyInputValidator,
248
+ idempotency: "none",
249
+ audit: {
250
+ actionName: "auth.session.read"
251
+ },
252
+ observability: {},
253
+ async execute(_input, context, deps) {
254
+ return deps.authService.authenticateRequest(requireRequestContext(context, "auth.session.read"));
255
+ }
256
+ }
257
+ ]);
258
+
259
+ function buildAuthActions() {
260
+ return baseAuthActions;
261
+ }
262
+
263
+ export { baseAuthActions, buildAuthActions, requireRequestContext };
@@ -0,0 +1,96 @@
1
+ import { normalizeOpaqueId } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeAuthProviderId } from "../shared/authCapabilities.js";
3
+ import { normalizeEmail } from "./utils.js";
4
+
5
+ function normalizeDisplayName(value, email = "") {
6
+ const displayName = String(value || "").trim();
7
+ if (displayName) {
8
+ return displayName;
9
+ }
10
+ const localPart = String(email || "").split("@")[0]?.trim();
11
+ return localPart || "User";
12
+ }
13
+
14
+ function normalizeProviderUserId(value) {
15
+ return String(value || "").trim();
16
+ }
17
+
18
+ function createAuthIdentityId(provider, providerUserId) {
19
+ if (!provider || !providerUserId) {
20
+ return "";
21
+ }
22
+ return `${provider}:${providerUserId}`;
23
+ }
24
+
25
+ function normalizeAuthActor(value = {}, options = {}) {
26
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
27
+ const provider = normalizeAuthProviderId(source.provider || source.authProvider || options.provider, {
28
+ fallback: normalizeAuthProviderId(options.provider, { fallback: "unknown" })
29
+ });
30
+ const providerUserId = normalizeProviderUserId(
31
+ source.providerUserId ||
32
+ source.authProviderUserSid ||
33
+ source.sub ||
34
+ source.userId ||
35
+ source.id
36
+ );
37
+ const email = normalizeEmail(source.email || "");
38
+ const displayName = normalizeDisplayName(source.displayName || source.username || source.name, email);
39
+ const appUserId = normalizeOpaqueId(source.appUserId || source.profileId || source.userProfileId, {
40
+ fallback: null
41
+ });
42
+ const authIdentityId = String(source.authIdentityId || createAuthIdentityId(provider, providerUserId)).trim();
43
+
44
+ if (!providerUserId || !email) {
45
+ return null;
46
+ }
47
+
48
+ return Object.freeze({
49
+ authIdentityId,
50
+ provider,
51
+ providerUserId,
52
+ email,
53
+ displayName,
54
+ appUserId,
55
+ profileSource: String(source.profileSource || options.profileSource || "auth-provider").trim() || "auth-provider"
56
+ });
57
+ }
58
+
59
+ function buildLegacyProfileFromActor(actorLike) {
60
+ const actor = normalizeAuthActor(actorLike);
61
+ if (!actor) {
62
+ return null;
63
+ }
64
+ const id = actor.appUserId || actor.providerUserId || actor.authIdentityId;
65
+ return Object.freeze({
66
+ id,
67
+ email: actor.email,
68
+ displayName: actor.displayName,
69
+ authProvider: actor.provider,
70
+ authProviderUserSid: actor.providerUserId
71
+ });
72
+ }
73
+
74
+ function normalizeAuthResult(value = {}, options = {}) {
75
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
76
+ const actor = normalizeAuthActor(source.actor, options) || normalizeAuthActor(source.profile, {
77
+ ...options,
78
+ profileSource: "users"
79
+ });
80
+ const profile = source.profile && typeof source.profile === "object"
81
+ ? source.profile
82
+ : buildLegacyProfileFromActor(actor);
83
+
84
+ return Object.freeze({
85
+ ...source,
86
+ ...(actor ? { actor } : {}),
87
+ ...(profile ? { profile } : {})
88
+ });
89
+ }
90
+
91
+ export {
92
+ createAuthIdentityId,
93
+ normalizeAuthActor,
94
+ buildLegacyProfileFromActor,
95
+ normalizeAuthResult
96
+ };
@@ -1,2 +1,14 @@
1
1
  export { authPolicyPlugin } from "./plugin.js";
2
2
  export { withAuthPolicy, mergeAuthPolicy } from "./routeMeta.js";
3
+ export {
4
+ createAuthIdentityId,
5
+ normalizeAuthActor,
6
+ buildLegacyProfileFromActor,
7
+ normalizeAuthResult
8
+ } from "../authActor.js";
9
+ export {
10
+ AUTH_OPERATION_UNSUPPORTED_CODE,
11
+ createUnsupportedAuthOperationError,
12
+ throwUnsupportedAuthOperation,
13
+ isUnsupportedAuthOperationError
14
+ } from "../unsupportedOperation.js";
@@ -0,0 +1,67 @@
1
+ import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
2
+ import { buildAuthActions } from "../actions/auth.contributor.js";
3
+ import { createAuthSessionEventsService } from "../services/authSessionEventsService.js";
4
+
5
+ class AuthActionsServiceProvider {
6
+ static id = "auth.actions";
7
+
8
+ static dependsOn = ["runtime.actions"];
9
+
10
+ register(app) {
11
+ if (
12
+ !app ||
13
+ typeof app.actions !== "function" ||
14
+ typeof app.has !== "function" ||
15
+ typeof app.service !== "function"
16
+ ) {
17
+ throw new Error("AuthActionsServiceProvider requires application actions()/has()/service().");
18
+ }
19
+
20
+ if (!app.has("auth.session.events.service")) {
21
+ app.service(
22
+ "auth.session.events.service",
23
+ () => createAuthSessionEventsService(),
24
+ {
25
+ events: {
26
+ notifySessionChanged: [
27
+ {
28
+ type: "entity.changed",
29
+ source: "auth",
30
+ entity: "session",
31
+ operation: "updated",
32
+ entityId: ({ result }) => result?.id,
33
+ realtime: {
34
+ event: "auth.session.changed",
35
+ audience: "actor_user"
36
+ }
37
+ },
38
+ {
39
+ type: "entity.changed",
40
+ source: "users",
41
+ entity: "bootstrap",
42
+ operation: "updated",
43
+ entityId: ({ result }) => result?.id,
44
+ realtime: {
45
+ event: "users.bootstrap.changed",
46
+ audience: "actor_user"
47
+ }
48
+ }
49
+ ]
50
+ }
51
+ }
52
+ );
53
+ }
54
+
55
+ app.actions(
56
+ withActionDefaults(buildAuthActions(), {
57
+ domain: "auth",
58
+ dependencies: {
59
+ authService: "authService",
60
+ authSessionEventsService: "auth.session.events.service"
61
+ }
62
+ })
63
+ );
64
+ }
65
+ }
66
+
67
+ export { AuthActionsServiceProvider };
@@ -38,8 +38,6 @@ function defaultHasPermission({ permission, permissions = [] } = {}) {
38
38
  class FastifyAuthPolicyServiceProvider {
39
39
  static id = "auth.policy.fastify";
40
40
 
41
- static dependsOn = ["auth.provider"];
42
-
43
41
  register(app) {
44
42
  if (!app || typeof app.has !== "function") {
45
43
  throw new Error("FastifyAuthPolicyServiceProvider requires application has().");
@@ -70,15 +68,18 @@ class FastifyAuthPolicyServiceProvider {
70
68
  if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
71
69
  throw new Error("FastifyAuthPolicyServiceProvider requires application make()/has().");
72
70
  }
73
- if (!app.has("authService")) {
74
- throw new Error("FastifyAuthPolicyServiceProvider requires authService binding.");
75
- }
76
-
77
71
  const env = app.has("jskit.env") ? app.make("jskit.env") : {};
78
72
  const fastify = app.make("jskit.fastify");
79
73
 
80
74
  const pluginDeps = {
81
75
  resolveActor: async (request) => {
76
+ if (!app.has("authService")) {
77
+ return {
78
+ authenticated: false,
79
+ actor: null,
80
+ transientFailure: false
81
+ };
82
+ }
82
83
  const authService = app.make("authService");
83
84
  if (authService && typeof authService.authenticateRequest === "function") {
84
85
  return authService.authenticateRequest(request);
@@ -0,0 +1,30 @@
1
+ import { normalizeOpaqueId } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ function resolveActorId(context = {}) {
4
+ return normalizeOpaqueId(
5
+ context?.actor?.id ||
6
+ context?.actor?.appUserId ||
7
+ context?.actor?.providerUserId ||
8
+ context?.profile?.id,
9
+ { fallback: null }
10
+ );
11
+ }
12
+
13
+ function createAuthSessionEventsService() {
14
+ async function notifySessionChanged(options = {}) {
15
+ const actorId = resolveActorId(options?.context || {});
16
+ if (!actorId) {
17
+ return null;
18
+ }
19
+
20
+ return {
21
+ id: actorId
22
+ };
23
+ }
24
+
25
+ return Object.freeze({
26
+ notifySessionChanged
27
+ });
28
+ }
29
+
30
+ export { createAuthSessionEventsService };
@@ -0,0 +1,28 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+
3
+ const AUTH_OPERATION_UNSUPPORTED_CODE = "AUTH_OPERATION_UNSUPPORTED";
4
+
5
+ function createUnsupportedAuthOperationError(operation, message = "") {
6
+ const operationName = String(operation || "auth operation").trim() || "auth operation";
7
+ return new AppError(501, message || `Auth operation "${operationName}" is not supported by the active provider.`, {
8
+ code: AUTH_OPERATION_UNSUPPORTED_CODE,
9
+ details: {
10
+ operation: operationName
11
+ }
12
+ });
13
+ }
14
+
15
+ function throwUnsupportedAuthOperation(operation, message = "") {
16
+ throw createUnsupportedAuthOperationError(operation, message);
17
+ }
18
+
19
+ function isUnsupportedAuthOperationError(error) {
20
+ return String(error?.code || "") === AUTH_OPERATION_UNSUPPORTED_CODE;
21
+ }
22
+
23
+ export {
24
+ AUTH_OPERATION_UNSUPPORTED_CODE,
25
+ createUnsupportedAuthOperationError,
26
+ throwUnsupportedAuthOperation,
27
+ isUnsupportedAuthOperationError
28
+ };