@c9up/station 0.1.11 → 0.1.13
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/dist/ResourceRegistry.d.ts +19 -0
- package/dist/ResourceRegistry.d.ts.map +1 -1
- package/dist/ResourceRegistry.js +14 -7
- package/dist/ResourceRegistry.js.map +1 -1
- package/dist/StationProvider.d.ts +33 -5
- package/dist/StationProvider.d.ts.map +1 -1
- package/dist/StationProvider.js +164 -66
- package/dist/StationProvider.js.map +1 -1
- package/dist/services/main.d.ts +9 -0
- package/dist/services/main.d.ts.map +1 -1
- package/dist/services/main.js +11 -0
- package/dist/services/main.js.map +1 -1
- package/dist/vendor/nodeEnv.d.ts +5 -0
- package/dist/vendor/nodeEnv.d.ts.map +1 -0
- package/dist/{nodeEnv.js → vendor/nodeEnv.js} +13 -7
- package/dist/vendor/nodeEnv.js.map +1 -0
- package/package.json +11 -11
- package/src/ResourceRegistry.ts +26 -0
- package/src/StationProvider.ts +174 -77
- package/src/services/main.ts +12 -0
- package/src/{nodeEnv.ts → vendor/nodeEnv.ts} +14 -7
- package/dist/nodeEnv.d.ts +0 -16
- package/dist/nodeEnv.d.ts.map +0 -1
- package/dist/nodeEnv.js.map +0 -1
package/src/StationProvider.ts
CHANGED
|
@@ -26,10 +26,10 @@ import type {
|
|
|
26
26
|
DatabaseConnection,
|
|
27
27
|
DateColumnConfig,
|
|
28
28
|
} from "@c9up/atlas";
|
|
29
|
-
import { inProduction } from "./nodeEnv.js";
|
|
30
29
|
import { ResourceRegistry } from "./ResourceRegistry.js";
|
|
31
|
-
import { setStation } from "./services/main.js";
|
|
30
|
+
import { clearStation, getStation, setStation } from "./services/main.js";
|
|
32
31
|
import type { AuditEvent, Resource, ResourceAction } from "./types.js";
|
|
32
|
+
import { inProduction } from "./vendor/nodeEnv.js";
|
|
33
33
|
// note: AuditEvent + ResourceAction are used by the CRUD handlers below;
|
|
34
34
|
// the imports stay in one block for clarity.
|
|
35
35
|
import { buildFormViewModel } from "./views/form.js";
|
|
@@ -287,7 +287,7 @@ type RuneValidationResult =
|
|
|
287
287
|
/**
|
|
288
288
|
* A built rune schema — only the result-based check is consumed.
|
|
289
289
|
*
|
|
290
|
-
* `validateResult`, NOT `validate`: rune reserves `validate()` for the
|
|
290
|
+
* `validateResult`, NOT `validate`: rune reserves `validate()` for the
|
|
291
291
|
* contract (async, throwing). Reading `.valid` off a Promise gives `undefined`,
|
|
292
292
|
* so keeping the old name would have made every admin write fail closed with an
|
|
293
293
|
* empty error list.
|
|
@@ -548,11 +548,14 @@ export function resourcesNeedValidation(
|
|
|
548
548
|
* NEVER `in` on an instance (`@Column() declare` makes `in` always false —
|
|
549
549
|
* mirror `BaseRepository.#hydrate`).
|
|
550
550
|
*
|
|
551
|
-
* Per-column rule mapping tolerates the form-post wire shape
|
|
552
|
-
*
|
|
551
|
+
* Per-column rule mapping tolerates the form-post wire shape, where every
|
|
552
|
+
* value arrives as a string: booleans `.parse(isCheckedValue)`, numbers
|
|
553
553
|
* `.parse` string→Number (NaN falls through to a type error), everything else
|
|
554
|
-
* `rules.string()`. `.parse
|
|
555
|
-
*
|
|
554
|
+
* `rules.string()`. The explicit `.parse` is deliberate even though rune now
|
|
555
|
+
* coerces both itself — a checkbox posts the values IT chooses (`isCheckedValue`
|
|
556
|
+
* is the column's own truthy spelling), which is a wider set than any
|
|
557
|
+
* general-purpose coercion should accept. `.parse`/`.optional`/`.nullable`
|
|
558
|
+
* force rune's TS path, so no native `.node` is touched.
|
|
556
559
|
*
|
|
557
560
|
* Returns the built schema plus the set of boolean columns — the handler
|
|
558
561
|
* default-fills an ABSENT boolean key to `false` before validating (an
|
|
@@ -653,7 +656,7 @@ function fieldErrors(
|
|
|
653
656
|
const out: Record<string, string> = {};
|
|
654
657
|
for (const e of errors) {
|
|
655
658
|
if (e.field === "_root") continue;
|
|
656
|
-
// Collapse a nested
|
|
659
|
+
// Collapse a nested dotted path (`profile.name`) to its top-level
|
|
657
660
|
// key — `form.ts` keys errors by column propertyKey. `split` always yields
|
|
658
661
|
// a non-empty array, so `[0]` is a string (no fallback needed).
|
|
659
662
|
const [key = e.field] = e.field.split(".");
|
|
@@ -834,6 +837,9 @@ let auditCloneWarnEmitted = false;
|
|
|
834
837
|
const TEMPLATES_ROOT = fileURLToPath(new URL("../templates/", import.meta.url));
|
|
835
838
|
|
|
836
839
|
export default class StationProvider {
|
|
840
|
+
/** What this provider bound, so shutdown only clears its own. */
|
|
841
|
+
#owned: ResourceRegistry | undefined;
|
|
842
|
+
|
|
837
843
|
#contexts: Map<Resource, ResourceContext> = new Map();
|
|
838
844
|
#viewRenderer: InkerViewRenderer | undefined;
|
|
839
845
|
/**
|
|
@@ -871,7 +877,8 @@ export default class StationProvider {
|
|
|
871
877
|
async boot(): Promise<void> {
|
|
872
878
|
// Force-resolve so `setStation` runs even if no preload touches the
|
|
873
879
|
// singleton. Mirrors AuroraProvider.boot().
|
|
874
|
-
|
|
880
|
+
this.#owned =
|
|
881
|
+
await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
|
|
875
882
|
}
|
|
876
883
|
|
|
877
884
|
async start(): Promise<void> {
|
|
@@ -879,8 +886,14 @@ export default class StationProvider {
|
|
|
879
886
|
|
|
880
887
|
const registry =
|
|
881
888
|
await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
|
|
882
|
-
|
|
883
|
-
|
|
889
|
+
|
|
890
|
+
// NOT gated on the registry being non-empty any more. Providers start
|
|
891
|
+
// BEFORE preloads run, and a preload is where the docs say to write
|
|
892
|
+
// `station.register(...)`, so reading a snapshot here and giving up when
|
|
893
|
+
// it was empty meant an application that followed the documentation got
|
|
894
|
+
// no admin at all. The surface is prepared here and each resource is
|
|
895
|
+
// mounted as it is declared — the shape upstream's Transmit uses for the
|
|
896
|
+
// same ordering problem.
|
|
884
897
|
|
|
885
898
|
// 54.7 — read the optional `station` config block. Defaults bake
|
|
886
899
|
// in `requireAuth: true` when @c9up/warden is detected later in
|
|
@@ -894,67 +907,94 @@ export default class StationProvider {
|
|
|
894
907
|
if (!peers) return;
|
|
895
908
|
const { router, atlas, rune } = peers;
|
|
896
909
|
|
|
897
|
-
// 57.7 fail-closed: a write-capable admin (create/edit) validates + guards
|
|
898
|
-
// mass-assignment through a rune-derived schema. If atlas is present (CRUD
|
|
899
|
-
// is live) but `@c9up/rune` is NOT installed, refuse to boot rather than
|
|
900
|
-
// silently accept unvalidated bodies — there is NO fallback to the deleted
|
|
901
|
-
// key-filter (`feedback_security_first`). Read-only admins need no rune.
|
|
902
|
-
if (rune === null && resourcesNeedValidation(resources)) {
|
|
903
|
-
throw new Error(
|
|
904
|
-
"[station] @c9up/rune is required to validate admin write forms (create/edit) but is not installed. Add @c9up/rune (a Station peer dependency) — Station will NOT fall back to unvalidated mass-assignment. See https://ream.dev/modules/station#mass-assignment.",
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
910
|
// Phase 1a2 — resolve the SHARED inker renderer (AdonisJS/Edge parity)
|
|
909
911
|
// and mount Station's package templates as the `station` disk. Unlike
|
|
910
912
|
// warden (whose absence keeps the open dev-preview path), a view engine
|
|
911
913
|
// is a HARD render requirement (D2): once an admin surface exists there
|
|
912
|
-
// is no page without it.
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
|
|
919
|
-
|
|
914
|
+
// is no page without it.
|
|
915
|
+
//
|
|
916
|
+
// The failure is HELD rather than thrown: with no resource declared
|
|
917
|
+
// there is no admin surface, and an application that installed the
|
|
918
|
+
// provider without ever registering a resource must not be forced to
|
|
919
|
+
// wire a view engine. It is rethrown on the first resource, which is the
|
|
920
|
+
// moment the requirement becomes real.
|
|
921
|
+
let rendererFailure: unknown;
|
|
922
|
+
try {
|
|
923
|
+
const renderer = await this.#resolveViewRenderer();
|
|
924
|
+
// Mount once: `station::<template>` now resolves to this package's
|
|
925
|
+
// templates/ dir on the host's single inker engine (edge.mount parity).
|
|
926
|
+
renderer.mount("station", TEMPLATES_ROOT);
|
|
927
|
+
this.#viewRenderer = renderer;
|
|
928
|
+
} catch (err) {
|
|
929
|
+
rendererFailure = err;
|
|
930
|
+
}
|
|
920
931
|
|
|
921
932
|
// Phase 1b — wire the warden auth gate (or warn-once when it stays open).
|
|
922
933
|
await this.#configureAuth(userConfig);
|
|
923
934
|
|
|
924
|
-
// Phase 2 — build per-resource context ONCE. `#resolveDb()` is
|
|
925
|
-
// loud: if the host installed `@c9up/atlas` but didn't register
|
|
926
|
-
// `@c9up/atlas/provider` in `reamrc.ts`, AC11's "surface
|
|
927
|
-
// AtlasProvider misconfiguration" intent kicks in.
|
|
928
935
|
const db = await this.#resolveDb();
|
|
929
|
-
|
|
936
|
+
|
|
937
|
+
// Phase 2 — mount every resource, now and as they arrive. The shell (the
|
|
938
|
+
// login surface, the `/admin` index) goes up with the first one: an
|
|
939
|
+
// application with no resources has no admin, and had none before.
|
|
940
|
+
let shellMounted = false;
|
|
941
|
+
registry.useMounter((resource) => {
|
|
942
|
+
if (!shellMounted) {
|
|
943
|
+
if (rendererFailure !== undefined) throw rendererFailure;
|
|
944
|
+
try {
|
|
945
|
+
this.#registerShellRoutes(router);
|
|
946
|
+
} catch (err) {
|
|
947
|
+
// The router proxy may still throw "Router accessed before
|
|
948
|
+
// initialization" on first property access (a boot-ordering
|
|
949
|
+
// hazard where the proxy module imported but Ignitor's
|
|
950
|
+
// `setRouter` never fired). That is a legitimate degraded-host
|
|
951
|
+
// shape — give up quietly. Anything else propagates.
|
|
952
|
+
if (isRouterProxyUninit(err)) return;
|
|
953
|
+
throw err;
|
|
954
|
+
}
|
|
955
|
+
shellMounted = true;
|
|
956
|
+
}
|
|
957
|
+
// 57.7 fail-closed: a write-capable admin (create/edit) validates +
|
|
958
|
+
// guards mass-assignment through a rune-derived schema. If atlas is
|
|
959
|
+
// present (CRUD is live) but `@c9up/rune` is NOT installed, refuse
|
|
960
|
+
// rather than silently accept unvalidated bodies — there is NO
|
|
961
|
+
// fallback to the deleted key-filter. Read-only admins need no rune.
|
|
962
|
+
// Checked per resource, so the refusal names the one at fault.
|
|
963
|
+
if (rune === null && resourcesNeedValidation([resource])) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
`[station] @c9up/rune is required to validate the write forms of the '${resource.name}' admin (create/edit) but is not installed. Add @c9up/rune (a Station peer dependency) — Station will NOT fall back to unvalidated mass-assignment. See https://ream.dev/modules/station#mass-assignment.`,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
930
968
|
this.#contexts.set(
|
|
931
969
|
resource,
|
|
932
970
|
buildResourceContext(resource, db, atlas, rune),
|
|
933
971
|
);
|
|
934
|
-
|
|
972
|
+
this.#registerResourceRoutes(router, resource);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
this.#started = true;
|
|
976
|
+
}
|
|
935
977
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
978
|
+
/**
|
|
979
|
+
* The gaps worth reporting once, when the full set of resources is known.
|
|
980
|
+
*
|
|
981
|
+
* `ready()` runs after every preload, so this is the first moment the whole
|
|
982
|
+
* admin surface exists. Nothing is mounted here — the routes were built as
|
|
983
|
+
* each resource was declared.
|
|
984
|
+
*/
|
|
985
|
+
async ready(): Promise<void> {
|
|
986
|
+
if (!this.#started) return;
|
|
987
|
+
const registry =
|
|
988
|
+
await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
|
|
989
|
+
const resources = registry.all();
|
|
990
|
+
if (resources.length === 0) return;
|
|
991
|
+
// 56.5 + 54.6 — "auth wired but no permissions seeded" and "no audit
|
|
992
|
+
// sink" are surfaced loud-and-once so a half-wired install cannot ship to
|
|
993
|
+
// prod without operators noticing. (CSRF is not a boot-time warn:
|
|
994
|
+
// enforcement is request-time, and every write route fail-closes on
|
|
995
|
+
// `ctx.request.csrfProtected`.)
|
|
941
996
|
this.#warnSeedPermissionsOnce(resources);
|
|
942
997
|
this.#warnAuditGapsOnce(resources);
|
|
943
|
-
|
|
944
|
-
// Phase 3 — route registration. The router proxy may still throw
|
|
945
|
-
// "Router accessed before initialization" on first property access
|
|
946
|
-
// (boot ordering hazard where the proxy module imported but
|
|
947
|
-
// Ignitor's `setRouter` never fired). That's another legitimate
|
|
948
|
-
// degraded-host shape — silent return. Anything else (slug
|
|
949
|
-
// collision, future validation) propagates.
|
|
950
|
-
try {
|
|
951
|
-
this.#registerAdminRoutes(router, resources);
|
|
952
|
-
} catch (err) {
|
|
953
|
-
if (isRouterProxyUninit(err)) return;
|
|
954
|
-
throw err;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
this.#started = true;
|
|
958
998
|
}
|
|
959
999
|
|
|
960
1000
|
/**
|
|
@@ -986,7 +1026,11 @@ export default class StationProvider {
|
|
|
986
1026
|
const rune = await this.#loadRune();
|
|
987
1027
|
return { router, atlas, rune };
|
|
988
1028
|
} catch (err) {
|
|
989
|
-
|
|
1029
|
+
// ONLY when atlas itself is what is missing. An atlas that is
|
|
1030
|
+
// installed but cannot load — a dependency of its own gone, a broken
|
|
1031
|
+
// build — is not a host without atlas, and disabling the admin over
|
|
1032
|
+
// it hides the actual fault.
|
|
1033
|
+
if (isPackageMissing(err, "@c9up/atlas")) return null;
|
|
990
1034
|
throw err;
|
|
991
1035
|
}
|
|
992
1036
|
}
|
|
@@ -1001,7 +1045,7 @@ export default class StationProvider {
|
|
|
1001
1045
|
try {
|
|
1002
1046
|
return loadBearingCast<RuneModule>(await import("@c9up/rune"));
|
|
1003
1047
|
} catch (err) {
|
|
1004
|
-
if (
|
|
1048
|
+
if (isPackageMissing(err, "@c9up/rune")) return null;
|
|
1005
1049
|
throw err;
|
|
1006
1050
|
}
|
|
1007
1051
|
}
|
|
@@ -1198,10 +1242,20 @@ export default class StationProvider {
|
|
|
1198
1242
|
}
|
|
1199
1243
|
|
|
1200
1244
|
/** Phase 3 — mount the login surface + per-resource CRUD routes. */
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
): void {
|
|
1245
|
+
/** Wrap a handler in the auth gate when one is configured. */
|
|
1246
|
+
#gate(
|
|
1247
|
+
handler: (ctx: StationHttpContext) => Promise<void>,
|
|
1248
|
+
): (ctx: StationHttpContext) => Promise<void> {
|
|
1249
|
+
return this.#authConfig.requireAuth ? this.#withAuth(handler) : handler;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* The routes that belong to the admin itself rather than to a resource.
|
|
1254
|
+
*
|
|
1255
|
+
* Registered once, while the provider starts — before the HTTP server is
|
|
1256
|
+
* listening, and before any resource has necessarily been declared.
|
|
1257
|
+
*/
|
|
1258
|
+
#registerShellRoutes(router: StationRouter): void {
|
|
1205
1259
|
// 54.7 — mount login surface first when auth is required, so
|
|
1206
1260
|
// `/admin/login` is reachable even when the auth gate redirects every
|
|
1207
1261
|
// other path to it.
|
|
@@ -1211,22 +1265,30 @@ export default class StationProvider {
|
|
|
1211
1265
|
router.post("/admin/logout", this.#buildLogoutHandler());
|
|
1212
1266
|
}
|
|
1213
1267
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1268
|
+
// `/admin` index — send the operator to the first listable resource (or
|
|
1269
|
+
// the login surface). Without it, the post-login redirect("/admin") lands
|
|
1270
|
+
// on a 404.
|
|
1271
|
+
//
|
|
1272
|
+
// The registry is read WHEN THE REQUEST ARRIVES, not when this route is
|
|
1273
|
+
// built: resources are declared in preloads, so a list captured here
|
|
1274
|
+
// would name whatever happened to exist first — or nothing at all.
|
|
1221
1275
|
router.get(
|
|
1222
1276
|
"/admin",
|
|
1223
|
-
gate(async (ctx) => {
|
|
1224
|
-
const
|
|
1277
|
+
this.#gate(async (ctx) => {
|
|
1278
|
+
const registry =
|
|
1279
|
+
await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
|
|
1280
|
+
const home = registry.all().find((r) => r.actions.includes("list"));
|
|
1225
1281
|
ctx.response.redirect(home ? `/admin/${home.name}` : "/admin/login");
|
|
1226
1282
|
}),
|
|
1227
1283
|
);
|
|
1284
|
+
}
|
|
1228
1285
|
|
|
1229
|
-
|
|
1286
|
+
/** Every route one resource contributes, mounted the moment it is declared. */
|
|
1287
|
+
#registerResourceRoutes(router: StationRouter, resource: Resource): void {
|
|
1288
|
+
const gate = (
|
|
1289
|
+
handler: (ctx: StationHttpContext) => Promise<void>,
|
|
1290
|
+
): ((ctx: StationHttpContext) => Promise<void>) => this.#gate(handler);
|
|
1291
|
+
{
|
|
1230
1292
|
const slug = resource.name;
|
|
1231
1293
|
if (resource.actions.includes("list")) {
|
|
1232
1294
|
router.get(`/admin/${slug}`, gate(this.#buildListHandler(resource)));
|
|
@@ -1310,9 +1372,15 @@ export default class StationProvider {
|
|
|
1310
1372
|
);
|
|
1311
1373
|
}
|
|
1312
1374
|
|
|
1313
|
-
async
|
|
1314
|
-
|
|
1315
|
-
|
|
1375
|
+
async shutdown(): Promise<void> {
|
|
1376
|
+
// Release the module-level singleton, while it is still ours. A stopped
|
|
1377
|
+
// application left a dead resource registry reachable through `services/main`, and
|
|
1378
|
+
// with two applications in one process the survivor's binding must not
|
|
1379
|
+
// be the one cleared.
|
|
1380
|
+
if (this.#owned !== undefined && getStation() === this.#owned)
|
|
1381
|
+
clearStation();
|
|
1382
|
+
this.#owned = undefined;
|
|
1383
|
+
}
|
|
1316
1384
|
|
|
1317
1385
|
#requireContext(resource: Resource): ResourceContext {
|
|
1318
1386
|
const ctx = this.#contexts.get(resource);
|
|
@@ -1344,7 +1412,7 @@ export default class StationProvider {
|
|
|
1344
1412
|
* Fail-closed response for invalid write input (57.7, AC4). Content-negotiated:
|
|
1345
1413
|
*
|
|
1346
1414
|
* - **JSON/XHR** → `422` with `{ error, code: "E_VALIDATION_ERROR", messages }`
|
|
1347
|
-
* (Adonis
|
|
1415
|
+
* (Adonis parity — API clients get the machine-readable errors).
|
|
1348
1416
|
* - **HTML with a session** → the AdonisJS web idiom (PRG): flash the old
|
|
1349
1417
|
* input + per-field errors, then `redirect().back()` to the form `GET`,
|
|
1350
1418
|
* which re-reads the flash and re-renders with values + errors. No inline
|
|
@@ -1367,7 +1435,7 @@ export default class StationProvider {
|
|
|
1367
1435
|
): Promise<void> {
|
|
1368
1436
|
if (wantsJsonResponse(ctx)) {
|
|
1369
1437
|
ctx.response.status(422);
|
|
1370
|
-
// AdonisJS
|
|
1438
|
+
// AdonisJS parity: `{ errors: [{ field, rule, message }] }` —
|
|
1371
1439
|
// the same shape ream's own `E_VALIDATION_ERROR` handler emits.
|
|
1372
1440
|
ctx.response.json({
|
|
1373
1441
|
errors: errors.map((e) => ({
|
|
@@ -2155,6 +2223,35 @@ export function isModuleNotFound(err: unknown): boolean {
|
|
|
2155
2223
|
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
2156
2224
|
}
|
|
2157
2225
|
|
|
2226
|
+
/**
|
|
2227
|
+
* Is THIS package the one that is missing?
|
|
2228
|
+
*
|
|
2229
|
+
* The code alone does not say. `@c9up/atlas` installed but missing one of its
|
|
2230
|
+
* OWN dependencies raises the same `ERR_MODULE_NOT_FOUND`, and station read
|
|
2231
|
+
* that as "atlas is absent" and disabled the admin — silently, which is the
|
|
2232
|
+
* whole point of the degraded-host path and exactly wrong here: the peer IS
|
|
2233
|
+
* there and something inside it is broken, which the operator has to be told.
|
|
2234
|
+
*
|
|
2235
|
+
* Node distinguishes the two in the message it writes:
|
|
2236
|
+
*
|
|
2237
|
+
* Cannot find package '@c9up/atlas' imported from … ← the peer is absent
|
|
2238
|
+
* Cannot find module '/…/node_modules/@c9up/atlas/…' ← something inside it
|
|
2239
|
+
*
|
|
2240
|
+
* A bare-specifier miss names the PACKAGE; a miss inside an installed package
|
|
2241
|
+
* names a path. Anything that is not unmistakably the first is re-thrown.
|
|
2242
|
+
*/
|
|
2243
|
+
export function isPackageMissing(err: unknown, specifier: string): boolean {
|
|
2244
|
+
if (!isModuleNotFound(err)) return false;
|
|
2245
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2246
|
+
// The exact forms Node emits for a bare specifier that resolves to nothing.
|
|
2247
|
+
return (
|
|
2248
|
+
message.includes(`Cannot find package '${specifier}'`) ||
|
|
2249
|
+
message.includes(`Cannot find module '${specifier}'`) ||
|
|
2250
|
+
// The CJS wording, kept for a host that transpiles.
|
|
2251
|
+
message.includes(`Cannot find module "${specifier}"`)
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2158
2255
|
/** Ream's router proxy throws this exact string before Ignitor wires it. */
|
|
2159
2256
|
function isRouterProxyUninit(err: unknown): boolean {
|
|
2160
2257
|
return (
|
package/src/services/main.ts
CHANGED
|
@@ -23,6 +23,18 @@ export function getStation(): ResourceRegistry | undefined {
|
|
|
23
23
|
return instance;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @internal Release the singleton, so a shut-down application does not leave a
|
|
28
|
+
* dead resource registry reachable through `services/main`.
|
|
29
|
+
*
|
|
30
|
+
* The caller checks ownership first (`getStation() === mine`): two applications
|
|
31
|
+
* share this module in one process, and the one shutting down must not clear
|
|
32
|
+
* what the other has since bound.
|
|
33
|
+
*/
|
|
34
|
+
export function clearStation(): void {
|
|
35
|
+
instance = undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
const station: ResourceRegistry = new Proxy({} as ResourceRegistry, {
|
|
27
39
|
get(_target, prop) {
|
|
28
40
|
if (!instance) {
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
// Generated from scripts/vendor/nodeEnv.ts — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// This package is published and built from its own repository, so the file
|
|
4
|
+
// has to exist here rather than be imported. `pnpm vendor:sync` rewrites it,
|
|
5
|
+
// and `pnpm vendor:check` fails if this copy has drifted from the original.
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
|
-
*
|
|
8
|
+
* `NODE_ENV`, read the way every runtime actually spells it.
|
|
3
9
|
*
|
|
4
|
-
* `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard
|
|
5
|
-
*
|
|
6
|
-
*
|
|
10
|
+
* `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard, and a
|
|
11
|
+
* bare `=== "production"` answers "not production" for it. What that costs
|
|
12
|
+
* differs per package, so the consequence is stated where the decision is
|
|
13
|
+
* taken, not here.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
15
|
+
* The tables mirror the framework's (`@c9up/ream`, src/env/nodeEnv.ts), which
|
|
16
|
+
* is the origin rather than a peer: it formats to different rules and exposes
|
|
17
|
+
* `currentNodeEnv()` instead of `inProduction()`.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
const DEV_ENVS = ["dev", "develop", "development"];
|
package/dist/nodeEnv.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading `NODE_ENV`, with the aliases people actually set.
|
|
3
|
-
*
|
|
4
|
-
* `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard. Read
|
|
5
|
-
* verbatim it answers "not production" — and here that decides whether an
|
|
6
|
-
* admin panel mounts with no authorisation at all.
|
|
7
|
-
*
|
|
8
|
-
* Duplicated rather than imported: station's peers are all optional, and a
|
|
9
|
-
* safety decision that only holds when one of them is installed is not a
|
|
10
|
-
* decision.
|
|
11
|
-
*/
|
|
12
|
-
/** The canonical name for whatever `NODE_ENV` holds. */
|
|
13
|
-
export declare function normalizeNodeEnv(value: string | undefined): string;
|
|
14
|
-
/** Whether this process is running in production, under any spelling. */
|
|
15
|
-
export declare function inProduction(): boolean;
|
|
16
|
-
//# sourceMappingURL=nodeEnv.d.ts.map
|
package/dist/nodeEnv.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"nodeEnv.d.ts","sourceRoot":"","sources":["../src/nodeEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAOlE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
|
package/dist/nodeEnv.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"nodeEnv.js","sourceRoot":"","sources":["../src/nodeEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC;AACnD,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AACzC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEtC,wDAAwD;AACxD,MAAM,UAAU,gBAAgB,CAAC,KAAyB;IACzD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAChC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,aAAa,CAAC;IACjD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IACjD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IAC3C,OAAO,GAAG,CAAC;AACZ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY;IAC3B,OAAO,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC;AAChE,CAAC"}
|