@cosmicdrift/kumiko-dev-server 0.13.0 → 0.14.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 +24 -0
- package/package.json +4 -3
- package/src/__tests__/walkthrough.integration.ts +118 -0
- package/src/scaffold-app.ts +139 -76
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-dev-server
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b8e1d48: scaffoldApp baut `src/run-config.ts` + `bin/main.ts` jetzt via ts-morph
|
|
8
|
+
(AST) statt template-strings. Selbes Tool wie scaffoldAppFeature →
|
|
9
|
+
ein konsistenter Mechanismus für generate + later modify. Plus:
|
|
10
|
+
ts-morph als explicit dependency aufgenommen (war bisher nur via
|
|
11
|
+
hoisted root-dep verfügbar; broken bei publish).
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- ce23d48: `walkthrough.integration.ts` — DX-3.1 walkthrough-snapshot-test. Pins
|
|
16
|
+
scaffoldApp + scaffoldAppFeature output gegen die Behauptungen in
|
|
17
|
+
docs.kumiko.so/en/walkthrough/. Catches doc-drift ohne actual
|
|
18
|
+
`bunx … && yarn install && bun run boot` CI-run.
|
|
19
|
+
|
|
20
|
+
5 Tests: file-list, auto-mount-diff, run-config text-content,
|
|
21
|
+
composeFeatures(includeBundled:true) = 7 features, bin/main auth.admin
|
|
22
|
+
stub.
|
|
23
|
+
|
|
24
|
+
- @cosmicdrift/kumiko-framework@0.14.0
|
|
25
|
+
- @cosmicdrift/kumiko-bundled-features@0.14.0
|
|
26
|
+
|
|
3
27
|
## 0.13.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -49,8 +49,9 @@
|
|
|
49
49
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
53
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
52
|
+
"@cosmicdrift/kumiko-bundled-features": "0.14.0",
|
|
53
|
+
"@cosmicdrift/kumiko-framework": "0.14.0",
|
|
54
|
+
"ts-morph": "^28.0.0"
|
|
54
55
|
},
|
|
55
56
|
"publishConfig": {
|
|
56
57
|
"registry": "https://registry.npmjs.org",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// DX-3.1 — walkthrough-snapshot-test. Reproduces the 3-command path from
|
|
2
|
+
// docs.kumiko.so/en/walkthrough/ in-process and asserts what the walkthrough
|
|
3
|
+
// claims. Catches drift in scaffoldApp + scaffoldAppFeature against the
|
|
4
|
+
// docs without an actual `bunx … && yarn install && bun run boot` CI run.
|
|
5
|
+
//
|
|
6
|
+
// What this test pins:
|
|
7
|
+
// - scaffoldApp produces the 6 files the walkthrough lists
|
|
8
|
+
// - scaffoldAppFeature scaffolds + auto-mounts (the diff-block shown)
|
|
9
|
+
// - composeFeatures(includeBundled:true) yields the exact feature-count
|
|
10
|
+
// the walkthrough advertises in "Expected output"
|
|
11
|
+
|
|
12
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
16
|
+
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
17
|
+
import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
19
|
+
import { composeFeatures } from "../compose-features";
|
|
20
|
+
import { scaffoldApp } from "../scaffold-app";
|
|
21
|
+
import { scaffoldAppFeature } from "../scaffold-app-feature";
|
|
22
|
+
|
|
23
|
+
describe("walkthrough — DX-3.1 snapshot", () => {
|
|
24
|
+
let tmp: string;
|
|
25
|
+
let appRoot: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmp = mkdtempSync(join(tmpdir(), "walkthrough-"));
|
|
29
|
+
appRoot = join(tmp, "my-notes");
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Step 1 (kumiko new app) — produces walkthrough's 6 files", () => {
|
|
36
|
+
const result = scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
37
|
+
expect(result.files).toEqual([
|
|
38
|
+
"package.json",
|
|
39
|
+
"tsconfig.json",
|
|
40
|
+
"src/run-config.ts",
|
|
41
|
+
"bin/main.ts",
|
|
42
|
+
".env.example",
|
|
43
|
+
"README.md",
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("Step 2 (kumiko add feature) — auto-mounts + walkthrough diff matches", () => {
|
|
48
|
+
scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
49
|
+
const result = scaffoldAppFeature({ name: "notes", appRoot });
|
|
50
|
+
expect(result.autoMounted).toBe(true);
|
|
51
|
+
|
|
52
|
+
const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
|
|
53
|
+
// Walkthrough's diff-block claims:
|
|
54
|
+
// + import { notesFeature } from "./features/notes";
|
|
55
|
+
// + notesFeature (in APP_FEATURES)
|
|
56
|
+
expect(runConfig).toContain(`import { notesFeature } from "./features/notes";`);
|
|
57
|
+
expect(runConfig).toContain("notesFeature");
|
|
58
|
+
// Foundation still mounted (createSecretsFeature + createSessionsFeature).
|
|
59
|
+
expect(runConfig).toContain("createSecretsFeature()");
|
|
60
|
+
expect(runConfig).toContain("createSessionsFeature()");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("Step 3 (boot validation) — scaffolded run-config matches walkthrough's APP_FEATURES claim", () => {
|
|
64
|
+
scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
65
|
+
scaffoldAppFeature({ name: "notes", appRoot });
|
|
66
|
+
|
|
67
|
+
// Text-assert: scaffolded run-config.ts contains exactly the 3 features
|
|
68
|
+
// the walkthrough's diff-block shows (secrets + sessions + notesFeature).
|
|
69
|
+
// Dynamic-import would fail because /tmp can't resolve @cosmicdrift/*
|
|
70
|
+
// workspace symlinks — instead we reproduce the equivalent APP_FEATURES
|
|
71
|
+
// array in-process below.
|
|
72
|
+
const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
|
|
73
|
+
expect(runConfig).toContain("createSecretsFeature()");
|
|
74
|
+
expect(runConfig).toContain("createSessionsFeature()");
|
|
75
|
+
expect(runConfig).toContain("notesFeature");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("Step 3 (composeFeatures) — 3 explicit + 4 auto-mounted = 7 features", () => {
|
|
79
|
+
// Reproduces the scaffolded APP_FEATURES in-process. notesFeature gets
|
|
80
|
+
// a dummy defineFeature here — the scaffold-side of "notesFeature"
|
|
81
|
+
// (file-content) is pinned in test 2; this test pins the runtime-side
|
|
82
|
+
// (composeFeatures auto-prepend behaviour the walkthrough claims).
|
|
83
|
+
const notesFeature = defineFeature("notes", () => {});
|
|
84
|
+
const APP_FEATURES = [createSecretsFeature(), createSessionsFeature(), notesFeature];
|
|
85
|
+
|
|
86
|
+
const composed = composeFeatures(APP_FEATURES, { includeBundled: true });
|
|
87
|
+
// 3 explicit + 4 auto-mounted bundled = 7 total features.
|
|
88
|
+
expect(composed.length).toBe(7);
|
|
89
|
+
|
|
90
|
+
const composedNames = composed.map((f) => f.name).sort();
|
|
91
|
+
expect(composedNames).toEqual([
|
|
92
|
+
"auth-email-password",
|
|
93
|
+
"config",
|
|
94
|
+
"notes",
|
|
95
|
+
"secrets",
|
|
96
|
+
"sessions",
|
|
97
|
+
"tenant",
|
|
98
|
+
"user",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// validateBoot must pass (no missing-requires, no schema-errors).
|
|
102
|
+
expect(() => validateBoot(composed)).not.toThrow();
|
|
103
|
+
// Registry must contain all 7 features.
|
|
104
|
+
const registry = createRegistry(composed);
|
|
105
|
+
expect(registry.features.size).toBe(7);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("bin/main.ts contains the auth.admin stub the walkthrough relies on", () => {
|
|
109
|
+
scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
110
|
+
const main = readFileSync(join(appRoot, "bin/main.ts"), "utf-8");
|
|
111
|
+
// composeFeatures(includeBundled:true)-trigger is `auth: { admin: { … } }`.
|
|
112
|
+
// Walkthrough explicitly says this is what auto-mounts the 4 bundled features.
|
|
113
|
+
expect(main).toContain("auth: {");
|
|
114
|
+
expect(main).toContain("admin: {");
|
|
115
|
+
expect(main).toContain("memberships:");
|
|
116
|
+
expect(main).toContain("runProdApp");
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/scaffold-app.ts
CHANGED
|
@@ -6,16 +6,14 @@
|
|
|
6
6
|
// stub, package.json with @cosmicdrift/* deps, tsconfig, .env.example,
|
|
7
7
|
// README.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// The generated app is born "boots cleanly, mounts nothing fancy". User
|
|
15
|
-
// runs `kumiko add feature` (DX-2) or hand-edits src/run-config.ts to grow.
|
|
9
|
+
// .ts files are built via ts-morph (same tool [[scaffoldAppFeature]] uses
|
|
10
|
+
// to auto-mount features). Means a single AST representation for both
|
|
11
|
+
// generate + later modify — no template-string ↔ ts-morph divergence.
|
|
12
|
+
// Static files (package.json, tsconfig, .env, README) stay text-based.
|
|
16
13
|
|
|
17
14
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
18
15
|
import { join, resolve } from "node:path";
|
|
16
|
+
import { IndentationText, Project, VariableDeclarationKind } from "ts-morph";
|
|
19
17
|
|
|
20
18
|
export type ScaffoldAppOptions = {
|
|
21
19
|
/** kebab-case app name (e.g. "my-shop"). Becomes package-name + folder. */
|
|
@@ -122,72 +120,143 @@ function renderTsconfig(): string {
|
|
|
122
120
|
)}\n`;
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
function
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// - bunx kumiko add feature <name> (DX-2, automatisch)
|
|
133
|
-
// - oder: hand-edit + import unten ergänzen
|
|
134
|
-
|
|
135
|
-
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
136
|
-
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
123
|
+
function newTsProject(): Project {
|
|
124
|
+
return new Project({
|
|
125
|
+
useInMemoryFileSystem: true,
|
|
126
|
+
compilerOptions: { target: 99, module: 99, strict: true },
|
|
127
|
+
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
137
130
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
function renderRunConfig(): string {
|
|
132
|
+
const project = newTsProject();
|
|
133
|
+
const sf = project.createSourceFile("run-config.ts", "");
|
|
134
|
+
|
|
135
|
+
sf.addImportDeclaration({
|
|
136
|
+
moduleSpecifier: "@cosmicdrift/kumiko-bundled-features/secrets",
|
|
137
|
+
namedImports: ["createSecretsFeature"],
|
|
138
|
+
});
|
|
139
|
+
sf.addImportDeclaration({
|
|
140
|
+
moduleSpecifier: "@cosmicdrift/kumiko-bundled-features/sessions",
|
|
141
|
+
namedImports: ["createSessionsFeature"],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
sf.addVariableStatement({
|
|
145
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
146
|
+
isExported: true,
|
|
147
|
+
declarations: [
|
|
148
|
+
{
|
|
149
|
+
name: "APP_FEATURES",
|
|
150
|
+
initializer: "[createSecretsFeature(), createSessionsFeature()] as const",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
sf.insertText(
|
|
156
|
+
0,
|
|
157
|
+
[
|
|
158
|
+
"// Single source of truth für die Feature-Komposition deiner App.",
|
|
159
|
+
"// Bundled-Foundation: secrets + sessions. config/user/tenant/auth-email-password",
|
|
160
|
+
"// werden via composeFeatures(includeBundled:true) automatisch ergänzt",
|
|
161
|
+
"// wenn runProdApp mit `auth: {…}` aufgerufen wird (siehe bin/main.ts).",
|
|
162
|
+
"//",
|
|
163
|
+
"// Neue features hinzufügen:",
|
|
164
|
+
"// - bunx @cosmicdrift/kumiko-cli add feature <name> (DX-2, automatisch)",
|
|
165
|
+
"// - oder: hand-edit + import unten ergänzen",
|
|
166
|
+
"",
|
|
167
|
+
"",
|
|
168
|
+
].join("\n"),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return sf.getFullText();
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
function renderMain(appName: string): string {
|
|
146
|
-
// Deterministic tenant-UUID derived from appName for the seed-admin
|
|
147
|
-
// membership. Reproducible across boots; tenants table sees the same
|
|
148
|
-
// ID. Format: 8-4-4-4-12 hex chars, version-4 marker at position 14.
|
|
149
|
-
// We hash the name into the digits using a tiny PRNG so two apps
|
|
150
|
-
// get different IDs without bun's crypto dependency.
|
|
151
175
|
const tenantId = deriveTenantId(appName);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
176
|
+
const project = newTsProject();
|
|
177
|
+
const sf = project.createSourceFile("main.ts", "");
|
|
178
|
+
|
|
179
|
+
sf.addImportDeclaration({
|
|
180
|
+
moduleSpecifier: "@cosmicdrift/kumiko-dev-server",
|
|
181
|
+
namedImports: ["frameworkCoreEnvSchema", "runProdApp"],
|
|
182
|
+
});
|
|
183
|
+
sf.addImportDeclaration({
|
|
184
|
+
moduleSpecifier: "@cosmicdrift/kumiko-framework/engine",
|
|
185
|
+
isTypeOnly: true,
|
|
186
|
+
namedImports: ["TenantId"],
|
|
187
|
+
});
|
|
188
|
+
sf.addImportDeclaration({
|
|
189
|
+
moduleSpecifier: "@cosmicdrift/kumiko-framework/env",
|
|
190
|
+
namedImports: ["composeEnvSchema"],
|
|
191
|
+
});
|
|
192
|
+
sf.addImportDeclaration({
|
|
193
|
+
moduleSpecifier: "../src/run-config",
|
|
194
|
+
namedImports: ["APP_FEATURES"],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
sf.addVariableStatement({
|
|
198
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
199
|
+
declarations: [
|
|
200
|
+
{
|
|
201
|
+
name: "DEFAULT_TENANT_ID",
|
|
202
|
+
initializer: `"${tenantId}" as TenantId`,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
sf.addVariableStatement({
|
|
208
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
209
|
+
declarations: [
|
|
210
|
+
{
|
|
211
|
+
name: "envSchema",
|
|
212
|
+
initializer: "composeEnvSchema({ core: frameworkCoreEnvSchema, features: APP_FEATURES })",
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
sf.addStatements((writer) => {
|
|
218
|
+
writer
|
|
219
|
+
.write("await runProdApp(")
|
|
220
|
+
.inlineBlock(() => {
|
|
221
|
+
writer.writeLine("features: APP_FEATURES,");
|
|
222
|
+
writer.writeLine("envSchema,");
|
|
223
|
+
writer.writeLine("migrations: false,");
|
|
224
|
+
writer.write("auth: ").inlineBlock(() => {
|
|
225
|
+
writer.write("admin: ").inlineBlock(() => {
|
|
226
|
+
writer.writeLine(`email: "admin@${appName}.local",`);
|
|
227
|
+
writer.writeLine(`password: "change-me-on-first-deploy",`);
|
|
228
|
+
writer.writeLine(`displayName: "Admin",`);
|
|
229
|
+
writer.write("memberships: [");
|
|
230
|
+
writer.indent(() => {
|
|
231
|
+
writer.inlineBlock(() => {
|
|
232
|
+
writer.writeLine("tenantId: DEFAULT_TENANT_ID,");
|
|
233
|
+
writer.writeLine(`tenantKey: "${appName}",`);
|
|
234
|
+
writer.writeLine(`tenantName: "${appName}",`);
|
|
235
|
+
writer.writeLine(`roles: ["TenantAdmin"],`);
|
|
236
|
+
});
|
|
237
|
+
writer.write(",");
|
|
238
|
+
});
|
|
239
|
+
writer.write("],");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
})
|
|
243
|
+
.write(");");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
sf.insertText(
|
|
247
|
+
0,
|
|
248
|
+
[
|
|
249
|
+
"// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after",
|
|
250
|
+
"// composeFeatures + validateBoot + createRegistry without DB/Redis-connect",
|
|
251
|
+
"// (siehe @cosmicdrift/kumiko-dev-server runProdApp). Echter Dev-Boot",
|
|
252
|
+
"// passiert via `yarn kumiko dev` (in-repo dev-tool) mit Docker-stack — DX-1.0 deckt nur",
|
|
253
|
+
"// den boot-mode-Pfad ab; `kumiko dev` kommt in einer späteren DX-Phase.",
|
|
254
|
+
"",
|
|
255
|
+
"",
|
|
256
|
+
].join("\n"),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return sf.getFullText();
|
|
191
260
|
}
|
|
192
261
|
|
|
193
262
|
function renderEnvExample(): string {
|
|
@@ -208,7 +277,7 @@ function renderReadme(appName: string): string {
|
|
|
208
277
|
return `# ${appName}
|
|
209
278
|
|
|
210
279
|
Scaffolded by \`kumiko new app\`. Boots out-of-the-box with secrets + sessions
|
|
211
|
-
mounted (foundation set). Add features with \`bunx kumiko add feature <name>\`.
|
|
280
|
+
mounted (foundation set). Add features with \`bunx @cosmicdrift/kumiko-cli add feature <name>\`.
|
|
212
281
|
|
|
213
282
|
## First boot
|
|
214
283
|
|
|
@@ -224,7 +293,7 @@ Expected: \`[runProdApp] boot validation OK (… features, … registry entries)
|
|
|
224
293
|
## Adding features
|
|
225
294
|
|
|
226
295
|
\`\`\`sh
|
|
227
|
-
bunx kumiko add feature my-domain
|
|
296
|
+
bunx @cosmicdrift/kumiko-cli add feature my-domain
|
|
228
297
|
# → editiert src/run-config.ts automatisch + scaffolded src/features/my-domain/
|
|
229
298
|
\`\`\`
|
|
230
299
|
|
|
@@ -241,10 +310,6 @@ For full docs see https://docs.kumiko.so.
|
|
|
241
310
|
// version-marker at the right spot. NOT cryptographically random —
|
|
242
311
|
// just a stable per-app default the user can change later.
|
|
243
312
|
function deriveTenantId(name: string): string {
|
|
244
|
-
// Tiny xorshift PRNG seeded from the name's char-codes. Same name →
|
|
245
|
-
// same ID. Sufficient for "give every scaffolded app a deterministic
|
|
246
|
-
// default tenant" — production sets its own via the create-tenant
|
|
247
|
-
// flow anyway.
|
|
248
313
|
let state = 2166136261;
|
|
249
314
|
for (const ch of name) {
|
|
250
315
|
state ^= ch.charCodeAt(0);
|
|
@@ -255,11 +320,9 @@ function deriveTenantId(name: string): string {
|
|
|
255
320
|
state ^= state << 13;
|
|
256
321
|
state >>>= 0;
|
|
257
322
|
const b = hex(state, 4);
|
|
258
|
-
// version-4 marker at first char of 3rd group:
|
|
259
323
|
state ^= state >>> 17;
|
|
260
324
|
state >>>= 0;
|
|
261
325
|
const c = `4${hex(state, 3)}`;
|
|
262
|
-
// RFC 4122 variant: 10xx (set top two bits of 4th group to 10):
|
|
263
326
|
state ^= state << 5;
|
|
264
327
|
state >>>= 0;
|
|
265
328
|
const d4 = (0x8 | (state & 0x3)).toString(16);
|