@tulipes/core 0.1.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/dist/acl/acl.d.ts +49 -0
- package/dist/acl/acl.js +123 -0
- package/dist/acl/acl.js.map +1 -0
- package/dist/acl/index.d.ts +2 -0
- package/dist/acl/index.js +3 -0
- package/dist/acl/index.js.map +1 -0
- package/dist/acl/load-acl.d.ts +11 -0
- package/dist/acl/load-acl.js +38 -0
- package/dist/acl/load-acl.js.map +1 -0
- package/dist/boot/app-files.d.ts +17 -0
- package/dist/boot/app-files.js +31 -0
- package/dist/boot/app-files.js.map +1 -0
- package/dist/boot/banner.d.ts +9 -0
- package/dist/boot/banner.js +82 -0
- package/dist/boot/banner.js.map +1 -0
- package/dist/boot/boot.d.ts +32 -0
- package/dist/boot/boot.js +212 -0
- package/dist/boot/boot.js.map +1 -0
- package/dist/boot/contracts.d.ts +47 -0
- package/dist/boot/contracts.js +2 -0
- package/dist/boot/contracts.js.map +1 -0
- package/dist/boot/import-file.d.ts +7 -0
- package/dist/boot/import-file.js +23 -0
- package/dist/boot/import-file.js.map +1 -0
- package/dist/boot/index.d.ts +3 -0
- package/dist/boot/index.js +2 -0
- package/dist/boot/index.js.map +1 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +7 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/dev.d.ts +8 -0
- package/dist/cli/dev.js +22 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/env-check.d.ts +6 -0
- package/dist/cli/env-check.js +28 -0
- package/dist/cli/env-check.js.map +1 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +45 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/new-module.d.ts +6 -0
- package/dist/cli/new-module.js +84 -0
- package/dist/cli/new-module.js.map +1 -0
- package/dist/cli/sync.d.ts +12 -0
- package/dist/cli/sync.js +117 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config/build-config.d.ts +13 -0
- package/dist/config/build-config.js +63 -0
- package/dist/config/build-config.js.map +1 -0
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/database.d.ts +25 -0
- package/dist/db/database.js +111 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/model-store.d.ts +22 -0
- package/dist/db/model-store.js +37 -0
- package/dist/db/model-store.js.map +1 -0
- package/dist/env/coerce.d.ts +17 -0
- package/dist/env/coerce.js +53 -0
- package/dist/env/coerce.js.map +1 -0
- package/dist/env/environment.d.ts +42 -0
- package/dist/env/environment.js +117 -0
- package/dist/env/environment.js.map +1 -0
- package/dist/env/index.d.ts +14 -0
- package/dist/env/index.js +5 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/meta-discovery.d.ts +17 -0
- package/dist/env/meta-discovery.js +65 -0
- package/dist/env/meta-discovery.js.map +1 -0
- package/dist/env/spec.d.ts +133 -0
- package/dist/env/spec.js +63 -0
- package/dist/env/spec.js.map +1 -0
- package/dist/env/variable-store.d.ts +26 -0
- package/dist/env/variable-store.js +49 -0
- package/dist/env/variable-store.js.map +1 -0
- package/dist/errors/boot-error.d.ts +28 -0
- package/dist/errors/boot-error.js +44 -0
- package/dist/errors/boot-error.js.map +1 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +2 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http/error-handler.d.ts +23 -0
- package/dist/http/error-handler.js +48 -0
- package/dist/http/error-handler.js.map +1 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +2 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/discovery.d.ts +10 -0
- package/dist/modules/discovery.js +91 -0
- package/dist/modules/discovery.js.map +1 -0
- package/dist/modules/explorer.d.ts +38 -0
- package/dist/modules/explorer.js +64 -0
- package/dist/modules/explorer.js.map +1 -0
- package/dist/modules/graph.d.ts +16 -0
- package/dist/modules/graph.js +80 -0
- package/dist/modules/graph.js.map +1 -0
- package/dist/modules/index.d.ts +4 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/manifest.d.ts +43 -0
- package/dist/modules/manifest.js +26 -0
- package/dist/modules/manifest.js.map +1 -0
- package/dist/queues/index.d.ts +2 -0
- package/dist/queues/index.js +3 -0
- package/dist/queues/index.js.map +1 -0
- package/dist/queues/load-queues.d.ts +12 -0
- package/dist/queues/load-queues.js +56 -0
- package/dist/queues/load-queues.js.map +1 -0
- package/dist/queues/queue-manager.d.ts +44 -0
- package/dist/queues/queue-manager.js +134 -0
- package/dist/queues/queue-manager.js.map +1 -0
- package/dist/sockets/index.d.ts +2 -0
- package/dist/sockets/index.js +3 -0
- package/dist/sockets/index.js.map +1 -0
- package/dist/sockets/load-sockets.d.ts +13 -0
- package/dist/sockets/load-sockets.js +42 -0
- package/dist/sockets/load-sockets.js.map +1 -0
- package/dist/sockets/socket-manager.d.ts +39 -0
- package/dist/sockets/socket-manager.js +85 -0
- package/dist/sockets/socket-manager.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
/**
|
|
3
|
+
* Global ACL registry.
|
|
4
|
+
*
|
|
5
|
+
* Roles are app-global (the sys core module defines them); every other
|
|
6
|
+
* module attaches permission grants to existing roles for its own resources.
|
|
7
|
+
* Resources are namespaced strings — `users:write`, `categories:read`,
|
|
8
|
+
* `users:*` — where the prefix is, by convention, the owning module.
|
|
9
|
+
*
|
|
10
|
+
* Every mutation happens through a per-module `AclBuilder` so each role
|
|
11
|
+
* definition and grant records who made it: that is what lets a conflict
|
|
12
|
+
* crash the boot with both module names in the message.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Acl {
|
|
15
|
+
#private;
|
|
16
|
+
forModule(moduleName: string, report: BootReport): AclBuilder;
|
|
17
|
+
/**
|
|
18
|
+
* Runtime permission check. Unknown roles simply deny — at request time
|
|
19
|
+
* the safe answer to "can this unheard-of role do X?" is always no.
|
|
20
|
+
*/
|
|
21
|
+
can(role: string, permission: string): boolean;
|
|
22
|
+
hasRole(role: string): boolean;
|
|
23
|
+
roles(): string[];
|
|
24
|
+
/** Flat grant list — feeds logs, docs and the future postman generator. */
|
|
25
|
+
describe(): {
|
|
26
|
+
role: string;
|
|
27
|
+
resource: string;
|
|
28
|
+
grantedBy: string;
|
|
29
|
+
}[];
|
|
30
|
+
/** @internal AclBuilder only. */
|
|
31
|
+
defineRole_(role: string, moduleName: string): string | undefined;
|
|
32
|
+
/** @internal AclBuilder only. */
|
|
33
|
+
grant_(role: string, resource: string, moduleName: string): string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The module-scoped facade handed to `module.acl.ts`. All validation issues
|
|
37
|
+
* flow into the boot report — collected, never thrown — so a module with
|
|
38
|
+
* three bad grants shows all three.
|
|
39
|
+
*/
|
|
40
|
+
export declare class AclBuilder {
|
|
41
|
+
#private;
|
|
42
|
+
constructor(acl: Acl, moduleName: string, report: BootReport);
|
|
43
|
+
/**
|
|
44
|
+
* Introducing a role is deliberately explicit — a typo in `allow()` must
|
|
45
|
+
* be an error, never a silently created role.
|
|
46
|
+
*/
|
|
47
|
+
defineRole(role: string): this;
|
|
48
|
+
allow(role: string, ...resources: string[]): this;
|
|
49
|
+
}
|
package/dist/acl/acl.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
/**
|
|
3
|
+
* Global ACL registry.
|
|
4
|
+
*
|
|
5
|
+
* Roles are app-global (the sys core module defines them); every other
|
|
6
|
+
* module attaches permission grants to existing roles for its own resources.
|
|
7
|
+
* Resources are namespaced strings — `users:write`, `categories:read`,
|
|
8
|
+
* `users:*` — where the prefix is, by convention, the owning module.
|
|
9
|
+
*
|
|
10
|
+
* Every mutation happens through a per-module `AclBuilder` so each role
|
|
11
|
+
* definition and grant records who made it: that is what lets a conflict
|
|
12
|
+
* crash the boot with both module names in the message.
|
|
13
|
+
*/
|
|
14
|
+
export class Acl {
|
|
15
|
+
/** role → module that defined it */
|
|
16
|
+
#roles = new Map();
|
|
17
|
+
/** role → (resource → module that granted it) */
|
|
18
|
+
#grants = new Map();
|
|
19
|
+
forModule(moduleName, report) {
|
|
20
|
+
return new AclBuilder(this, moduleName, report);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runtime permission check. Unknown roles simply deny — at request time
|
|
24
|
+
* the safe answer to "can this unheard-of role do X?" is always no.
|
|
25
|
+
*/
|
|
26
|
+
can(role, permission) {
|
|
27
|
+
const grants = this.#grants.get(role);
|
|
28
|
+
if (!grants)
|
|
29
|
+
return false;
|
|
30
|
+
if (grants.has(permission) || grants.has("*"))
|
|
31
|
+
return true;
|
|
32
|
+
const namespace = permission.split(":")[0];
|
|
33
|
+
return grants.has(`${namespace}:*`);
|
|
34
|
+
}
|
|
35
|
+
hasRole(role) {
|
|
36
|
+
return this.#roles.has(role);
|
|
37
|
+
}
|
|
38
|
+
roles() {
|
|
39
|
+
return [...this.#roles.keys()];
|
|
40
|
+
}
|
|
41
|
+
/** Flat grant list — feeds logs, docs and the future postman generator. */
|
|
42
|
+
describe() {
|
|
43
|
+
return [...this.#grants.entries()].flatMap(([role, grants]) => [...grants.entries()].map(([resource, grantedBy]) => ({
|
|
44
|
+
role,
|
|
45
|
+
resource,
|
|
46
|
+
grantedBy,
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
49
|
+
/** @internal AclBuilder only. */
|
|
50
|
+
defineRole_(role, moduleName) {
|
|
51
|
+
const existing = this.#roles.get(role);
|
|
52
|
+
if (existing)
|
|
53
|
+
return existing;
|
|
54
|
+
this.#roles.set(role, moduleName);
|
|
55
|
+
this.#grants.set(role, new Map());
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/** @internal AclBuilder only. */
|
|
59
|
+
grant_(role, resource, moduleName) {
|
|
60
|
+
const grants = this.#grants.get(role);
|
|
61
|
+
if (!grants)
|
|
62
|
+
return undefined; // caller reports the unknown role
|
|
63
|
+
const existing = grants.get(resource);
|
|
64
|
+
if (existing)
|
|
65
|
+
return existing;
|
|
66
|
+
grants.set(resource, moduleName);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** `name:action`, `name:*`, or the standalone `*`. */
|
|
71
|
+
const RESOURCE = /^([a-z0-9-]+:[a-z0-9-]+|[a-z0-9-]+:\*|\*)$/;
|
|
72
|
+
const ROLE = /^[a-z][a-z0-9-]*$/;
|
|
73
|
+
/**
|
|
74
|
+
* The module-scoped facade handed to `module.acl.ts`. All validation issues
|
|
75
|
+
* flow into the boot report — collected, never thrown — so a module with
|
|
76
|
+
* three bad grants shows all three.
|
|
77
|
+
*/
|
|
78
|
+
export class AclBuilder {
|
|
79
|
+
#acl;
|
|
80
|
+
#module;
|
|
81
|
+
#report;
|
|
82
|
+
constructor(acl, moduleName, report) {
|
|
83
|
+
this.#acl = acl;
|
|
84
|
+
this.#module = moduleName;
|
|
85
|
+
this.#report = report;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Introducing a role is deliberately explicit — a typo in `allow()` must
|
|
89
|
+
* be an error, never a silently created role.
|
|
90
|
+
*/
|
|
91
|
+
defineRole(role) {
|
|
92
|
+
if (!ROLE.test(role)) {
|
|
93
|
+
this.#issue(`role "${role}" is invalid — lowercase kebab-case required`);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
const definedBy = this.#acl.defineRole_(role, this.#module);
|
|
97
|
+
if (definedBy) {
|
|
98
|
+
this.#issue(`role "${role}" is already defined by module "${definedBy}"`);
|
|
99
|
+
}
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
allow(role, ...resources) {
|
|
103
|
+
if (!this.#acl.hasRole(role)) {
|
|
104
|
+
this.#issue(`allow() on unknown role "${role}" — roles must be defined first (defineRole)`);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
for (const resource of resources) {
|
|
108
|
+
if (!RESOURCE.test(resource)) {
|
|
109
|
+
this.#issue(`resource "${resource}" is invalid — expected "name:action", "name:*" or "*"`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const grantedBy = this.#acl.grant_(role, resource, this.#module);
|
|
113
|
+
if (grantedBy) {
|
|
114
|
+
this.#issue(`"${role}" already granted "${resource}" by module "${grantedBy}" — duplicate grant`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
#issue(message) {
|
|
120
|
+
this.#report.add({ scope: "acl", module: this.#module, message });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=acl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acl.js","sourceRoot":"","sources":["../../src/acl/acl.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/acl/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAc,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
import type { ModuleFiles } from "../modules/explorer.js";
|
|
3
|
+
import { Acl, type AclBuilder } from "./acl.js";
|
|
4
|
+
/** Default export of `module.acl.ts`. */
|
|
5
|
+
export type AclFn = (acl: AclBuilder) => void | Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Boot phase 7: run every `module.acl.ts` in load order against the shared
|
|
8
|
+
* registry. Load order is what makes "define roles in the sys core module,
|
|
9
|
+
* grant everywhere else" work without further ceremony: sys runs first.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadAcl(explored: readonly ModuleFiles[], report: BootReport): Promise<Acl>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
import { importFile } from "../boot/import-file.js";
|
|
3
|
+
import { Acl } from "./acl.js";
|
|
4
|
+
/**
|
|
5
|
+
* Boot phase 7: run every `module.acl.ts` in load order against the shared
|
|
6
|
+
* registry. Load order is what makes "define roles in the sys core module,
|
|
7
|
+
* grant everywhere else" work without further ceremony: sys runs first.
|
|
8
|
+
*/
|
|
9
|
+
export async function loadAcl(explored, report) {
|
|
10
|
+
const acl = new Acl();
|
|
11
|
+
for (const { module, aclFile } of explored) {
|
|
12
|
+
if (!aclFile)
|
|
13
|
+
continue;
|
|
14
|
+
const exported = await importFile(aclFile, "acl", module.name, report);
|
|
15
|
+
if (!exported)
|
|
16
|
+
continue;
|
|
17
|
+
if (typeof exported.default !== "function") {
|
|
18
|
+
report.add({
|
|
19
|
+
scope: "acl",
|
|
20
|
+
module: module.name,
|
|
21
|
+
message: "module.acl must default-export a function (acl) => void",
|
|
22
|
+
});
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await exported.default(acl.forModule(module.name, report));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
report.add({
|
|
30
|
+
scope: "acl",
|
|
31
|
+
module: module.name,
|
|
32
|
+
message: `acl function threw: ${error.message}`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return acl;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=load-acl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-acl.js","sourceRoot":"","sources":["../../src/acl/load-acl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD,OAAO,EAAE,GAAG,EAAmB,MAAM,UAAU,CAAC;AAKhD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,QAAgC,EAChC,MAAkB;IAElB,MAAM,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;IAEtB,KAAK,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC3C,IAAI,CAAC,OAAO;YAAE,SAAS;QAEvB,MAAM,QAAQ,GAAG,MAAM,UAAU,CAC/B,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CACpC,CAAC;QACF,IAAI,CAAC,QAAQ;YAAE,SAAS;QAExB,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YAC3C,MAAM,CAAC,GAAG,CAAC;gBACT,KAAK,EAAE,KAAK;gBACZ,MAAM,EAAE,MAAM,CAAC,IAAI;gBACnB,OAAO,EAAE,yDAAyD;aACnE,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,IAAI,CAAC;YACH,MAAO,QAAQ,CAAC,OAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACxE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,GAAG,CAAC;gBACT,KAAK,EAAE,KAAK;gBACZ,MAAM,EAAE,MAAM,CAAC,IAAI;gBACnB,OAAO,EAAE,uBAAwB,KAAe,CAAC,OAAO,EAAE;aAC3D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
import type { Ctx } from "./contracts.js";
|
|
3
|
+
/**
|
|
4
|
+
* The one app-level (non-module) convention file. Optional — an app that
|
|
5
|
+
* doesn't need it simply doesn't create it.
|
|
6
|
+
*
|
|
7
|
+
* config/app.config.ts default-exports (Environment) => object; the
|
|
8
|
+
* result seeds the global config BEFORE any module
|
|
9
|
+
* factory runs. The place for app-wide values that
|
|
10
|
+
* belong to no single module.
|
|
11
|
+
*
|
|
12
|
+
* There is deliberately no app-level middleware file: global middleware is
|
|
13
|
+
* a sys module's job — sys modules load before everything, so their routes
|
|
14
|
+
* files mounting on ctx.app IS the "before all routes" slot.
|
|
15
|
+
*/
|
|
16
|
+
export type AppConfigFn = (environment: Ctx["Environment"]) => Record<string, unknown> | void | Promise<Record<string, unknown> | void>;
|
|
17
|
+
export declare function loadAppConfigSeed(rootDir: string, ctx: Ctx, report: BootReport): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
4
|
+
import { detectSourceExtension } from "../modules/explorer.js";
|
|
5
|
+
import { importFile } from "./import-file.js";
|
|
6
|
+
export async function loadAppConfigSeed(rootDir, ctx, report) {
|
|
7
|
+
const file = join(rootDir, "config", `app.config${detectSourceExtension()}`);
|
|
8
|
+
if (!existsSync(file))
|
|
9
|
+
return;
|
|
10
|
+
const exported = await importFile(file, "config", "app", report);
|
|
11
|
+
if (!exported)
|
|
12
|
+
return;
|
|
13
|
+
if (typeof exported.default !== "function") {
|
|
14
|
+
report.add({
|
|
15
|
+
scope: "config",
|
|
16
|
+
message: "config/app.config must default-export a function (Environment) => object",
|
|
17
|
+
});
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const seed = await exported.default(ctx.Environment);
|
|
22
|
+
Object.assign(ctx.config, seed ?? {});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
report.add({
|
|
26
|
+
scope: "config",
|
|
27
|
+
message: `app.config threw: ${error.message}`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=app-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-files.js","sourceRoot":"","sources":["../../src/boot/app-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAqB9C,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAe,EACf,GAAQ,EACR,MAAkB;IAElB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,qBAAqB,EAAE,EAAE,CAAC,CAAC;IAC7E,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO;IAE9B,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAwB,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACxF,IAAI,CAAC,QAAQ;QAAE,OAAO;IAEtB,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC;YACT,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,0EAA0E;SACpF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAO,QAAQ,CAAC,OAAuB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACtE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,GAAG,CAAC;YACT,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,qBAAsB,KAAe,CAAC,OAAO,EAAE;SACzD,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BootHandle } from "./boot.js";
|
|
2
|
+
import type { ModuleManifest } from "../modules/manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* The end-of-boot banner: one glance answers every "is it actually up?"
|
|
5
|
+
* question — where the server listens, database state, queues, sockets,
|
|
6
|
+
* modules, roles, runtime. Colorette strips ANSI when stdout isn't a TTY,
|
|
7
|
+
* so piped/production logs stay clean text.
|
|
8
|
+
*/
|
|
9
|
+
export declare function printStartupBanner(handle: BootHandle, modules: readonly ModuleManifest[], bootMs: number): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { bold, cyan, cyanBright, dim, gray, green, greenBright, magenta, red, } from "colorette";
|
|
2
|
+
/**
|
|
3
|
+
* The end-of-boot banner: one glance answers every "is it actually up?"
|
|
4
|
+
* question — where the server listens, database state, queues, sockets,
|
|
5
|
+
* modules, roles, runtime. Colorette strips ANSI when stdout isn't a TTY,
|
|
6
|
+
* so piped/production logs stay clean text.
|
|
7
|
+
*/
|
|
8
|
+
export function printStartupBanner(handle, modules, bootMs) {
|
|
9
|
+
const { env, ctx, mode } = handle;
|
|
10
|
+
const ec = envColor(env.appEnv);
|
|
11
|
+
const appName = String(ctx.config.app?.name ?? "tulipes");
|
|
12
|
+
const rows = [];
|
|
13
|
+
if (handle.server) {
|
|
14
|
+
const address = handle.server.address();
|
|
15
|
+
const port = typeof address === "object" && address ? address.port : "?";
|
|
16
|
+
rows.push([
|
|
17
|
+
"Server",
|
|
18
|
+
`${cyan(`http://localhost:${port}`)} ${dot(true)} ${green("listening")}`,
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const workers = ctx.queues?.workerCount ?? 0;
|
|
23
|
+
rows.push([
|
|
24
|
+
"Worker",
|
|
25
|
+
`${dot(workers > 0)} ${green(`consuming ${workers} queue(s)`)}`,
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
if (handle.connection) {
|
|
29
|
+
const { host, port, name, readyState } = handle.connection;
|
|
30
|
+
const connected = readyState === 1;
|
|
31
|
+
rows.push([
|
|
32
|
+
"Database",
|
|
33
|
+
`${cyan(`mongodb://${host}:${port}/${name}`)} ${dot(connected)} ${connected ? green("connected") : red("disconnected")}`,
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
rows.push(["Database", gray("not configured")]);
|
|
38
|
+
}
|
|
39
|
+
if (ctx.queues) {
|
|
40
|
+
const names = ctx.queues.names();
|
|
41
|
+
rows.push([
|
|
42
|
+
"Queues",
|
|
43
|
+
`${bold(String(names.length))} ${gray(`(${names.join(", ")})`)} ${dim("·")} ${gray(`${ctx.queues.processorCount} processor(s) registered`)}`,
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
if (ctx.sockets) {
|
|
47
|
+
rows.push(["Sockets", cyan(ctx.sockets.namespaces().join(", ") || "—")]);
|
|
48
|
+
}
|
|
49
|
+
rows.push([
|
|
50
|
+
"Modules",
|
|
51
|
+
`${bold(String(modules.length))} ${gray(`(${modules.map((m) => (m.tier === "sys" ? `${m.name}*` : m.name)).join(", ")})`)} ${dim("·")} ${gray("* sys tier")}`,
|
|
52
|
+
]);
|
|
53
|
+
if (ctx.acl) {
|
|
54
|
+
const roles = ctx.acl.roles();
|
|
55
|
+
rows.push(["Roles", `${bold(String(roles.length))} ${gray(`(${roles.join(", ")})`)}`]);
|
|
56
|
+
}
|
|
57
|
+
rows.push([
|
|
58
|
+
"Runtime",
|
|
59
|
+
`${gray(`Node ${process.version}`)} ${dim("·")} ${gray(`PID ${process.pid}`)} ${dim("·")} ${gray(`ready in ${bootMs}ms`)}`,
|
|
60
|
+
]);
|
|
61
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
62
|
+
const arrow = green("➜");
|
|
63
|
+
const lines = [
|
|
64
|
+
"",
|
|
65
|
+
` ${bold(cyan(`▲ ${appName}`))} ${ec(bold(env.appEnv))} ${dim("·")} ${dim(`${mode} mode`)}`,
|
|
66
|
+
"",
|
|
67
|
+
...rows.map(([label, value]) => ` ${arrow} ${bold(label.padEnd(labelWidth))} ${value}`),
|
|
68
|
+
"",
|
|
69
|
+
];
|
|
70
|
+
console.log(lines.join("\n"));
|
|
71
|
+
}
|
|
72
|
+
function envColor(env) {
|
|
73
|
+
if (env === "production")
|
|
74
|
+
return greenBright;
|
|
75
|
+
if (env === "test")
|
|
76
|
+
return magenta;
|
|
77
|
+
return cyanBright;
|
|
78
|
+
}
|
|
79
|
+
function dot(ok) {
|
|
80
|
+
return ok ? green("●") : red("●");
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=banner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.js","sourceRoot":"","sources":["../../src/boot/banner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { Connection } from "mongoose";
|
|
3
|
+
import { type Express } from "express";
|
|
4
|
+
import { Environment, type AppEnv } from "../env/environment.js";
|
|
5
|
+
import type { Ctx } from "./contracts.js";
|
|
6
|
+
export declare const processModes: readonly ["web", "worker"];
|
|
7
|
+
export type ProcessMode = (typeof processModes)[number];
|
|
8
|
+
export interface BootOptions {
|
|
9
|
+
/** App root — the directory containing `modules/` and `.envs/`. */
|
|
10
|
+
rootDir: string;
|
|
11
|
+
/** Defaults to process.env.PROCESS_MODE, then "web". */
|
|
12
|
+
mode?: ProcessMode;
|
|
13
|
+
appEnv?: AppEnv;
|
|
14
|
+
/** Skip SIGTERM/SIGINT wiring — for tests that manage shutdown themselves. */
|
|
15
|
+
handleSignals?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface BootHandle {
|
|
18
|
+
mode: ProcessMode;
|
|
19
|
+
env: Environment;
|
|
20
|
+
ctx: Ctx;
|
|
21
|
+
connection?: Connection;
|
|
22
|
+
app?: Express;
|
|
23
|
+
server?: Server;
|
|
24
|
+
shutdown: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The whole run workflow, in the order the POC pins down:
|
|
28
|
+
* environment → module graph → exploration → global config → (acl, db —
|
|
29
|
+
* future steps) → mode fork (web | worker) → onReady. Each phase crashes
|
|
30
|
+
* with its aggregate report before the next begins.
|
|
31
|
+
*/
|
|
32
|
+
export declare function boot(options: BootOptions): Promise<BootHandle>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import express, {} from "express";
|
|
4
|
+
import { Environment } from "../env/environment.js";
|
|
5
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
6
|
+
import { discoverModules } from "../modules/discovery.js";
|
|
7
|
+
import { sortModules } from "../modules/graph.js";
|
|
8
|
+
import { exploreModules } from "../modules/explorer.js";
|
|
9
|
+
import { buildGlobalConfig } from "../config/build-config.js";
|
|
10
|
+
import { loadAcl } from "../acl/load-acl.js";
|
|
11
|
+
import { initDatabase } from "../db/database.js";
|
|
12
|
+
import { initQueues } from "../queues/load-queues.js";
|
|
13
|
+
import { initSockets } from "../sockets/load-sockets.js";
|
|
14
|
+
import { errorHandler, notFoundHandler } from "../http/error-handler.js";
|
|
15
|
+
import { loadAppConfigSeed } from "./app-files.js";
|
|
16
|
+
import { printStartupBanner } from "./banner.js";
|
|
17
|
+
import { importFile } from "./import-file.js";
|
|
18
|
+
export const processModes = ["web", "worker"];
|
|
19
|
+
/**
|
|
20
|
+
* The whole run workflow, in the order the POC pins down:
|
|
21
|
+
* environment → module graph → exploration → global config → (acl, db —
|
|
22
|
+
* future steps) → mode fork (web | worker) → onReady. Each phase crashes
|
|
23
|
+
* with its aggregate report before the next begins.
|
|
24
|
+
*/
|
|
25
|
+
export async function boot(options) {
|
|
26
|
+
const bootStartedAt = Date.now();
|
|
27
|
+
const mode = resolveMode(options.mode);
|
|
28
|
+
// Phases 1–3 — Environment engine (crashes internally on bad env)
|
|
29
|
+
const env = Environment.load({
|
|
30
|
+
rootDir: options.rootDir,
|
|
31
|
+
...(options.appEnv && { appEnv: options.appEnv }),
|
|
32
|
+
});
|
|
33
|
+
// Phase 4 — module graph
|
|
34
|
+
const graphReport = new BootReport();
|
|
35
|
+
const manifests = discoverModules(join(options.rootDir, "modules"), graphReport);
|
|
36
|
+
const order = sortModules(manifests, graphReport);
|
|
37
|
+
graphReport.throwIfAny("module graph");
|
|
38
|
+
// Phase 5 — exploration (no failure modes)
|
|
39
|
+
const explored = exploreModules(order);
|
|
40
|
+
// Phase 6 — global config: app-level seed first, then module factories
|
|
41
|
+
const ctx = { Environment: env, config: {} };
|
|
42
|
+
const configReport = new BootReport();
|
|
43
|
+
await loadAppConfigSeed(options.rootDir, ctx, configReport);
|
|
44
|
+
const lifecycles = await buildGlobalConfig(explored, ctx, configReport);
|
|
45
|
+
configReport.throwIfAny("config");
|
|
46
|
+
// Phase 7 — ACL merge
|
|
47
|
+
const aclReport = new BootReport();
|
|
48
|
+
ctx.acl = await loadAcl(explored, aclReport);
|
|
49
|
+
aclReport.throwIfAny("acl");
|
|
50
|
+
// Phase 8 — DB + models + bootstrap
|
|
51
|
+
const dbReport = new BootReport();
|
|
52
|
+
const connection = await initDatabase(explored, ctx, dbReport);
|
|
53
|
+
dbReport.throwIfAny("database");
|
|
54
|
+
// Phase 9 — mode fork
|
|
55
|
+
const handle = {
|
|
56
|
+
mode,
|
|
57
|
+
env,
|
|
58
|
+
ctx,
|
|
59
|
+
connection,
|
|
60
|
+
shutdown: () => shutdown(handle, lifecycles),
|
|
61
|
+
};
|
|
62
|
+
// Phase 9 — queues register in both modes (producers everywhere; the
|
|
63
|
+
// worker branch additionally starts consuming below).
|
|
64
|
+
const queueReport = new BootReport();
|
|
65
|
+
await initQueues(explored, ctx, queueReport);
|
|
66
|
+
queueReport.throwIfAny("queues");
|
|
67
|
+
if (mode === "web") {
|
|
68
|
+
await startWeb(handle, explored);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const workerReport = new BootReport();
|
|
72
|
+
const started = ctx.queues?.startWorkers(workerReport) ?? 0;
|
|
73
|
+
workerReport.throwIfAny("workers");
|
|
74
|
+
// BullMQ workers hold the event loop open; a worker with nothing to
|
|
75
|
+
// consume is a misconfigured deployment, not a valid idle state.
|
|
76
|
+
if (started === 0) {
|
|
77
|
+
throw new Error("worker mode started with zero processors — no module registered queue work");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Phase 12 — onReady, load order
|
|
81
|
+
for (const { module, onReady } of lifecycles) {
|
|
82
|
+
try {
|
|
83
|
+
await onReady?.(ctx);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Late-phase hook failures must still take the process down cleanly:
|
|
87
|
+
// a half-ready app serving traffic is worse than no app.
|
|
88
|
+
await handle.shutdown();
|
|
89
|
+
throw new Error(`onReady hook of "${module}" failed: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
printStartupBanner(handle, order, Date.now() - bootStartedAt);
|
|
93
|
+
if (options.handleSignals !== false) {
|
|
94
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
95
|
+
process.once(signal, () => {
|
|
96
|
+
console.log(`\n[tulipes] ${signal} — shutting down`);
|
|
97
|
+
handle.shutdown().then(() => process.exit(0), (error) => {
|
|
98
|
+
console.error("[tulipes] shutdown failed:", error);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return handle;
|
|
105
|
+
}
|
|
106
|
+
function resolveMode(override) {
|
|
107
|
+
const raw = override ?? process.env.PROCESS_MODE ?? "web";
|
|
108
|
+
if (!processModes.includes(raw)) {
|
|
109
|
+
throw new Error(`PROCESS_MODE "${raw}" is invalid — expected one of: ${processModes.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
return raw;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Phases 9w–11w: express, module routes, http server.
|
|
115
|
+
*
|
|
116
|
+
* The request pipeline, in order:
|
|
117
|
+
* 1. every module router (load order — sys modules first, so their
|
|
118
|
+
* routes files mounting on ctx.app ARE the
|
|
119
|
+
* global middleware slot: request ids and
|
|
120
|
+
* cross-cutting glue in the core module,
|
|
121
|
+
* headers/logging/body parsing in security)
|
|
122
|
+
* 2. notFoundHandler (core)
|
|
123
|
+
* 3. errorHandler (core — terminal, catches async throws too)
|
|
124
|
+
*
|
|
125
|
+
* Core deliberately mounts NO middleware of its own: what runs on every
|
|
126
|
+
* request is the app's decision, expressed as sys modules within the
|
|
127
|
+
* developer's reach. Core only guarantees the fail-closed tail — nothing
|
|
128
|
+
* escapes unrouted or unhandled. @tulipes/core/http ships only that tail's
|
|
129
|
+
* building blocks (HttpError, notFoundHandler, errorHandler).
|
|
130
|
+
*/
|
|
131
|
+
async function startWeb(handle, explored) {
|
|
132
|
+
const app = express();
|
|
133
|
+
handle.app = app;
|
|
134
|
+
handle.ctx.app = app;
|
|
135
|
+
const report = new BootReport();
|
|
136
|
+
for (const { module, routeFiles } of explored) {
|
|
137
|
+
for (const file of routeFiles) {
|
|
138
|
+
const exported = await importFile(file, "boot", module.name, report);
|
|
139
|
+
if (!exported)
|
|
140
|
+
continue;
|
|
141
|
+
if (typeof exported.default !== "function") {
|
|
142
|
+
report.add({
|
|
143
|
+
scope: "boot",
|
|
144
|
+
module: module.name,
|
|
145
|
+
message: `${file} must default-export a routes function`,
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const router = await exported.default(handle.ctx);
|
|
151
|
+
// Mounting the return value is optional sugar — a routes fn may
|
|
152
|
+
// have attached handlers to ctx.app directly instead. Routers are
|
|
153
|
+
// callables in express, so a function return is a mountable.
|
|
154
|
+
if (typeof router === "function")
|
|
155
|
+
app.use(router);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
report.add({
|
|
159
|
+
scope: "boot",
|
|
160
|
+
module: module.name,
|
|
161
|
+
message: `routes function threw: ${error.message}`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
app.use(notFoundHandler());
|
|
167
|
+
app.use(errorHandler(handle.env.appEnv));
|
|
168
|
+
report.throwIfAny("routes");
|
|
169
|
+
const port = handle.env.store.has("PORT")
|
|
170
|
+
? Number(handle.env.get("PORT"))
|
|
171
|
+
: 3000;
|
|
172
|
+
const server = createServer(app);
|
|
173
|
+
handle.server = server;
|
|
174
|
+
// Phase 11w — sockets attach before listen() so no client can connect
|
|
175
|
+
// to a namespace that isn't wired yet.
|
|
176
|
+
const socketReport = new BootReport();
|
|
177
|
+
await initSockets(explored, handle.ctx, server, socketReport);
|
|
178
|
+
socketReport.throwIfAny("sockets");
|
|
179
|
+
await new Promise((resolve, reject) => {
|
|
180
|
+
server.once("error", reject);
|
|
181
|
+
server.listen(port, () => resolve());
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Reverse load order, every hook attempted even if an earlier one throws. */
|
|
185
|
+
async function shutdown(handle, lifecycles) {
|
|
186
|
+
const failures = [];
|
|
187
|
+
for (const { module, onShutdown } of [...lifecycles].reverse()) {
|
|
188
|
+
try {
|
|
189
|
+
await onShutdown?.(handle.ctx);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
failures.push(`${module}: ${error.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Kick socket clients first so the http server's close() isn't held open
|
|
196
|
+
// by long-lived websocket connections.
|
|
197
|
+
handle.ctx.sockets?.close();
|
|
198
|
+
if (handle.server?.listening) {
|
|
199
|
+
await new Promise((resolve, reject) => {
|
|
200
|
+
handle.server.close((error) => (error ? reject(error) : resolve()));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// Queues drain after http stops (no new producers), workers finish their
|
|
204
|
+
// in-flight jobs inside close(). DB goes down last — hooks, requests and
|
|
205
|
+
// jobs may all need it right up to this point.
|
|
206
|
+
await handle.ctx.queues?.close();
|
|
207
|
+
await handle.connection?.close();
|
|
208
|
+
if (failures.length > 0) {
|
|
209
|
+
throw new Error(`onShutdown hook(s) failed — ${failures.join("; ")}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=boot.js.map
|