@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 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.
@@ -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"]));
@@ -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
@@ -392,6 +392,11 @@ export declare const why: {
392
392
  en: string;
393
393
  fix: string;
394
394
  };
395
+ "swr-extension-from-package": {
396
+ code: string;
397
+ en: string;
398
+ fix: string;
399
+ };
395
400
  "fe-stack-import": {
396
401
  code: string;
397
402
  en: string;
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.",
@@ -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" | "authDoor", []>;
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, isAuthDoor, isAuthTokenStore, isOutcomeModule } from "./lib/scope.js";
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/`) and the auth door (`modules/auth/auth-door.ts`)", allowed: (context) => isApiRest(context) || isAuthDoor(context) }],
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`, but the in-memory access-token store `modules/auth/token-store.ts`." },
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) && !(inSlot(context, "fe.modules.auth") && !isAuthTokenStore(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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "9.10.0",
3
+ "version": "9.11.0",
4
4
  "description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.13
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.12.0
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.10.0
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.13
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.13
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.0
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.1.0
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 of integrations/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces); 3.1150.0 is the release of 2026-10-12, chosen over newer ones that had not aged two weeks.'
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 of integrations/object-storage locally, the same release as client-s3.'
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, fe.modules.auth]
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, and modules/auth/auth-door.ts for the app's own auth doors only; GraphQL carries every other call.
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 (lite - auth-door.ts over the app's own back-end auth doors with the in-memory token store; full - the app's IdP client); it yields the session and the access token for the Apollo link and never reads data.
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": [integrations/object-storage] # the ObjectStorage port: one S3 adapter for MinIO, S3 and Spaces (R331)
240
- "@aws-sdk/s3-request-presigner": [integrations/object-storage]
241
- minio: [] # storage goes through the S3 client of integrations/object-storage
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": [integrations/passport] # the identity libraries (R328)
244
- passport: [integrations/passport]
245
- passport-google-oauth20: [integrations/passport]
246
- passport-local: [integrations/passport]
247
- "@nestjs/jwt": [integrations/passport]
248
- jose: [integrations/passport]
249
- bcryptjs: [integrations/passport]
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 one owner of the lite identity libraries (ruleParams.be.infraOwners: @nestjs/passport, passport, passport-google-oauth20,
1982
- # passport-local, @nestjs/jwt, jose, bcryptjs): the Google OAuth 2.0 strategy (state and PKCE), the local email and password
1983
- # strategy (bcrypt), the app's own asymmetric access tokens and their JWKS, and the rotating refresh cookie. domain/identity
1984
- # verifies a token through this integration's verifier and builds the principal (contexts/code/be/api-auth.md).
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.tokens.ts, passport.password.ts, passport.cookie.ts] # Secrets, no defaults (R44); options and named injectors (R83); tokens and JWKS; bcrypt; the refresh cookie
1991
- allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.tokens.ts, passport.password.ts, passport.cookie.ts, strategies/, errors/]
1992
- logic: [passport.tokens.ts, passport.password.ts, passport.cookie.ts] # JWT sign and verify over the own JWKS, bcrypt, the refresh cookie: measured at 100 per file
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 one owner of the S3 client (@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner) over S3-compatible storage (MinIO in
2002
- # lite): the ObjectStorage port with presigned PUT and GET; no other module imports an S3 SDK or minio.
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.client.ts]
2010
- allows: [index.ts, object-storage.module.ts, object-storage.module-definition.ts, object-storage.config.ts, object-storage.options.ts, object-storage.decorators.ts, object-storage.client.ts, errors/]
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: jose's jwtVerify
2
- // there checks the app's OWN key set (createLocalJWKSet over the keys it signs with, never createRemoteJWKSet), pins issuer,
3
- // audience and algorithms, and no file decodes a token unverified or reads authorization claims from a request body (R329).
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 { fromConfig, importedFrom, jwtPolicyProblems, reportAt, unwrap } from './jwt-ast.mjs';
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
- return verifyJwtCall(kit, checker, file, node, counters, violations);
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, verifiedCalls: 0 } };
150
- const counters = { verifyCalls: 0, verifiedCalls: 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
- if (counters.verifyCalls === 0) {
156
- const anchor = integrations.find((file) => path.posix.basename(file.rel).endsWith('.tokens.ts')) ?? integrations[0];
157
- if (anchor)
158
- reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, `${PASSPORT_SLOT} has no jose.jwtVerify call backed by createLocalJWKSet over the app's own keys.`);
159
- }
160
- return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls, verifiedCalls: counters.verifiedCalls } };
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 and by the auth door
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) || isAuthDoor(file))))
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) and the auth door (modules/auth/auth-door.ts) only: call their functions'
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. */