@starci/eslint-canon-fe 9.10.0 → 9.11.0
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.
- package/CHANGELOG.md +4 -0
- package/dist/fe-stack.d.ts +5 -0
- package/dist/fe-stack.js +34 -0
- package/dist/lib/scope.d.ts +0 -8
- package/dist/lib/scope.js +0 -10
- package/dist/lib/why.d.ts +5 -0
- package/dist/lib/why.js +5 -0
- package/dist/transport.d.ts +2 -1
- package/dist/transport.js +4 -28
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +30 -9
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
- package/runtime/knowledge/hfs/slots.yaml +71 -22
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
- package/runtime/scripts/hfs/manifest-shape.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.11.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
- Changed: `modules/auth` holds the front-end transport rules like `modules/api`: no auth door file takes axios or `fetch`, and no file keeps module-level mutable state (the access token lives in the `@starci/auth` client); the stack concern `auth` names `@starci/auth`.
|
|
6
|
+
|
|
3
7
|
## 9.10.0
|
|
4
8
|
|
|
5
9
|
- Added: `starci-fe/directive-first` (R369, `FE_DIRECTIVE_FIRST`) at error: a `"use client"` or `"use server"` directive is the first statement of its module; below an import it does nothing and `next build` refuses the file.
|
package/dist/fe-stack.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ import { type Rules, type Recommended } from "./lib/rule.js";
|
|
|
4
4
|
* No import (static, re-export, dynamic or `require`) of a package the fe-stack catalog forbids.
|
|
5
5
|
*/
|
|
6
6
|
export declare const feStackImport: import("./lib/rule.js").Rule<"forbidden", []>;
|
|
7
|
+
/**
|
|
8
|
+
* The SWR extension points belong to `@starci/next-data`: `pollingRestart` is the one middleware and `createViewerCache` the one
|
|
9
|
+
* binding of the viewer's cache, so an app that imports the `Middleware` or `ScopedMutator` type is writing a second one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const swrExtensionFromPackage: import("./lib/rule.js").Rule<"own", []>;
|
|
7
12
|
/** The rules this law contributes to the plugin. */
|
|
8
13
|
export declare const rules: Rules;
|
|
9
14
|
/** Every rule is an error: a second library for a concern is the defect, not a style. */
|
package/dist/fe-stack.js
CHANGED
|
@@ -59,9 +59,43 @@ export const feStackImport = defineRule({
|
|
|
59
59
|
};
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
|
+
/** The SWR types an app needs only to write its own middleware or to bind the writes of a viewer's cache by hand. */
|
|
63
|
+
const PACKAGE_OWNED_SWR_TYPES = new Set(["Middleware", "ScopedMutator"]);
|
|
64
|
+
/**
|
|
65
|
+
* The SWR extension points belong to `@starci/next-data`: `pollingRestart` is the one middleware and `createViewerCache` the one
|
|
66
|
+
* binding of the viewer's cache, so an app that imports the `Middleware` or `ScopedMutator` type is writing a second one.
|
|
67
|
+
*/
|
|
68
|
+
export const swrExtensionFromPackage = defineRule({
|
|
69
|
+
meta: {
|
|
70
|
+
type: "problem",
|
|
71
|
+
docs: { description: "An app writes no SWR middleware and no cache binding of its own." },
|
|
72
|
+
schema: [],
|
|
73
|
+
messages: {
|
|
74
|
+
own: "`{{name}}` of `swr` is the type of an SWR extension the stack already ships. Use `pollingRestart`, `viewerSwrConfig` and `createViewerCache` of `@starci/next-data` (FE-DATA-SWR, FE-DATA-KEY) instead of writing a second one.",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
create(context) {
|
|
78
|
+
if (!isProductSource(context))
|
|
79
|
+
return {};
|
|
80
|
+
return {
|
|
81
|
+
ImportDeclaration(node) {
|
|
82
|
+
if (node.source.value !== "swr")
|
|
83
|
+
return;
|
|
84
|
+
for (const specifier of node.specifiers) {
|
|
85
|
+
if (specifier.type !== "ImportSpecifier")
|
|
86
|
+
continue;
|
|
87
|
+
const name = specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
|
|
88
|
+
if (PACKAGE_OWNED_SWR_TYPES.has(name))
|
|
89
|
+
context.report({ node: specifier, messageId: "own", data: { name } });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
});
|
|
62
95
|
/** The rules this law contributes to the plugin. */
|
|
63
96
|
export const rules = {
|
|
64
97
|
"fe-stack-import": feStackImport,
|
|
98
|
+
"swr-extension-from-package": swrExtensionFromPackage,
|
|
65
99
|
};
|
|
66
100
|
/** Every rule is an error: a second library for a concern is the defect, not a style. */
|
|
67
101
|
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
package/dist/lib/scope.d.ts
CHANGED
|
@@ -39,14 +39,6 @@ export declare const isProductSource: (context: AnyContext) => boolean;
|
|
|
39
39
|
export declare const isComponentFile: (context: AnyContext) => boolean;
|
|
40
40
|
/** True when the given file (an absolute or repo-relative name, e.g. where an import resolves to) sits in a component layer of slot `fe.components` or `fe.package.ui`. */
|
|
41
41
|
export declare const isComponentPath: (context: AnyContext, file: string) => boolean;
|
|
42
|
-
/**
|
|
43
|
-
* The one auth door of the session issuer's client: `modules/auth/auth-door.ts` (slot `fe.modules.auth`). It alone calls the app's
|
|
44
|
-
* own back-end auth doors (login, signup, refresh, logout, password reset, resend verification) with the REST client, credentials
|
|
45
|
-
* included; every other file of `modules/auth` and every other module stays outside the transport.
|
|
46
|
-
*/
|
|
47
|
-
export declare const isAuthDoor: (context: AnyContext) => boolean;
|
|
48
|
-
/** The in-memory access-token store of `modules/auth` (`modules/auth/token-store.ts`), the one module-level mutable state the auth client keeps. */
|
|
49
|
-
export declare const isAuthTokenStore: (context: AnyContext) => boolean;
|
|
50
42
|
/** The one place that may read the environment: slot `fe.modules.config`. */
|
|
51
43
|
export declare const isConfigModule: (context: AnyContext) => boolean;
|
|
52
44
|
/**
|
package/dist/lib/scope.js
CHANGED
|
@@ -48,16 +48,6 @@ export const isComponentPath = (context, file) => {
|
|
|
48
48
|
const found = hfsOf(context).classify(file);
|
|
49
49
|
return (found.slot === "fe.components" || found.slot === "fe.package.ui") && found.kind !== undefined;
|
|
50
50
|
};
|
|
51
|
-
/** The base name of the linted file. */
|
|
52
|
-
const baseNameOf = (context) => (context.filename || context.getFilename()).split(/[\\/]/).at(-1) ?? "";
|
|
53
|
-
/**
|
|
54
|
-
* The one auth door of the session issuer's client: `modules/auth/auth-door.ts` (slot `fe.modules.auth`). It alone calls the app's
|
|
55
|
-
* own back-end auth doors (login, signup, refresh, logout, password reset, resend verification) with the REST client, credentials
|
|
56
|
-
* included; every other file of `modules/auth` and every other module stays outside the transport.
|
|
57
|
-
*/
|
|
58
|
-
export const isAuthDoor = (context) => inSlot(context, "fe.modules.auth") && baseNameOf(context) === "auth-door.ts";
|
|
59
|
-
/** The in-memory access-token store of `modules/auth` (`modules/auth/token-store.ts`), the one module-level mutable state the auth client keeps. */
|
|
60
|
-
export const isAuthTokenStore = (context) => inSlot(context, "fe.modules.auth") && baseNameOf(context) === "token-store.ts";
|
|
61
51
|
/** The one place that may read the environment: slot `fe.modules.config`. */
|
|
62
52
|
export const isConfigModule = (context) => inSlot(context, "fe.modules.config");
|
|
63
53
|
/**
|
package/dist/lib/why.d.ts
CHANGED
package/dist/lib/why.js
CHANGED
|
@@ -392,6 +392,11 @@ export const why = {
|
|
|
392
392
|
en: "`<file>` starts a recurring timer that reads the wall clock for live time labels.",
|
|
393
393
|
fix: "Render Grammar RelativeTime with the target dateTime, locale and mode; its shared ticker owns live relative time and countdown updates.",
|
|
394
394
|
},
|
|
395
|
+
"swr-extension-from-package": {
|
|
396
|
+
code: "FE_STACK_FORBIDDEN",
|
|
397
|
+
en: "`<file>` imports the `Middleware` or `ScopedMutator` type of `swr`, so it writes an SWR middleware or a cache binding the stack already ships.",
|
|
398
|
+
fix: "List `viewerSwrConfig` (it carries `pollingRestart`) in the app's SWRConfig and write through the `createViewerCache()` instance of `@starci/next-data`.",
|
|
399
|
+
},
|
|
395
400
|
"fe-stack-import": {
|
|
396
401
|
code: "FE_STACK_FORBIDDEN",
|
|
397
402
|
en: "`<file>` imports `<what>`, a package or scoped package family the front-end stack forbids.",
|
package/dist/transport.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* standing in for a status.
|
|
20
20
|
*/
|
|
21
21
|
import { type Recommended, type Rules } from "./lib/rule.js";
|
|
22
|
+
/** The HTTP methods of the REST client whose first argument is the request path. */
|
|
22
23
|
/**
|
|
23
24
|
* `fetch` is reached only in the API layer, each canonical transport library only in its home, and no other HTTP library exists.
|
|
24
25
|
*
|
|
@@ -28,7 +29,7 @@ import { type Recommended, type Rules } from "./lib/rule.js";
|
|
|
28
29
|
* `socket.io-client` only by `hooks/socketio/`. Native `WebSocket` is refused everywhere, including the API layer.
|
|
29
30
|
* Every other file reaches the back end through an SWR hook over an operation.
|
|
30
31
|
*/
|
|
31
|
-
export declare const fetchOnlyInApiClient: import("./lib/rule.js").Rule<"library" | "outside" | "channel" | "websocket" | "xhr" | "placed"
|
|
32
|
+
export declare const fetchOnlyInApiClient: import("./lib/rule.js").Rule<"library" | "outside" | "channel" | "websocket" | "xhr" | "placed", []>;
|
|
32
33
|
/** Every `fetch` of the API layer (a custom fetch handed to the Apollo http link, a raw call) carries an abort signal, so a hung backend cannot hang a reader. */
|
|
33
34
|
export declare const clientFetchHasSignal: import("./lib/rule.js").Rule<"signal", []>;
|
|
34
35
|
/** No module-level mutable state in the API layer: a token or locale in a `let` leaks across requests. */
|
package/dist/transport.js
CHANGED
|
@@ -23,7 +23,7 @@ import { isNode } from "./lib/ast.js";
|
|
|
23
23
|
import { hfsOf } from "./lib/hfs.js";
|
|
24
24
|
import { forbiddingConcern, packageNameOf } from "./lib/fe-stack.js";
|
|
25
25
|
import { defineRule } from "./lib/rule.js";
|
|
26
|
-
import { globalReferences, inSlot, isApiClient, isApiLayer, isApiRest,
|
|
26
|
+
import { globalReferences, inSlot, isApiClient, isApiLayer, isApiRest, isOutcomeModule } from "./lib/scope.js";
|
|
27
27
|
import { typed } from "./lib/types.js";
|
|
28
28
|
/**
|
|
29
29
|
* The canonical transport libraries and the one place each may be imported (fe-stack graphql, rest and realtime): Apollo in
|
|
@@ -31,7 +31,7 @@ import { typed } from "./lib/types.js";
|
|
|
31
31
|
*/
|
|
32
32
|
const PLACED_TRANSPORTS = new Map([
|
|
33
33
|
["@apollo/client", { where: "the API layer (`modules/api`)", allowed: (context) => isApiLayer(context) }],
|
|
34
|
-
["axios", { where: "the REST calls of the API layer (`modules/api/rest/`)
|
|
34
|
+
["axios", { where: "the REST calls of the API layer (`modules/api/rest/`)", allowed: (context) => isApiRest(context) }],
|
|
35
35
|
["socket.io-client", { where: "`hooks/socketio/`", allowed: (context) => inSlot(context, "fe.hooks.socketio") }],
|
|
36
36
|
["graphql-ws", { where: "the Apollo clients of the API layer (`modules/api/graphql/clients/`, or the shared api package's `graphql/clients/`)", allowed: (context) => isApiClient(context) }],
|
|
37
37
|
]);
|
|
@@ -140,26 +140,6 @@ const transportUses = (context) => {
|
|
|
140
140
|
};
|
|
141
141
|
// -- FE-TRANSPORT-1 --------------------------------------------------------------------------------
|
|
142
142
|
/** The HTTP methods of the REST client whose first argument is the request path. */
|
|
143
|
-
const REST_METHODS = new Set(["get", "post", "put", "patch", "delete", "request"]);
|
|
144
|
-
/** The static text of a path argument (a literal, or a template's fixed parts), or null when nothing about it is fixed. */
|
|
145
|
-
const staticPath = (node) => {
|
|
146
|
-
if (node?.type === "Literal" && typeof node.value === "string")
|
|
147
|
-
return node.value;
|
|
148
|
-
if (node?.type === "TemplateLiteral")
|
|
149
|
-
return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("<expression>");
|
|
150
|
-
return null;
|
|
151
|
-
};
|
|
152
|
-
/** A path segment `auth/`: the app's own back-end auth doors. */
|
|
153
|
-
const AUTH_DOOR_PATH = /(?:^|\/)auth\//;
|
|
154
|
-
/** In the auth door, a REST call whose path the rule can read must name an `/auth/` door; a fully dynamic path is left to review. */
|
|
155
|
-
const judgeAuthDoorCall = (context, node) => {
|
|
156
|
-
if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || !REST_METHODS.has(node.callee.property.name))
|
|
157
|
-
return;
|
|
158
|
-
const path = staticPath(node.arguments[0]);
|
|
159
|
-
if (path === null || AUTH_DOOR_PATH.test(path))
|
|
160
|
-
return;
|
|
161
|
-
context.report({ node, messageId: "authDoor", data: { call: `.${node.callee.property.name}()`, path } });
|
|
162
|
-
};
|
|
163
143
|
/**
|
|
164
144
|
* `fetch` is reached only in the API layer, each canonical transport library only in its home, and no other HTTP library exists.
|
|
165
145
|
*
|
|
@@ -180,13 +160,11 @@ export const fetchOnlyInApiClient = defineRule({
|
|
|
180
160
|
library: "`{{name}}` is a second HTTP transport. The front end talks to its back end through Apollo (`modules/api/graphql`) and the axios REST calls (`modules/api/rest`); another library is another set of timeout, retry and error rules. Use an operation of `modules/api`.",
|
|
181
161
|
placed: "`{{name}}` is imported outside {{where}}, the one place the front-end stack gives it. A component or hook that holds the transport itself skips the link chain (token, locale, retry, timeout) and the one result vocabulary. Call an operation of `modules/api` through its SWR hook (`hooks/swr/`), or listen through `hooks/socketio/`.",
|
|
182
162
|
xhr: "`XMLHttpRequest` is a second transport. Use an operation of the API layer (`modules/api`).",
|
|
183
|
-
authDoor: "`{{call}}` calls `{{path}}` from the auth door. `modules/auth/auth-door.ts` calls only the app's own back-end auth doors (`/auth/...`: login, signup, refresh, logout, password reset, resend verification); every other REST call is `modules/api/rest/`.",
|
|
184
163
|
websocket: "Native `WebSocket` is a second realtime transport. Use socket.io-client in `hooks/socketio/`, the front end's realtime owner.",
|
|
185
164
|
},
|
|
186
165
|
},
|
|
187
166
|
create(context) {
|
|
188
167
|
const layer = isApiLayer(context);
|
|
189
|
-
const authDoor = isAuthDoor(context);
|
|
190
168
|
const library = (node, source) => {
|
|
191
169
|
const name = packageNameOf(source);
|
|
192
170
|
if (name === null || forbiddingConcern(source))
|
|
@@ -231,8 +209,6 @@ export const fetchOnlyInApiClient = defineRule({
|
|
|
231
209
|
library(node, node.source.value);
|
|
232
210
|
},
|
|
233
211
|
CallExpression(node) {
|
|
234
|
-
if (authDoor)
|
|
235
|
-
judgeAuthDoorCall(context, node);
|
|
236
212
|
// `require("axios")` where `require` is the CommonJS global.
|
|
237
213
|
const [first] = node.arguments;
|
|
238
214
|
if (node.callee.type !== "Identifier" || node.callee.name !== "require" || first?.type !== "Literal")
|
|
@@ -290,14 +266,14 @@ const isTransportFile = (context) => isApiLayer(context) && !hfsOf(context).rela
|
|
|
290
266
|
export const noSharedTransportState = defineRule({
|
|
291
267
|
meta: {
|
|
292
268
|
type: "problem",
|
|
293
|
-
docs: { description: "The API layer (`modules/api/**`) and `modules/auth` hold no module-level `let`/`var
|
|
269
|
+
docs: { description: "The API layer (`modules/api/**`) and `modules/auth` hold no module-level `let`/`var`." },
|
|
294
270
|
schema: [],
|
|
295
271
|
messages: {
|
|
296
272
|
shared: "A module-level `{{kind}}` in the API layer. A token or locale kept there is shared by every request on the server and by every reader in one tab, so one reader's credential can answer another's request. Pass the credential as a parameter or read it from context.",
|
|
297
273
|
},
|
|
298
274
|
},
|
|
299
275
|
create(context) {
|
|
300
|
-
if (!isTransportFile(context) && !
|
|
276
|
+
if (!isTransportFile(context) && !inSlot(context, "fe.modules.auth"))
|
|
301
277
|
return {};
|
|
302
278
|
const check = (node) => {
|
|
303
279
|
const declaration = node.type === "VariableDeclaration" ? node : node.type === "ExportNamedDeclaration" ? node.declaration : undefined;
|
package/package.json
CHANGED
|
@@ -30,36 +30,43 @@ pins:
|
|
|
30
30
|
side: fe
|
|
31
31
|
source: packages/event-stream/package.json
|
|
32
32
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
33
|
+
'@starci/next-data':
|
|
34
|
+
version: 0.1.0
|
|
35
|
+
group: starci
|
|
36
|
+
install: registry
|
|
37
|
+
side: fe
|
|
38
|
+
source: packages/next-data/package.json
|
|
39
|
+
why: 'the SWR data helpers of every front end (fe-stack data, owner:next-data-package): pollingRestart, the viewer and tenant key helpers, viewerSwrConfig and the viewer cache; each operation hook stays in the app and the package holds no component.'
|
|
33
40
|
'@starci/grammar':
|
|
34
|
-
version: 0.21.
|
|
41
|
+
version: 0.21.14
|
|
35
42
|
group: starci
|
|
36
43
|
install: registry
|
|
37
44
|
side: fe
|
|
38
45
|
source: packages/grammar/package.json
|
|
39
46
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
40
47
|
'@starci/eslint-canon-be':
|
|
41
|
-
version: 3.
|
|
48
|
+
version: 3.13.0
|
|
42
49
|
group: starci
|
|
43
50
|
install: registry
|
|
44
51
|
side: be
|
|
45
52
|
source: packages/eslint-canon-be/package.json
|
|
46
53
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
47
54
|
'@starci/eslint-canon-fe':
|
|
48
|
-
version: 9.
|
|
55
|
+
version: 9.11.0
|
|
49
56
|
group: starci
|
|
50
57
|
install: registry
|
|
51
58
|
side: fe
|
|
52
59
|
source: packages/eslint-canon-fe/package.json
|
|
53
60
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
54
61
|
'@starci/eslint-grammar-core':
|
|
55
|
-
version: 0.21.
|
|
62
|
+
version: 0.21.14
|
|
56
63
|
group: starci
|
|
57
64
|
install: registry
|
|
58
65
|
side: fe
|
|
59
66
|
source: packages/eslint-grammar-core/package.json
|
|
60
67
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
61
68
|
'@starci/eslint-grammar-offset-pop':
|
|
62
|
-
version: 0.21.
|
|
69
|
+
version: 0.21.14
|
|
63
70
|
group: starci
|
|
64
71
|
install: registry
|
|
65
72
|
side: fe
|
|
@@ -85,7 +92,7 @@ pins:
|
|
|
85
92
|
side: both
|
|
86
93
|
source: packages/prettier-config/package.json
|
|
87
94
|
'@starci/jest-preset':
|
|
88
|
-
version: 2.6.
|
|
95
|
+
version: 2.6.1
|
|
89
96
|
group: starci
|
|
90
97
|
install: registry
|
|
91
98
|
side: be
|
|
@@ -105,8 +112,22 @@ pins:
|
|
|
105
112
|
side: be
|
|
106
113
|
source: packages/ai-router/package.json
|
|
107
114
|
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
115
|
+
'@starci/auth':
|
|
116
|
+
version: 0.1.0
|
|
117
|
+
group: starci
|
|
118
|
+
install: registry
|
|
119
|
+
side: both
|
|
120
|
+
source: packages/auth/package.json
|
|
121
|
+
why: 'the lite sign-in: the NestJS module (EdDSA access tokens with a multi-key JWKS, passwords, Google OAuth with state and PKCE, rotating refresh sessions, the sign-in budget, the refresh cookie), the front-end client and its React bindings, the door contracts and test doubles; the one importer of the identity libraries, reached by a back end only through integrations/passport; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
122
|
+
'@starci/object-storage':
|
|
123
|
+
version: 0.1.0
|
|
124
|
+
group: starci
|
|
125
|
+
install: registry
|
|
126
|
+
side: be
|
|
127
|
+
source: packages/object-storage/package.json
|
|
128
|
+
why: 'object storage of a back end: a NestJS module and the ObjectStorage port over S3-compatible storage (presigned PUT and GET that sign the content type and size, head, get, put, list, delete, bucket setup, size and content-type limits, coded errors), with @starci/object-storage/testing for its specs; the only importer of @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner (R331).'
|
|
108
129
|
'@starci/hfs':
|
|
109
|
-
version: 5.
|
|
130
|
+
version: 5.2.0
|
|
110
131
|
group: starci
|
|
111
132
|
install: registry
|
|
112
133
|
side: both
|
|
@@ -543,12 +564,12 @@ pins:
|
|
|
543
564
|
version: 3.1150.0
|
|
544
565
|
group: framework
|
|
545
566
|
side: be
|
|
546
|
-
why: 'the S3 transport
|
|
567
|
+
why: 'the S3 transport inside @starci/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces), which depends on it at exactly this version and which no app file imports; 3.1150.0 is the release of 2026-10-12, chosen over newer ones that had not aged two weeks.'
|
|
547
568
|
'@aws-sdk/s3-request-presigner':
|
|
548
569
|
version: 3.1150.0
|
|
549
570
|
group: framework
|
|
550
571
|
side: be
|
|
551
|
-
why: 'signs the presigned PUT and GET
|
|
572
|
+
why: 'signs the presigned PUT and GET inside @starci/object-storage locally, the same release as client-s3; no app file imports it.'
|
|
552
573
|
'@apollo/server':
|
|
553
574
|
version: 5.5.1
|
|
554
575
|
group: framework
|
|
@@ -37,17 +37,17 @@ concerns:
|
|
|
37
37
|
- id: rest
|
|
38
38
|
tier: core
|
|
39
39
|
packages: [axios]
|
|
40
|
-
home: [fe.modules.api
|
|
40
|
+
home: [fe.modules.api]
|
|
41
41
|
pattern: FE-API-REST
|
|
42
42
|
forbids: [ky, superagent, node-fetch, cross-fetch, isomorphic-fetch]
|
|
43
|
-
why: modules/api/rest/<area>/*.ts for uploads and presigned URLs,
|
|
43
|
+
why: modules/api/rest/<area>/*.ts for uploads and presigned URLs, only; GraphQL carries every other call.
|
|
44
44
|
- id: data
|
|
45
45
|
tier: core
|
|
46
|
-
packages: [swr]
|
|
46
|
+
packages: [swr, '@starci/next-data']
|
|
47
47
|
home: [fe.hooks.swr]
|
|
48
48
|
pattern: FE-DATA-SWR
|
|
49
49
|
forbids: ['@tanstack/react-query', react-query]
|
|
50
|
-
why: hooks/swr/api/{graphql,rest}/{queries,mutations}/use*Swr.ts wrap one operation each; components call hooks only.
|
|
50
|
+
why: hooks/swr/api/{graphql,rest}/{queries,mutations}/use*Swr.ts wrap one operation each; components call hooks only. The polling-restart middleware (pollingRestart), the key helpers, the viewer-keyed config and the viewer cache are @starci/next-data's (owner next-data-package); an app imports no Middleware or ScopedMutator type of swr and writes none of them.
|
|
51
51
|
- id: toast
|
|
52
52
|
tier: core
|
|
53
53
|
packages: ['@heroui/react']
|
|
@@ -96,11 +96,11 @@ concerns:
|
|
|
96
96
|
hand-written reader (read-event-stream.ts) and no second SSE library is installed.
|
|
97
97
|
- id: auth
|
|
98
98
|
tier: core
|
|
99
|
-
packages: []
|
|
99
|
+
packages: ['@starci/auth']
|
|
100
100
|
home: [fe.modules.auth]
|
|
101
101
|
pattern: FE-AUTH
|
|
102
|
-
forbids: ['@supabase/supabase-js', '@supabase/ssr', '@supabase/auth-js']
|
|
103
|
-
why: modules/auth is the session issuer's client (
|
|
102
|
+
forbids: ['@supabase/supabase-js', '@supabase/ssr', '@supabase/auth-js', jose, jwt-decode, next-auth, '@auth/core', keycloak-js, oidc-client-ts]
|
|
103
|
+
why: modules/auth is the session issuer's client (one @starci/auth client instance over the app's own back-end auth doors, the access token in its memory; full - the app's IdP client); it yields the session and the access token for the Apollo link and never reads data.
|
|
104
104
|
- id: i18n
|
|
105
105
|
tier: core
|
|
106
106
|
packages: [next-intl]
|
|
@@ -236,17 +236,20 @@ ruleParams:
|
|
|
236
236
|
"node:https": [platform/http]
|
|
237
237
|
http: [platform/http]
|
|
238
238
|
https: [platform/http]
|
|
239
|
-
"@aws-sdk/client-s3": [
|
|
240
|
-
"@aws-sdk/s3-request-presigner": [
|
|
241
|
-
|
|
239
|
+
"@aws-sdk/client-s3": [] # the S3 client is a dependency of @starci/object-storage, imported by no app file (R331)
|
|
240
|
+
"@aws-sdk/s3-request-presigner": []
|
|
241
|
+
"@starci/object-storage": [integrations/object-storage] # the ObjectStorage port: one S3 adapter for MinIO, S3 and Spaces (R331)
|
|
242
|
+
minio: [] # storage goes through @starci/object-storage
|
|
242
243
|
"@supabase": [] # no Supabase client in a back end
|
|
243
|
-
"@nestjs/passport": [
|
|
244
|
-
passport: [
|
|
245
|
-
passport-google-oauth20: [
|
|
246
|
-
passport-local: [
|
|
247
|
-
"@nestjs/jwt": [
|
|
248
|
-
jose: [
|
|
249
|
-
bcryptjs: [
|
|
244
|
+
"@nestjs/passport": [] # the identity libraries are @starci/auth's dependencies, never an app's (R328)
|
|
245
|
+
passport: []
|
|
246
|
+
passport-google-oauth20: []
|
|
247
|
+
passport-local: []
|
|
248
|
+
"@nestjs/jwt": []
|
|
249
|
+
jose: []
|
|
250
|
+
bcryptjs: []
|
|
251
|
+
"@starci/auth": [integrations/passport] # the lite sign-in package: its server entry is reached only through integrations/passport (R328)
|
|
252
|
+
"@starci/auth/testing": [integrations/passport] # its test doubles, for the glue's specs
|
|
250
253
|
bcrypt: []
|
|
251
254
|
cache-manager: [integrations/cache, integrations/redis]
|
|
252
255
|
"@nestjs/cache-manager": [integrations/cache, integrations/redis] # the CACHE_MANAGER token: caching goes through the cache capability
|
|
@@ -1978,36 +1981,68 @@ slots:
|
|
|
1978
1981
|
editions: [lite]
|
|
1979
1982
|
exemplifiedBy:
|
|
1980
1983
|
- packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/
|
|
1981
|
-
# The
|
|
1982
|
-
#
|
|
1983
|
-
#
|
|
1984
|
-
#
|
|
1984
|
+
# The glue of @starci/auth, the lite sign-in package (Google OAuth with state and PKCE, email and password, EdDSA access tokens with
|
|
1985
|
+
# their JWKS, rotating refresh sessions): the one importer of the package (ruleParams.be.infraOwners), which owns the identity
|
|
1986
|
+
# libraries (@nestjs/passport, passport, passport-google-oauth20, passport-local, @nestjs/jwt, jose, bcryptjs) that no app imports.
|
|
1987
|
+
# It parses the typed options, builds the verifier and names the injectors; domain/identity verifies a token through the kit and
|
|
1988
|
+
# builds the principal (contexts/code/be/api-auth.md).
|
|
1985
1989
|
path: "src/modules/integrations/passport/"
|
|
1986
1990
|
presence: required
|
|
1987
1991
|
tracked: tracked
|
|
1988
1992
|
tier: integrations
|
|
1989
1993
|
owner: true
|
|
1990
|
-
requires: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.
|
|
1991
|
-
allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.
|
|
1992
|
-
logic: [passport.
|
|
1994
|
+
requires: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.kit.ts, passport.registration.ts] # Secrets, no defaults (R44); options and named injectors (R83); the kit and the module over @starci/auth
|
|
1995
|
+
allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.kit.ts, passport.registration.ts, errors/]
|
|
1996
|
+
logic: [passport.kit.ts] # the verifier mapping over the package: measured at 100 per file
|
|
1993
1997
|
tests: unit-beside
|
|
1994
1998
|
rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INFRA_OWNER, BE_AUTH_PASSPORT_OWNER, BE_AUTH_JWT_OWN_JWKS, BE_AUTH_REFRESH_COOKIE]
|
|
1995
1999
|
coverage: required
|
|
1996
2000
|
since: 3.9.0
|
|
2001
|
+
- id: be.identity.sign-in
|
|
2002
|
+
since: 3.16.0
|
|
2003
|
+
profiles: [be]
|
|
2004
|
+
editions: [lite]
|
|
2005
|
+
editionWhy: "lite signs people in with the package @starci/auth, so its sign-in glue is rendered, not written (owner ruling auth-package); full uses its identity provider."
|
|
2006
|
+
# The identity capability's sign-in glue over @starci/auth: the entities of the session, attempt and user tables, the stores the package asks for (sessions, accounts, rate counter), the authenticator and the service the doors call, with their specs. `starci app sync` renders them; a hand edit is drift.
|
|
2007
|
+
path: "{src/modules/domain/identity/account-directory.service.spec.ts,src/modules/domain/identity/account-directory.service.ts,src/modules/domain/identity/authenticator.service.spec.ts,src/modules/domain/identity/authenticator.service.ts,src/modules/domain/identity/persistence/entities/auth-identity.entity.ts,src/modules/domain/identity/persistence/entities/auth-user.entity.ts,src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts,src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts,src/modules/domain/identity/persistence/entities/identity-session.entity.ts,src/modules/domain/identity/persistence/entities/index.ts,src/modules/domain/identity/rate-counter.service.spec.ts,src/modules/domain/identity/rate-counter.service.ts,src/modules/domain/identity/session-store.service.spec.ts,src/modules/domain/identity/session-store.service.ts,src/modules/domain/identity/sign-in.service.spec.ts,src/modules/domain/identity/sign-in.service.ts,src/tests/fixtures/builders/identity.builder.ts}"
|
|
2008
|
+
presence: optional
|
|
2009
|
+
tracked: tracked
|
|
2010
|
+
tier: inherit
|
|
2011
|
+
tests: unit-beside
|
|
2012
|
+
coverage: none
|
|
2013
|
+
managedBy: auth-lite
|
|
2014
|
+
managedOnly: true # lists files sync renders; the slot owning the path judges them
|
|
2015
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
2016
|
+
- id: be.feature.auth
|
|
2017
|
+
since: 3.16.0
|
|
2018
|
+
profiles: [be]
|
|
2019
|
+
editions: [lite]
|
|
2020
|
+
editionWhy: "lite signs people in with the package @starci/auth, so its sign-in glue is rendered, not written (owner ruling auth-package); full uses its identity provider."
|
|
2021
|
+
# The auth feature: the REST doors under /auth (login, refresh, session, logout, Google start and callback, jwks) over IdentitySignInService, one command or query and handler per door.
|
|
2022
|
+
path: "{src/features/api/auth/application/auth.contracts.ts,src/features/api/auth/application/get-jwks.handler.ts,src/features/api/auth/application/get-jwks.query.ts,src/features/api/auth/application/google-begin.command.ts,src/features/api/auth/application/google-begin.handler.ts,src/features/api/auth/application/google-complete.command.ts,src/features/api/auth/application/google-complete.handler.ts,src/features/api/auth/application/login.command.ts,src/features/api/auth/application/login.handler.ts,src/features/api/auth/application/logout.command.ts,src/features/api/auth/application/logout.handler.ts,src/features/api/auth/application/refresh.command.ts,src/features/api/auth/application/refresh.handler.ts,src/features/api/auth/application/session.command.ts,src/features/api/auth/application/session.handler.ts,src/features/api/auth/auth.module-definition.ts,src/features/api/auth/auth.module.ts,src/features/api/auth/auth.options.ts,src/features/api/auth/index.ts,src/features/api/auth/transport/http/auth-http.module-definition.ts,src/features/api/auth/transport/http/auth-http.module.ts,src/features/api/auth/transport/http/auth-http.options.ts,src/features/api/auth/transport/http/auth.controller.ts,src/features/api/auth/transport/http/dto/google-start.request.ts,src/features/api/auth/transport/http/dto/login.request.ts}"
|
|
2023
|
+
presence: optional
|
|
2024
|
+
tracked: tracked
|
|
2025
|
+
tier: inherit
|
|
2026
|
+
tests: none
|
|
2027
|
+
coverage: none
|
|
2028
|
+
managedBy: auth-lite
|
|
2029
|
+
managedOnly: true # lists files sync renders; the slot owning the path judges them
|
|
2030
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
1997
2031
|
- id: be.integrations.object-storage
|
|
1998
2032
|
profiles: [be]
|
|
1999
2033
|
exemplifiedBy:
|
|
2000
2034
|
- packages/hfs/templates/be/skeleton-lite/src/modules/integrations/object-storage/
|
|
2001
|
-
# The
|
|
2002
|
-
# lite): the
|
|
2035
|
+
# The glue of @starci/object-storage, the owner of the S3 client (@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner) over
|
|
2036
|
+
# S3-compatible storage (MinIO in lite): this folder keeps the bucket, parses the settings and registers the module; no app
|
|
2037
|
+
# file imports an S3 SDK or minio, and no other module imports the package.
|
|
2003
2038
|
path: "src/modules/integrations/object-storage/"
|
|
2004
2039
|
presence: optional
|
|
2005
2040
|
litePresence: required
|
|
2006
2041
|
tracked: tracked
|
|
2007
2042
|
tier: integrations
|
|
2008
2043
|
owner: true
|
|
2009
|
-
requires: [index.ts, object-storage.config.ts, object-storage.options.ts, object-storage.decorators.ts, object-storage.
|
|
2010
|
-
allows: [index.ts, object-storage.
|
|
2044
|
+
requires: [index.ts, object-storage.config.ts, object-storage.options.ts, object-storage.decorators.ts, object-storage.module-definition.ts, object-storage.module.ts]
|
|
2045
|
+
allows: [index.ts, object-storage.config.ts, object-storage.options.ts, object-storage.decorators.ts, object-storage.module-definition.ts, object-storage.module.ts, errors/]
|
|
2011
2046
|
tests: unit-beside
|
|
2012
2047
|
rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INFRA_OWNER, BE_STORAGE_OWNER]
|
|
2013
2048
|
coverage: required
|
|
@@ -2541,7 +2576,7 @@ slots:
|
|
|
2541
2576
|
profiles: [fe]
|
|
2542
2577
|
# The identity-provider client (fe-stack auth, FE-AUTH): the session, the access token the Apollo link attaches and the
|
|
2543
2578
|
# sign-in/sign-out calls. It talks only to the app's own back-end auth doors and holds no credential: the refresh token lives in
|
|
2544
|
-
# the back end's httpOnly cookie and the access token in memory (ruling:lite-self-hosted-postgres-passport-minio).
|
|
2579
|
+
# the back end's httpOnly cookie and the access token in the memory of the one @starci/auth client (ruling:lite-self-hosted-postgres-passport-minio).
|
|
2545
2580
|
path: "apps/<app>/src/modules/auth/"
|
|
2546
2581
|
presence: optional
|
|
2547
2582
|
tracked: tracked
|
|
@@ -2551,6 +2586,20 @@ slots:
|
|
|
2551
2586
|
tests: none
|
|
2552
2587
|
since: 3.0.0
|
|
2553
2588
|
rules: [FE_STACK_FORBIDDEN]
|
|
2589
|
+
- id: fe.modules.auth.client
|
|
2590
|
+
since: 3.16.0
|
|
2591
|
+
profiles: [fe]
|
|
2592
|
+
editions: [lite]
|
|
2593
|
+
editionWhy: "lite signs people in with the package @starci/auth, so its auth client is rendered, not written (owner ruling auth-package); full wires its identity provider."
|
|
2594
|
+
# The AuthClient every edition shares, implemented by one @starci/auth/client instance over the app's own back-end auth doors.
|
|
2595
|
+
path: "apps/web/src/modules/auth/client.ts"
|
|
2596
|
+
presence: optional
|
|
2597
|
+
tracked: tracked
|
|
2598
|
+
tier: inherit
|
|
2599
|
+
tests: none
|
|
2600
|
+
managedBy: auth-lite
|
|
2601
|
+
managedOnly: true # lists files sync renders; the slot owning the path judges them
|
|
2602
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
2554
2603
|
- id: fe.modules.toast
|
|
2555
2604
|
profiles: [fe]
|
|
2556
2605
|
# Mutation feedback (fe-stack toast, FE-TOAST): the HeroUI toast wrapper, showGraphQLToast over a GraphQLResponse and the
|
|
@@ -1,57 +1,17 @@
|
|
|
1
|
-
// identity.mjs - the back-end identity machine. Under lite the one token verifier is be.integrations.passport:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// In both editions every jwtVerify call holds the JWT policy (R260, jwtPolicyProblems).
|
|
1
|
+
// identity.mjs - the back-end identity machine. Under lite the one token verifier is @starci/auth, composed by be.integrations.passport:
|
|
2
|
+
// no app file calls jose's jwtVerify, decodes a token unverified or reads authorization claims from a request body (R329), and the
|
|
3
|
+
// passport integration imports @starci/auth/server. In both editions every jwtVerify call holds the JWT policy (R260, jwtPolicyProblems).
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { machineKit } from './machine-ast.mjs';
|
|
7
|
-
import {
|
|
6
|
+
import { jwtPolicyProblems, reportAt, unwrap } from './jwt-ast.mjs';
|
|
8
7
|
import { accessName } from '../../lib/ts-ast.mjs';
|
|
9
8
|
const PASSPORT_SLOT = 'be.integrations.passport';
|
|
9
|
+
const PACKAGE_ENTRY = '@starci/auth/server';
|
|
10
10
|
const BE_JWT_VERIFIED = 'BE_AUTH_JWT_OWN_JWKS';
|
|
11
11
|
/** The rule ids the back-end identity machine reports. */
|
|
12
12
|
export const IDENTITY_RULE_IDS = Object.freeze(['BE_AUTH_JWT_OWN_JWKS', 'BE_JWT_ALG_PINNED']);
|
|
13
13
|
const JWT_MODULES = new Set(['jose', 'jsonwebtoken', 'jwt-decode']);
|
|
14
14
|
const CLAIM_NAMES = new Set(['role', 'roles', 'membership', 'memberships']);
|
|
15
|
-
const objectLiteral = (kit, checker, node) => {
|
|
16
|
-
const current = unwrap(kit.ts, node);
|
|
17
|
-
if (!current)
|
|
18
|
-
return null;
|
|
19
|
-
if (kit.ts.isObjectLiteralExpression(current))
|
|
20
|
-
return current;
|
|
21
|
-
if (!kit.ts.isIdentifier(current))
|
|
22
|
-
return null;
|
|
23
|
-
for (const declaration of kit.declarationsOf(checker, current)) {
|
|
24
|
-
if (kit.ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
25
|
-
const value = unwrap(kit.ts, declaration.initializer);
|
|
26
|
-
if (kit.ts.isObjectLiteralExpression(value))
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
};
|
|
32
|
-
const staticStrings = (kit, checker, node) => {
|
|
33
|
-
const current = unwrap(kit.ts, node);
|
|
34
|
-
if (kit.ts.isStringLiteralLike(current))
|
|
35
|
-
return [current.text];
|
|
36
|
-
if (kit.ts.isArrayLiteralExpression(current)) {
|
|
37
|
-
const literals = current.elements.filter((element) => kit.ts.isStringLiteralLike(element));
|
|
38
|
-
if (literals.length === current.elements.length)
|
|
39
|
-
return literals.map((element) => element.text);
|
|
40
|
-
}
|
|
41
|
-
const value = kit.stringValue(checker, current);
|
|
42
|
-
return value === null ? null : [value];
|
|
43
|
-
};
|
|
44
|
-
const jwksArgument = (kit, checker, node) => {
|
|
45
|
-
const current = unwrap(kit.ts, node);
|
|
46
|
-
if (!current)
|
|
47
|
-
return false;
|
|
48
|
-
if (kit.ts.isCallExpression(current))
|
|
49
|
-
return importedFrom(kit, checker, current.expression, new Set(['jose']), new Set(['createLocalJWKSet']));
|
|
50
|
-
if (!kit.ts.isIdentifier(current))
|
|
51
|
-
return false;
|
|
52
|
-
return kit.declarationsOf(checker, current).some((declaration) => kit.ts.isVariableDeclaration(declaration) && declaration.initializer
|
|
53
|
-
&& jwksArgument(kit, checker, declaration.initializer));
|
|
54
|
-
};
|
|
55
15
|
const importedCall = (kit, checker, call) => {
|
|
56
16
|
const direct = kit.importBinding(checker, call.expression);
|
|
57
17
|
if (direct?.module === 'jwt-decode' && direct.name === 'default')
|
|
@@ -84,41 +44,16 @@ const fromRequestBody = (kit, checker, node, seen = new Set()) => {
|
|
|
84
44
|
return kit.declarationsOf(checker, current).some(declaration => kit.ts.isVariableDeclaration(declaration) && declaration.initializer
|
|
85
45
|
&& fromRequestBody(kit, checker, declaration.initializer, seen));
|
|
86
46
|
};
|
|
87
|
-
function verifyJwtCall(kit, checker, file, node, counters, violations) {
|
|
88
|
-
counters.verifyCalls += 1;
|
|
89
|
-
if (file.slot !== PASSPORT_SLOT) {
|
|
90
|
-
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `jwtVerify is called outside ${PASSPORT_SLOT}; the integration is the one token verifier.`);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
const options = objectLiteral(kit, checker, node.arguments[2]);
|
|
94
|
-
const property = (name) => options ? kit.propertyOf(options, name) : null;
|
|
95
|
-
const values = (name) => {
|
|
96
|
-
const entry = property(name);
|
|
97
|
-
return entry ? staticStrings(kit, checker, kit.valueOfProperty(entry)) : null;
|
|
98
|
-
};
|
|
99
|
-
const issuer = property('issuer');
|
|
100
|
-
const audience = values('audience');
|
|
101
|
-
const algorithms = values('algorithms');
|
|
102
|
-
// The audience is pinned by a literal or, as R260 accepts, by typed configuration (the skeleton's TokenOptions.audience).
|
|
103
|
-
const audienceEntry = property('audience');
|
|
104
|
-
const pinnedAudience = (audience !== null && audience.length > 0)
|
|
105
|
-
|| (audienceEntry != null && fromConfig(kit.ts, checker, kit.valueOfProperty(audienceEntry)));
|
|
106
|
-
const good = jwksArgument(kit, checker, node.arguments[1]) && issuer != null && pinnedAudience
|
|
107
|
-
&& algorithms !== null && algorithms.length > 0;
|
|
108
|
-
if (!good) {
|
|
109
|
-
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'jwtVerify must check the app\'s own key set (createLocalJWKSet(...) over the keys integrations/passport signs with) and pin issuer, audience and a non-empty algorithms list; jwtVerify then enforces signature and exp.');
|
|
110
|
-
}
|
|
111
|
-
else
|
|
112
|
-
counters.verifiedCalls += 1;
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
47
|
function inspectJwtCall(kit, checker, file, node, counters, violations) {
|
|
116
48
|
const binding = importedCall(kit, checker, node);
|
|
117
49
|
if (binding && JWT_MODULES.has(binding.module) && ['decode', 'decodeJwt', 'jwtDecode'].includes(binding.name)) {
|
|
118
50
|
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${binding.module}.${binding.name} decodes a token without proving its signature; verify it through integrations/passport.`, { module: binding.module, method: binding.name });
|
|
119
51
|
}
|
|
120
|
-
if (binding?.module === 'jose' && binding.name === 'jwtVerify')
|
|
121
|
-
|
|
52
|
+
if (binding?.module === 'jose' && binding.name === 'jwtVerify') {
|
|
53
|
+
counters.verifyCalls += 1;
|
|
54
|
+
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'jwtVerify is called in the app; @starci/auth verifies the own access tokens, reached through ' + PASSPORT_SLOT + '.');
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
122
57
|
return false;
|
|
123
58
|
}
|
|
124
59
|
function inspectRequestClaims(kit, file, node, checker, violations) {
|
|
@@ -146,18 +81,18 @@ function checkOwnJwks(input) {
|
|
|
146
81
|
const violations = [];
|
|
147
82
|
const integrations = [...input.graph.files.values()].filter((file) => file.slot === PASSPORT_SLOT);
|
|
148
83
|
if (!integrations.length)
|
|
149
|
-
return { violations, coverage: { status: 'not-applicable', integrationFiles: 0,
|
|
150
|
-
const counters = { verifyCalls: 0
|
|
84
|
+
return { violations, coverage: { status: 'not-applicable', integrationFiles: 0, verifyCalls: 0 } };
|
|
85
|
+
const counters = { verifyCalls: 0 };
|
|
151
86
|
for (const file of input.graph.files.values()) {
|
|
152
87
|
const checker = kit.checkerOf(file.sourceFile);
|
|
153
88
|
kit.walk(file.sourceFile, (node) => inspectBackendNode(kit, checker, file, node, counters, violations));
|
|
154
89
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls,
|
|
90
|
+
const composes = integrations.some((file) => file.sourceFile.statements.some((statement) => kit.ts.isImportDeclaration(statement)
|
|
91
|
+
&& kit.ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === PACKAGE_ENTRY));
|
|
92
|
+
const anchor = integrations.find((file) => path.posix.basename(file.rel) === 'index.ts') ?? integrations[0];
|
|
93
|
+
if (!composes && anchor)
|
|
94
|
+
reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, PASSPORT_SLOT + ' does not import ' + PACKAGE_ENTRY + ', the one verifier of the own access tokens.');
|
|
95
|
+
return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls, composesPackage: composes } };
|
|
161
96
|
}
|
|
162
97
|
/** R260 BE_JWT_ALG_PINNED: every jose jwtVerify call pins a literal secure algorithm list and a configured issuer, and binds the principal by a configured audience or the authenticated role. */
|
|
163
98
|
export function checkJwtPolicy(input) {
|
|
@@ -17,8 +17,7 @@ import { propertyValue } from './function-spec-providers.mjs';
|
|
|
17
17
|
* - no file outside the client folder and the stream reader folder (`fe.transport.stream`, `modules/api/sse/clients/`) references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
|
|
18
18
|
* reference passed as a value (`useSWR(key, fetch)`, `const send = fetch`), all of which a spelling-based rule misses; with no
|
|
19
19
|
* client at all every such reference is a finding that says the repository has no client;
|
|
20
|
-
* - axios is imported only by the api owner (modules/api, the api package) for its rest calls
|
|
21
|
-
* (modules/auth/auth-door.ts) for the app's own auth doors; every other HTTP library is a
|
|
20
|
+
* - axios is imported only by the api owner (modules/api, the api package) for its rest calls; every other HTTP library is a
|
|
22
21
|
* second transport wherever it is imported, by static import, `import()` or `require()`;
|
|
23
22
|
* - an `ApolloClient` is constructed only inside the client folder;
|
|
24
23
|
* - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`, `graphql/subscriptions/subscription-*.ts`) imports the client (or the api
|
|
@@ -105,17 +104,15 @@ function isApolloClientConstruction(ts, kit, checker, node) {
|
|
|
105
104
|
const binding = kit.importBinding(checker, node.expression);
|
|
106
105
|
return Boolean(binding?.name === 'ApolloClient' && binding.module.startsWith('@apollo/client'));
|
|
107
106
|
}
|
|
108
|
-
/** The one auth door of the session issuer's client (`modules/auth/auth-door.ts`, slot fe.modules.auth): it calls the app's own auth doors with axios. */
|
|
109
|
-
const isAuthDoor = (file) => file.slot === 'fe.modules.auth' && path.posix.basename(file.rel) === 'auth-door.ts';
|
|
110
107
|
function inspectLibraryImport({ ts, file, node, transport, report }) {
|
|
111
108
|
const specifier = moduleSpecifierOf(ts, node);
|
|
112
109
|
if (!specifier || !ts.isStringLiteralLike(specifier))
|
|
113
110
|
return;
|
|
114
111
|
const library = specifier.text.split('/')[0];
|
|
115
|
-
if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && (API_OWNER_SLOTS.has(file.slot)
|
|
112
|
+
if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && (API_OWNER_SLOTS.has(file.slot))))
|
|
116
113
|
return;
|
|
117
114
|
const where = library === REST_LIBRARY
|
|
118
|
-
? 'axios belongs to the api owner (modules/api/rest, the api package)
|
|
115
|
+
? 'axios belongs to the api owner (modules/api/rest, the api package) only: call their functions'
|
|
119
116
|
: `the transport is Apollo GraphQL (${transport}) with axios for the rest calls of modules/api/rest`;
|
|
120
117
|
report(file, node, `${specifier.text} is a second HTTP transport in ${file.rel}; ${where}.`, { library: specifier.text });
|
|
121
118
|
}
|
|
@@ -121,7 +121,7 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
|
|
|
121
121
|
export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
|
|
122
122
|
const strList = (v) => stringList(v);
|
|
123
123
|
|
|
124
|
-
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy', 'role', 'importedBy', 'logic'];
|
|
124
|
+
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy', 'role', 'importedBy', 'logic', 'managedOnly'];
|
|
125
125
|
/** The roles a file slot may play that a rule reads (`role`): the entity registry of persistence.md BE-PERSISTENCE-3 (architecture/entity-registry.mjs). */
|
|
126
126
|
const SLOT_ROLES = ['entity-registry'];
|
|
127
127
|
/** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
|
|
@@ -232,7 +232,7 @@ export function readRepoDeclaration(manifest, repoRoot) {
|
|
|
232
232
|
function createScopeResolver(manifest, repo) {
|
|
233
233
|
const profile = repo.profile;
|
|
234
234
|
const edition = repo.edition ?? 'full';
|
|
235
|
-
const slots = manifest.slots.filter((s) => s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
|
|
235
|
+
const slots = manifest.slots.filter((s) => s.managedOnly !== true && s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
|
|
236
236
|
const scope = { manifest, repo, profile, slots, byId: new Map(slots.map((s) => [s.id, s])), variants: slots.flatMap(variantsOf), appKind: new Map(repo.apps.map((a) => [a.name, a.kind])) };
|
|
237
237
|
const classifyPath = (input) => classifyIn(scope, input);
|
|
238
238
|
/** tracked | ignored | external for the slot owning `p`, or null when no slot owns it. */
|