@jskit-ai/auth-core 0.1.83 → 0.1.85

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.83",
4
+ "version": "0.1.85",
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.84",
72
+ "@jskit-ai/kernel": "0.1.86",
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.83",
3
+ "version": "0.1.85",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -23,6 +23,7 @@
23
23
  "./shared": "./src/shared/index.js",
24
24
  "./shared/authApi": "./src/shared/authApi.js",
25
25
  "./shared/signOutFlow": "./src/shared/signOutFlow.js",
26
+ "./shared/authDenied": "./src/shared/authDenied.js",
26
27
  "./shared/authPaths": "./src/shared/authPaths.js",
27
28
  "./shared/authConstraints": "./src/shared/authConstraints.js",
28
29
  "./shared/authMethods": "./src/shared/authMethods.js",
@@ -46,7 +47,7 @@
46
47
  "./shared/commands/authSessionReadCommand": "./src/shared/commands/authSessionReadCommand.js"
47
48
  },
48
49
  "dependencies": {
49
- "@jskit-ai/kernel": "0.1.84",
50
+ "@jskit-ai/kernel": "0.1.86",
50
51
  "@fastify/cookie": "^11.0.2",
51
52
  "@fastify/csrf-protection": "^7.1.0",
52
53
  "@fastify/rate-limit": "^10.3.0",
@@ -0,0 +1,70 @@
1
+ const AUTH_DENIED_CODE_MAX_LENGTH = 80;
2
+ const AUTH_DENIED_MESSAGE_MAX_LENGTH = 240;
3
+ const AUTH_DENIED_CODE_PATTERN = "^[a-z][a-z0-9_.:-]{1,79}$";
4
+
5
+ const AUTH_DENIED_CODES = Object.freeze({
6
+ NOT_ALLOWLISTED: "not_allowlisted",
7
+ BLOCKED: "blocked"
8
+ });
9
+
10
+ const AUTH_DENIED_DEFAULT_MESSAGES = Object.freeze({
11
+ [AUTH_DENIED_CODES.NOT_ALLOWLISTED]: "This account is not allowed to access this application.",
12
+ [AUTH_DENIED_CODES.BLOCKED]: "This account has been blocked from accessing this application."
13
+ });
14
+
15
+ const AUTH_DENIED_LOGIN_MESSAGES = Object.freeze({
16
+ [AUTH_DENIED_CODES.NOT_ALLOWLISTED]:
17
+ "Sign-in succeeded, but this account is not allowed to access this application.",
18
+ [AUTH_DENIED_CODES.BLOCKED]:
19
+ "Sign-in succeeded, but this account has been blocked from accessing this application."
20
+ });
21
+
22
+ function normalizeAuthDeniedCode(value = "") {
23
+ const code = String(value || "")
24
+ .trim()
25
+ .toLowerCase();
26
+ if (!code || code.length > AUTH_DENIED_CODE_MAX_LENGTH) {
27
+ return "";
28
+ }
29
+ return new RegExp(AUTH_DENIED_CODE_PATTERN).test(code) ? code : "";
30
+ }
31
+
32
+ function normalizeAuthDenied(input = null) {
33
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
34
+ return null;
35
+ }
36
+
37
+ const code = normalizeAuthDeniedCode(input.code);
38
+ if (!code) {
39
+ return null;
40
+ }
41
+
42
+ const explicitMessage = String(input.message || "").trim();
43
+ const message = explicitMessage || AUTH_DENIED_DEFAULT_MESSAGES[code] || "This account cannot access this application.";
44
+
45
+ return Object.freeze({
46
+ code,
47
+ message: message.slice(0, AUTH_DENIED_MESSAGE_MAX_LENGTH)
48
+ });
49
+ }
50
+
51
+ function resolveAuthDeniedLoginMessage(input = null) {
52
+ const authDenied = normalizeAuthDenied(input);
53
+ if (!authDenied) {
54
+ return "";
55
+ }
56
+
57
+ return AUTH_DENIED_LOGIN_MESSAGES[authDenied.code] || authDenied.message;
58
+ }
59
+
60
+ export {
61
+ AUTH_DENIED_CODE_MAX_LENGTH,
62
+ AUTH_DENIED_CODE_PATTERN,
63
+ AUTH_DENIED_CODES,
64
+ AUTH_DENIED_DEFAULT_MESSAGES,
65
+ AUTH_DENIED_LOGIN_MESSAGES,
66
+ AUTH_DENIED_MESSAGE_MAX_LENGTH,
67
+ normalizeAuthDenied,
68
+ normalizeAuthDeniedCode,
69
+ resolveAuthDeniedLoginMessage
70
+ };
@@ -1,5 +1,10 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
+ import {
4
+ AUTH_DENIED_CODE_MAX_LENGTH,
5
+ AUTH_DENIED_CODE_PATTERN,
6
+ AUTH_DENIED_MESSAGE_MAX_LENGTH
7
+ } from "../authDenied.js";
3
8
  import {
4
9
  AUTH_ACCESS_TOKEN_MAX_LENGTH,
5
10
  AUTH_EMAIL_MAX_LENGTH,
@@ -209,6 +214,22 @@ const oauthProviderCatalogEntryOutputValidator = deepFreeze({
209
214
  mode: "replace"
210
215
  });
211
216
 
217
+ const authDeniedOutputSchema = createSchema({
218
+ code: {
219
+ type: "string",
220
+ required: true,
221
+ minLength: 2,
222
+ maxLength: AUTH_DENIED_CODE_MAX_LENGTH,
223
+ pattern: AUTH_DENIED_CODE_PATTERN
224
+ },
225
+ message: {
226
+ type: "string",
227
+ required: true,
228
+ minLength: 1,
229
+ maxLength: AUTH_DENIED_MESSAGE_MAX_LENGTH
230
+ }
231
+ });
232
+
212
233
  const sessionOutputSchema = createSchema({
213
234
  authenticated: { type: "boolean", required: true },
214
235
  username: { type: "string", required: false, minLength: 1, maxLength: 120 },
@@ -222,6 +243,11 @@ const sessionOutputSchema = createSchema({
222
243
  maxLength: 200
223
244
  }
224
245
  },
246
+ authDenied: {
247
+ type: "object",
248
+ required: false,
249
+ schema: authDeniedOutputSchema
250
+ },
225
251
  csrfToken: { type: "string", required: true, minLength: 1 },
226
252
  oauthProviders: {
227
253
  type: "array",
@@ -306,6 +332,7 @@ export {
306
332
  devLoginAsOutputValidator,
307
333
  logoutOutputValidator,
308
334
  oauthProviderCatalogEntryOutputValidator,
335
+ authDeniedOutputSchema,
309
336
  sessionOutputValidator,
310
337
  sessionUnavailableOutputValidator,
311
338
  createCommandMessages
@@ -1,3 +1,10 @@
1
1
  export { createApi } from "./authApi.js";
2
2
  export { runAuthSignOutFlow } from "./signOutFlow.js";
3
+ export {
4
+ AUTH_DENIED_CODES,
5
+ AUTH_DENIED_DEFAULT_MESSAGES,
6
+ AUTH_DENIED_LOGIN_MESSAGES,
7
+ normalizeAuthDenied,
8
+ resolveAuthDeniedLoginMessage
9
+ } from "./authDenied.js";
3
10
  export { AUTH_PATHS, buildAuthOauthStartPath } from "./authPaths.js";
@@ -0,0 +1,45 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ AUTH_DENIED_CODES,
5
+ normalizeAuthDenied,
6
+ resolveAuthDeniedLoginMessage
7
+ } from "../src/shared/authDenied.js";
8
+
9
+ test("normalizeAuthDenied preserves stable denial codes and messages", () => {
10
+ assert.deepEqual(
11
+ normalizeAuthDenied({
12
+ code: "NOT_ALLOWLISTED",
13
+ message: " Custom denial. "
14
+ }),
15
+ {
16
+ code: AUTH_DENIED_CODES.NOT_ALLOWLISTED,
17
+ message: "Custom denial."
18
+ }
19
+ );
20
+
21
+ assert.deepEqual(
22
+ normalizeAuthDenied({
23
+ code: AUTH_DENIED_CODES.BLOCKED
24
+ }),
25
+ {
26
+ code: AUTH_DENIED_CODES.BLOCKED,
27
+ message: "This account has been blocked from accessing this application."
28
+ }
29
+ );
30
+
31
+ assert.equal(normalizeAuthDenied({ message: "Missing code" }), null);
32
+ assert.equal(normalizeAuthDenied({ code: "../bad" }), null);
33
+ });
34
+
35
+ test("resolveAuthDeniedLoginMessage maps default denial reasons to post-login messages", () => {
36
+ assert.equal(
37
+ resolveAuthDeniedLoginMessage({ code: AUTH_DENIED_CODES.NOT_ALLOWLISTED }),
38
+ "Sign-in succeeded, but this account is not allowed to access this application."
39
+ );
40
+ assert.equal(
41
+ resolveAuthDeniedLoginMessage({ code: AUTH_DENIED_CODES.BLOCKED }),
42
+ "Sign-in succeeded, but this account has been blocked from accessing this application."
43
+ );
44
+ assert.equal(resolveAuthDeniedLoginMessage(null), "");
45
+ });
@@ -104,6 +104,9 @@ test("auth session and oauth start commands expose explicit nested response sche
104
104
  assert.equal(sessionResponseSchema.properties.oauthProviders.items["x-json-rest-schema"]?.castType, "object");
105
105
  assert.equal(Array.isArray(sessionResponseSchema.properties.oauthProviders.items.allOf), true);
106
106
  assert.match(sessionResponseSchema.properties.oauthProviders.items.allOf[0]?.$ref || "", /^#\/definitions\//);
107
+ assert.equal(sessionResponseSchema.properties.authDenied["x-json-rest-schema"]?.castType, "object");
108
+ assert.equal(Array.isArray(sessionResponseSchema.properties.authDenied.allOf), true);
109
+ assert.match(sessionResponseSchema.properties.authDenied.allOf[0]?.$ref || "", /^#\/definitions\//);
107
110
  assert.equal(Array.isArray(sessionUnavailableSchema.properties.oauthDefaultProvider.anyOf), true);
108
111
  assert.equal(oauthStartResponseSchema.properties.provider.type, "string");
109
112
  assert.equal(oauthStartResponseSchema.properties.returnTo.type, "string");