@hearthlog/tooling 0.0.0-stage → 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.
Files changed (85) hide show
  1. package/LICENSE +235 -0
  2. package/bin/hearthlog.mjs +52 -0
  3. package/package.json +64 -3
  4. package/src/apply.ts +73 -0
  5. package/src/check/check-layers.ts +118 -0
  6. package/src/check/check-modules.ts +198 -0
  7. package/src/check/check-sdk-exports.ts +79 -0
  8. package/src/check/check-tenant-config.ts +208 -0
  9. package/src/cli/hearthlog.ts +212 -0
  10. package/src/cli/list-sites.ts +15 -0
  11. package/src/compose/compose-modules.ts +490 -0
  12. package/src/compose/module-scaffold.ts +194 -0
  13. package/src/deploy/artifact-compare.ts +93 -0
  14. package/src/deploy/artifact.ts +127 -0
  15. package/src/deploy/async-utils.ts +71 -0
  16. package/src/deploy/backup-tenants.ts +139 -0
  17. package/src/deploy/cloudflare-access.ts +789 -0
  18. package/src/deploy/copy-tenant.ts +395 -0
  19. package/src/deploy/dedicated-ledger.ts +77 -0
  20. package/src/deploy/deploy-ledger.ts +162 -0
  21. package/src/deploy/deploy-runner.ts +240 -0
  22. package/src/deploy/deploy-tenant.ts +170 -0
  23. package/src/deploy/deployment-security.ts +141 -0
  24. package/src/deploy/ensure-tenant.ts +988 -0
  25. package/src/deploy/event-policy.ts +23 -0
  26. package/src/deploy/frontend-dry-run.ts +18 -0
  27. package/src/deploy/ledger-credential.ts +48 -0
  28. package/src/deploy/ledger-scripts.ts +13 -0
  29. package/src/deploy/split-tenant.ts +295 -0
  30. package/src/dev/local-development.ts +288 -0
  31. package/src/dev/provision-local.ts +13 -0
  32. package/src/generate/build-generated.ts +166 -0
  33. package/src/generate/generate-brand-icons.ts +128 -0
  34. package/src/generate/generate-entities.ts +627 -0
  35. package/src/generate/generate-tenant.ts +161 -0
  36. package/src/host/core-routes.ts +39 -0
  37. package/src/host/write-host.ts +119 -0
  38. package/src/index.ts +8 -0
  39. package/src/init/check-module.ts +143 -0
  40. package/src/init/export-tenant.ts +124 -0
  41. package/src/init/init.ts +167 -0
  42. package/src/move.ts +189 -0
  43. package/src/plan.ts +160 -0
  44. package/src/public-workers/build-gallery-public.ts +114 -0
  45. package/src/public-workers/calendar-public-config.ts +27 -0
  46. package/src/public-workers/check-calendar-public-route.ts +41 -0
  47. package/src/public-workers/check-gallery-public-route.ts +43 -0
  48. package/src/public-workers/gallery-public-config.ts +22 -0
  49. package/src/receipt.ts +74 -0
  50. package/src/root.ts +16 -0
  51. package/src/security/audit-push.ts +136 -0
  52. package/src/security/forbidden-identities.ts +107 -0
  53. package/src/security/forbidden-init.ts +88 -0
  54. package/src/tenants/host-configs.ts +96 -0
  55. package/src/tenants/ledger-config.ts +57 -0
  56. package/src/tenants/lock-file.ts +45 -0
  57. package/src/tenants/module-lock.ts +58 -0
  58. package/src/tenants/tenant-config.ts +887 -0
  59. package/src/tenants/tenant-copy-plan.ts +119 -0
  60. package/src/tenants/tenant-schema.ts +171 -0
  61. package/src/tenants/tenant-wrangler.ts +37 -0
  62. package/src/tenants/wrangler-base.ts +50 -0
  63. package/src/tenants/wrangler-config.ts +266 -0
  64. package/src/vite/config.ts +310 -0
  65. package/src/vite/dev-plugins.ts +338 -0
  66. package/src/vite/vite-plugin-entity-gen.ts +70 -0
  67. package/src/vite/vite-plugin-tenant-manifest.ts +61 -0
  68. package/src/vite/vite-plugin-worker-assets.ts +42 -0
  69. package/templates/host/app.d.ts.txt +31 -0
  70. package/templates/host/microlighter.d.ts.txt +11 -0
  71. package/templates/module/ci.yml.tmpl +44 -0
  72. package/templates/module/gitignore.tmpl +7 -0
  73. package/templates/module/release.yml.tmpl +53 -0
  74. package/templates/tenant/README.md.tmpl +13 -0
  75. package/templates/tenant/generated-package.json.tmpl +16 -0
  76. package/templates/tenant/github-workflows/deploy.yml.tmpl +110 -0
  77. package/templates/tenant/gitignore.tmpl +11 -0
  78. package/templates/tenant/package.json.tmpl +11 -0
  79. package/templates/tenant/patches/sqlocal@0.18.0.patch +18 -0
  80. package/templates/tenant/pnpm-workspace.yaml.tmpl +8 -0
  81. package/templates/tenant/tenants/__id__/tenant.json.tmpl +8 -0
  82. package/templates/tenant/tenants.json.tmpl +17 -0
  83. package/templates/tenant/tsconfig.json.tmpl +21 -0
  84. package/templates/tenant/vite.config.ts.tmpl +5 -0
  85. package/README.md +0 -3
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The Ledger Worker's config. The Ledger is the same code for every stack; only its Worker name (and, for a site in
3
+ * its own account, the account) differs. The Durable Object classes and their SQLite storage are tied to deployed data
4
+ * and are never renamed.
5
+ */
6
+
7
+ import type { InfrastructureDefinition } from "./tenant-config.js";
8
+ import { WRANGLER_BASE } from "./wrangler-base.js";
9
+
10
+ export const LEDGER_BASE = {
11
+ main: "./index.ts",
12
+ compatibility_flags: ["nodejs_compat"],
13
+ preview_urls: false,
14
+ durable_objects: {
15
+ bindings: [
16
+ { name: "LEDGER_APP", class_name: "LedgerApp" },
17
+ { name: "APP_REGISTRY", class_name: "AppRegistry" },
18
+ ],
19
+ },
20
+ exports: {
21
+ LedgerApp: { type: "durable-object", storage: "sqlite" },
22
+ AppRegistry: { type: "durable-object", storage: "sqlite" },
23
+ },
24
+ vars: {
25
+ TOKEN_SIGNING_KEY_ID: "v1",
26
+ TOKEN_SIGNING_PREVIOUS_KEY_ID: "",
27
+ ALLOWED_ORIGINS: "",
28
+ // "report" logs payloads that "enforce" would reject with 422; flip to "enforce" once the logs show only
29
+ // intended rejections.
30
+ EVENT_VALIDATION: "report",
31
+ },
32
+ observability: {
33
+ enabled: true,
34
+ logs: { enabled: true, head_sampling_rate: 1 },
35
+ traces: { enabled: true, head_sampling_rate: 0.05 },
36
+ },
37
+ } as const;
38
+
39
+ /** The Ledger Worker of a stack: its name from the infrastructure, and the account when the site pins one. */
40
+ export function buildLedgerConfig(
41
+ infrastructure: Pick<InfrastructureDefinition, "ledgerWorker">,
42
+ accountId?: string,
43
+ ): Record<string, unknown> {
44
+ return {
45
+ name: infrastructure.ledgerWorker,
46
+ main: LEDGER_BASE.main,
47
+ compatibility_date: WRANGLER_BASE.compatibility_date,
48
+ compatibility_flags: LEDGER_BASE.compatibility_flags,
49
+ preview_urls: LEDGER_BASE.preview_urls,
50
+ durable_objects: LEDGER_BASE.durable_objects,
51
+ exports: LEDGER_BASE.exports,
52
+ vars: LEDGER_BASE.vars,
53
+ observability: LEDGER_BASE.observability,
54
+ // Pinning the account makes Wrangler refuse to deploy into any other one.
55
+ ...(accountId ? { account_id: accountId } : {}),
56
+ };
57
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The lock file's content: which modules a site ships and the exact manifest each was composed from. It
3
+ * imports nothing generated, so `hearthlog init` can write one before a site has any generated files.
4
+ */
5
+
6
+ import { createHash } from "node:crypto";
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ import { MODULE_API_VERSION } from "@hearthlog/contracts";
11
+
12
+ import { modulesForFeatures, type LoadedModule } from "../compose/compose-modules.js";
13
+
14
+ export interface ModuleLockEntry {
15
+ id: string;
16
+ package: string;
17
+ version: string;
18
+ /** The site's enabled features this module provides. */
19
+ features: string[];
20
+ /** SHA-256 of the module's `hearthlog.module.json`. */
21
+ manifestSha256: string;
22
+ }
23
+
24
+ export interface ModuleLock {
25
+ schemaVersion: 1;
26
+ platform: { moduleApi: number };
27
+ modules: ModuleLockEntry[];
28
+ }
29
+
30
+ /** The lock for the modules that provide a site's features. */
31
+ export function lockFor(modules: readonly LoadedModule[], features: readonly string[]): ModuleLock {
32
+ const entries: ModuleLockEntry[] = modulesForFeatures(modules, features).map(({ directory, manifest }) => ({
33
+ id: manifest.id,
34
+ package: manifest.package,
35
+ version: manifest.version,
36
+ features: manifest.features.map((feature) => feature.id).filter((id) => features.includes(id)),
37
+ manifestSha256: createHash("sha256").update(readFileSync(join(directory, "hearthlog.module.json"))).digest("hex"),
38
+ }));
39
+ entries.sort((a, b) => a.id.localeCompare(b.id));
40
+ return { schemaVersion: 1, platform: { moduleApi: MODULE_API_VERSION }, modules: entries };
41
+ }
42
+
43
+ export function renderModuleLock(lock: ModuleLock): string {
44
+ return `${JSON.stringify(lock, null, 2)}\n`;
45
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * `tenants/<site>/modules.lock.json`: the modules a site ships, with the
4
+ * exact manifest each was composed from. The composer builds from the
5
+ * workspace, so the lock does not choose versions; it records them, and
6
+ * `pnpm check:tenants` fails when a site's modules or a manifest changed
7
+ * without the lock being regenerated. That makes a release reviewable: the
8
+ * lock diff names every module whose contract moved.
9
+ *
10
+ * Usage: pnpm lock:modules [--check]
11
+ */
12
+
13
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ import { loadModules } from "../compose/compose-modules.js";
18
+ import { lockFor, renderModuleLock, type ModuleLock } from "./lock-file.js";
19
+
20
+ import { loadTenantConfig, TENANTS_DIRECTORY, type ResolvedSite } from "./tenant-config.js";
21
+
22
+ export { lockFor, renderModuleLock, type ModuleLock, type ModuleLockEntry } from "./lock-file.js";
23
+
24
+ export function moduleLockPath(siteId: string): string {
25
+ return join(TENANTS_DIRECTORY, siteId, "modules.lock.json");
26
+ }
27
+
28
+ export async function buildModuleLock(site: Pick<ResolvedSite, "features">): Promise<ModuleLock> {
29
+ return lockFor(loadModules(), site.features);
30
+ }
31
+
32
+ /** What differs between a site's lock file and the modules it would ship today. */
33
+ export async function moduleLockProblems(site: Pick<ResolvedSite, "id" | "features">): Promise<string[]> {
34
+ const expected = renderModuleLock(await buildModuleLock(site));
35
+ const current = await readFile(moduleLockPath(site.id), "utf8").catch(() => null);
36
+ if (current === null) return [`tenants/${site.id}/modules.lock.json is missing; run pnpm lock:modules`];
37
+ return current === expected ? [] : [`tenants/${site.id}/modules.lock.json is stale; run pnpm lock:modules`];
38
+ }
39
+
40
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
41
+ const { sites } = await loadTenantConfig();
42
+ const check = process.argv.includes("--check");
43
+ const problems: string[] = [];
44
+ for (const site of sites) {
45
+ if (check) {
46
+ problems.push(...(await moduleLockProblems(site)));
47
+ continue;
48
+ }
49
+ const path = moduleLockPath(site.id);
50
+ await mkdir(dirname(path), { recursive: true });
51
+ await writeFile(path, renderModuleLock(await buildModuleLock(site)));
52
+ }
53
+ if (problems.length) {
54
+ console.error(problems.join("\n"));
55
+ process.exit(1);
56
+ }
57
+ console.log(check ? "Module locks are current." : `Wrote ${sites.length} module lock(s).`);
58
+ }