@tokenoftrust/storefront-runner 2.0.8 → 2.0.10

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 (97) hide show
  1. package/apps/storefront/integrations/materialize-guard.mjs +8 -2
  2. package/apps/storefront/src/lib/auth/brokerAssertion.ts +31 -5
  3. package/apps/storefront/src/lib/shared/principalGrants.ts +215 -94
  4. package/apps/storefront/src/lib/tot/syncState.ts +109 -0
  5. package/package.json +1 -1
  6. package/packages/public-runtime/src/customization-reconcile.ts +15 -1
  7. package/packages/public-runtime/src/evidence.ts +14 -11
  8. package/pnpm-workspace.yaml +0 -3
  9. package/scripts/tenant/materialize-tenant-content.mjs +107 -7
  10. package/apps/storefront/.astro/content.d.ts +0 -159
  11. package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +0 -1
  12. package/apps/storefront/.astro/types.d.ts +0 -3
  13. package/apps/storefront/node_modules/.bin/acorn +0 -43
  14. package/apps/storefront/node_modules/.bin/astro +0 -43
  15. package/apps/storefront/node_modules/.bin/astro-check +0 -43
  16. package/apps/storefront/node_modules/.bin/browserslist +0 -43
  17. package/apps/storefront/node_modules/.bin/cf-wrangler +0 -43
  18. package/apps/storefront/node_modules/.bin/depcruise +0 -43
  19. package/apps/storefront/node_modules/.bin/depcruise-baseline +0 -43
  20. package/apps/storefront/node_modules/.bin/depcruise-fmt +0 -43
  21. package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +0 -43
  22. package/apps/storefront/node_modules/.bin/dependency-cruise +0 -43
  23. package/apps/storefront/node_modules/.bin/dependency-cruiser +0 -43
  24. package/apps/storefront/node_modules/.bin/drizzle-kit +0 -43
  25. package/apps/storefront/node_modules/.bin/esbuild +0 -43
  26. package/apps/storefront/node_modules/.bin/jiti +0 -43
  27. package/apps/storefront/node_modules/.bin/playwright +0 -43
  28. package/apps/storefront/node_modules/.bin/playwright-core +0 -43
  29. package/apps/storefront/node_modules/.bin/prettier +0 -43
  30. package/apps/storefront/node_modules/.bin/rollup +0 -43
  31. package/apps/storefront/node_modules/.bin/tsc +0 -43
  32. package/apps/storefront/node_modules/.bin/tsserver +0 -43
  33. package/apps/storefront/node_modules/.bin/tsx +0 -43
  34. package/apps/storefront/node_modules/.bin/vite +0 -43
  35. package/apps/storefront/node_modules/.bin/vitest +0 -43
  36. package/apps/storefront/node_modules/.bin/workerd +0 -43
  37. package/apps/storefront/node_modules/.bin/wrangler +0 -43
  38. package/apps/storefront/node_modules/.bin/wrangler2 +0 -43
  39. package/apps/storefront/node_modules/.bin/yaml +0 -43
  40. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +0 -58
  41. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +0 -1
  42. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +0 -741
  43. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +0 -1
  44. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +0 -2549
  45. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +0 -1
  46. package/apps/storefront/node_modules/.vite/deps/_metadata.json +0 -150
  47. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +0 -434
  48. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +0 -1
  49. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +0 -6390
  50. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -1
  51. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +0 -2705
  52. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -1
  53. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +0 -65
  54. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -1
  55. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -455
  56. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -1
  57. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +0 -1535
  58. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +0 -1
  59. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +0 -48
  60. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +0 -1
  61. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +0 -42
  62. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +0 -1
  63. package/apps/storefront/node_modules/.vite/deps/package.json +0 -3
  64. package/apps/storefront/node_modules/.vite/deps/preact.js +0 -310
  65. package/apps/storefront/node_modules/.vite/deps/preact.js.map +0 -1
  66. package/apps/storefront/node_modules/.vite/deps/preact_compat.js +0 -420
  67. package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +0 -1
  68. package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +0 -4
  69. package/apps/storefront/node_modules/.vite/deps/preact_debug.js +0 -236
  70. package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +0 -1
  71. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +0 -12
  72. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +0 -1
  73. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +0 -192
  74. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +0 -1
  75. package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +0 -3
  76. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +0 -92
  77. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +0 -1
  78. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +0 -59
  79. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +0 -1
  80. package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +0 -24
  81. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +0 -189
  82. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +0 -1
  83. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +0 -13
  84. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +0 -1
  85. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +0 -458
  86. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +0 -1
  87. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +0 -1016
  88. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +0 -1
  89. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +0 -32
  90. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +0 -1
  91. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +0 -113
  92. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +0 -1
  93. package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +0 -8
  94. package/apps/storefront/node_modules/.vite/deps_ssr/package.json +0 -3
  95. package/packages/public-runtime/node_modules/.bin/tsc +0 -43
  96. package/packages/public-runtime/node_modules/.bin/tsserver +0 -43
  97. package/packages/public-runtime/node_modules/.bin/vitest +0 -43
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Worker-side reader for the durable per-tenant catalog sync state (hsby-B1).
3
+ * The migration CLI writes one JSON document per tenant scope at
4
+ * out/catalog/<scope>/sync-state.json (packages/migration/src/load/sync-state.ts,
5
+ * `SyncStateStore`/`nextSyncState`) — this module is the Worker's read side of
6
+ * that same file, via the same Vite glob mechanism `ToTClient.ts` uses for
7
+ * products.json/collections.json/reviews.json. OPTIONAL: a tenant with no
8
+ * recorded sync (or one migrated before hsby-B1 landed) just gets `null`.
9
+ *
10
+ * This is intentionally a thin, standalone read — not wired into `ToTClient`,
11
+ * since not every caller needs sync provenance. hsby-B3's readiness projection
12
+ * is the expected first consumer.
13
+ */
14
+
15
+ /** Mirrors packages/migration/src/load/sync-state.ts's `SourcePlatform`. */
16
+ export type SyncStateSourcePlatform =
17
+ | "shopify-admin"
18
+ | "shopify-public"
19
+ | "fixtures"
20
+ | "existing";
21
+
22
+ /** Mirrors packages/migration/src/load/sync-state.ts's `FidelityLevel`. */
23
+ export type SyncStateFidelityLevel = "full" | "reduced" | "unknown";
24
+
25
+ export interface SyncStateFidelity {
26
+ level: SyncStateFidelityLevel;
27
+ note: string;
28
+ missing: string[];
29
+ }
30
+
31
+ export interface SyncStateSourceMode {
32
+ platform: SyncStateSourcePlatform;
33
+ mode: "full" | "incremental";
34
+ fullSweepOnly: boolean;
35
+ }
36
+
37
+ export interface SyncStateEntity {
38
+ checksums: Record<string, string>;
39
+ lastUpdatedAt: string | null;
40
+ }
41
+
42
+ /** Mirrors packages/migration/src/load/sync-state.ts's `EntityChangeSet`. */
43
+ export interface SyncStateEntityChangeSet {
44
+ added: string[];
45
+ updated: string[];
46
+ removed: string[];
47
+ unchanged: number;
48
+ }
49
+
50
+ /** Mirrors packages/migration/src/load/sync-state.ts's `SweepChanges`. */
51
+ export interface SyncStateSweepChanges {
52
+ computedAt: string;
53
+ mode: "full" | "incremental";
54
+ removalsAssessed: boolean;
55
+ fullSweepOnly: boolean;
56
+ products: SyncStateEntityChangeSet;
57
+ collections: SyncStateEntityChangeSet;
58
+ }
59
+
60
+ /** The on-disk shape written by the migration CLI (schema "tot.sync-state/1"). */
61
+ export interface CatalogSyncState {
62
+ schema: string;
63
+ tenantId: string;
64
+ scope: string;
65
+ source: SyncStateSourceMode;
66
+ fidelity: SyncStateFidelity;
67
+ lastSyncAt: string | null;
68
+ products: SyncStateEntity;
69
+ collections: SyncStateEntity;
70
+ /**
71
+ * The content-hash delta of the most recent sweep. Optional: a catalog
72
+ * migrated before hsby-B2 (change detection) has no `lastChanges`.
73
+ */
74
+ lastChanges?: SyncStateSweepChanges;
75
+ }
76
+
77
+ // Vite resolves this glob at build time and bundles every tenant's sync-state
78
+ // document, so the Worker has no runtime filesystem dependency — same pattern
79
+ // as productGlobs/collectionGlobs/reviewGlobs in ToTClient.ts.
80
+ const syncStateGlobs = import.meta.glob<CatalogSyncState>(
81
+ "../../../../../packages/migration/out/catalog/*/sync-state.json",
82
+ { import: "default" },
83
+ );
84
+
85
+ const syncStateCacheByScope = new Map<string, CatalogSyncState | null>();
86
+
87
+ /** Pick the glob entry for a tenant scope (matches .../catalog/<scope>/file). */
88
+ function pickByScope(scope: string): (() => Promise<CatalogSyncState>) | null {
89
+ const suffix = `/catalog/${scope}/`;
90
+ const key = Object.keys(syncStateGlobs).find((k) => k.includes(suffix));
91
+ return (key ? syncStateGlobs[key] : null) ?? null;
92
+ }
93
+
94
+ /**
95
+ * The tenant's durable sync state, or null when none was ever recorded (no
96
+ * migration run yet, or a pre-hsby-B1 catalog). Never throws.
97
+ */
98
+ export async function getSyncState(scope: string): Promise<CatalogSyncState | null> {
99
+ if (syncStateCacheByScope.has(scope)) return syncStateCacheByScope.get(scope)!;
100
+ const loader = pickByScope(scope);
101
+ const state = loader ? await loader() : null;
102
+ syncStateCacheByScope.set(scope, state);
103
+ return state;
104
+ }
105
+
106
+ /** Test seam: drop the per-isolate cache. */
107
+ export function clearSyncStateCache(): void {
108
+ syncStateCacheByScope.clear();
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -90,7 +90,21 @@ export interface PreviewState {
90
90
  * post-hoc merge sets it, after `reportPreviewEvidence` computes it. Absent and
91
91
  * `null` both mean "no verdict yet"; callers should treat them the same.
92
92
  */
93
- evidence?: { checks: PreviewStateEvidenceCheck[]; promotable: boolean; generatedAt: string } | null;
93
+ evidence?: {
94
+ checks: PreviewStateEvidenceCheck[];
95
+ promotable: boolean;
96
+ generatedAt: string;
97
+ /**
98
+ * Set when the MCP `change_report_evidence` push for this verdict failed —
99
+ * `promotable` above is already forced `false` in that case (fail-closed: MCP
100
+ * has no evidence for this commit, so the local record must not claim
101
+ * shippability the other source of truth doesn't share). Mirrors
102
+ * `ReviewEnvironmentEvidence.pushFailed` (candidate-index.ts).
103
+ */
104
+ pushFailed?: boolean;
105
+ /** The push failure's error message, when `pushFailed` is true. */
106
+ pushError?: string;
107
+ } | null;
94
108
  }
95
109
 
96
110
  export function previewStateKey(tenantId: string): string {
@@ -9,16 +9,16 @@
9
9
  * See `epics/dev-loop.md` (D4 + the MCP-foundations block).
10
10
  */
11
11
 
12
- // NOTE (2026-08): a v2 `assets_materialized` check was briefly added here and REVERTED —
13
- // the deployed tot-mcp evidence validator pins `contract_version:1` + a 7-id enum and
14
- // REJECTED every v2 report (".contract_version should be equal to constant; .checks[7].id
15
- // should be equal to one of the allowed values"), silently dropping the evidence push so
16
- // `preview_status` hung `pending` on every reconcile. The reconcile STILL fetch-verifies
17
- // the served asset subtree (verify-assets.ts) and surfaces the result in the reconcile
18
- // trace ("assets-verified" marker) — that regression guard is intact. Re-add the
19
- // `assets_materialized` check id here + bump to 2 ONLY in lockstep with tot-mcp accepting
20
- // contract_version 2 (and mirroring the evidence-url CRC). Until then this stays v1.
21
- export const EVIDENCE_CONTRACT_VERSION = 1;
12
+ // v2 (2026-08): added the `assets_materialized` check — reconcile now fetch-verifies
13
+ // that a version's public/app asset subtree actually SERVES (the pagination-truncation
14
+ // class of silent drop, fb-1787794734109-hjru1k, reported success + promotable while
15
+ // the newest asset subtree 404'd). GRANDFATHER: a consumer must treat an
16
+ // already-passed v1 report as still valid — the new check is version-gated, so a v1
17
+ // report (which cannot carry it) is never retroactively failed for its absence. The
18
+ // promote-gate addition (`assets_materialized` → REQUIRED_FOR_PROMOTE) is
19
+ // version-gated to v2 on the CONSUMER (tot-mcp); this producer emits the check but
20
+ // leaves REQUIRED_FOR_PROMOTE unchanged so a v1-shaped promote rule still holds.
21
+ export const EVIDENCE_CONTRACT_VERSION = 2;
22
22
 
23
23
  /** The per-check verdicts. `skip` = not applicable / not yet measured. */
24
24
  export const EVIDENCE_STATUSES = ["pass", "warn", "fail", "skip"] as const;
@@ -37,7 +37,10 @@ export const EVIDENCE_CHECK_IDS = [
37
37
  "json_ld",
38
38
  "sitemap",
39
39
  "cutover_crawl",
40
- // (v2 `assets_materialized` intentionally omitted — see EVIDENCE_CONTRACT_VERSION note.)
40
+ // v2: did this version's public/app asset subtree actually materialize + serve
41
+ // (fetch-verified against R2 at the published versionId). Fail-closed — a silent
42
+ // drop (pagination truncation, a failed ingest) reads `fail`, never a vacuous pass.
43
+ "assets_materialized",
41
44
  ] as const;
42
45
  export type EvidenceCheckId = (typeof EVIDENCE_CHECK_IDS)[number];
43
46
 
@@ -1,9 +1,6 @@
1
1
  packages:
2
2
  - "apps/*"
3
3
  - "packages/*"
4
- allowBuilds:
5
- esbuild: set this to true or false
6
- workerd: set this to true or false
7
4
  linkWorkspacePackages: true
8
5
  onlyBuiltDependencies:
9
6
  - esbuild
@@ -38,6 +38,12 @@
38
38
  * node scripts/tenant/materialize-tenant-content.mjs --check-pinned [--expect-ref <sha>] [--allow-unpinned]
39
39
  * # stricter deploy gate: verify populated AND materialized from Gitea at a
40
40
  * # PINNED ref (.materialized.json present, pinned, on the expected ref); no network
41
+ * node scripts/tenant/materialize-tenant-content.mjs --check-pinned --verify-fresh
42
+ * # opt-in NETWORK addition to --check-pinned: for any stamp pinned to a
43
+ * # MOVABLE ref (a branch name, or an unpinned default-branch materialize),
44
+ * # resolve that ref's CURRENT tip on Gitea and refuse if the stamped sha no
45
+ * # longer matches it (`git ls-remote`, no clone). A stamp pinned to an exact
46
+ * # commit SHA is frozen by design and can never be "stale" under this check.
41
47
  *
42
48
  * Auth (never logged; tokens are redacted from every error):
43
49
  * TENANT_GIT_BASE clone base. Default https://git.tokenoftrust.com/storefront
@@ -71,6 +77,7 @@ const repoRoot = resolve(here, "..", "..");
71
77
 
72
78
  export const MANIFEST_PATH = "tenants/.gitea-tenants.json";
73
79
  const TENANT_ID_RE = /^[a-z0-9][a-z0-9.-]{1,60}[a-z0-9]$/;
80
+ const FULL_SHA_RE = /^[0-9a-f]{40}$/i;
74
81
  const DEFAULT_GIT_BASE = "https://git.tokenoftrust.com/storefront";
75
82
 
76
83
  /** Load the Gitea-tenant manifest. Returns { tenants: { <id>: { repo } } }. */
@@ -380,9 +387,87 @@ export function checkPinned(
380
387
  return { ok: problems.length === 0, problems };
381
388
  }
382
389
 
383
- /** Human-readable refusal for a failed `checkPinned` (shared by the CLI + the
384
- * build-time guard). `problems` is checkPinned().problems; `expectRef` is echoed
385
- * into the off-ref explanation when set. */
390
+ /**
391
+ * `--verify-fresh` (opt-in, NETWORK): the one thing `checkPinned` deliberately cannot tell you —
392
+ * whether a pinned stamp is still current. `checkPinned` only proves the stamp is pinned to SOME
393
+ * ref (and, with `expectRef`, that it matches a caller-supplied value); it never talks to Gitea,
394
+ * so a stamp pinned to a MOVABLE ref (a branch name, or an unpinned default-branch materialize)
395
+ * can silently drift arbitrarily far behind that ref's current tip and keep passing forever.
396
+ *
397
+ * A stamp pinned to an exact commit SHA is excluded on purpose: that's a deliberate promotion to
398
+ * an immutable commit, frozen by design, and is never "stale" — re-promoting to a newer commit is
399
+ * a separate, explicit act (a fresh `--ref <sha>` materialize), not something this check should
400
+ * flag as a problem.
401
+ *
402
+ * For each in-scope tenant with a stamp, resolves the stamped ref's CURRENT tip via
403
+ * `git ls-remote` (no clone) and compares it to the stamped sha. Requires read access to Gitea —
404
+ * same TENANT_GIT_USER/TENANT_GIT_TOKEN as materializeTenant. Tenants that already fail
405
+ * `checkPinned` (absent/missing/unpinned-without-allowUnpinned/off-ref) are left to those
406
+ * existing reasons and skipped here to avoid double-reporting the same tenant.
407
+ *
408
+ * Returns { ok, problems: [{ tenant, reason }] } where reason ∈ "unreachable" | "stale":
409
+ * unreachable the live tip could not be resolved (network/auth failure, deleted ref, etc.) —
410
+ * never treated as fresh; an unreachable Gitea must not silently pass this gate.
411
+ * stale the stamped sha no longer matches the ref's live tip.
412
+ *
413
+ * @param {string} [root]
414
+ * @param {object} [opts]
415
+ * @param {string} [opts.gitBase]
416
+ * @param {string} [opts.user]
417
+ * @param {string} [opts.token]
418
+ * @param {string[]|null} [opts.onlyTenants]
419
+ */
420
+ export function checkFresh(
421
+ root = repoRoot,
422
+ {
423
+ gitBase = DEFAULT_GIT_BASE,
424
+ user = process.env.TENANT_GIT_USER,
425
+ token = process.env.TENANT_GIT_TOKEN,
426
+ onlyTenants = null,
427
+ } = {},
428
+ ) {
429
+ const manifest = loadManifest(root);
430
+ const problems = [];
431
+ const scope = onlyTenants ? new Set(onlyTenants) : null;
432
+ for (const tenant of Object.keys(manifest.tenants)) {
433
+ if (scope && !scope.has(tenant)) continue;
434
+ const stampPath = join(root, "tenants", tenant, ".materialized.json");
435
+ if (!existsSync(stampPath)) continue; // checkPinned already reports absent/missing
436
+ let stamp;
437
+ try {
438
+ stamp = JSON.parse(readFileSync(stampPath, "utf8"));
439
+ } catch {
440
+ continue; // checkPinned already reports this as "missing"
441
+ }
442
+ const sha = typeof stamp.sha === "string" ? stamp.sha : "";
443
+ const rawRef = typeof stamp.ref === "string" ? stamp.ref : "";
444
+ if (!sha || !FULL_SHA_RE.test(sha)) continue; // no usable sha to compare — leave to checkPinned
445
+ if (FULL_SHA_RE.test(rawRef)) continue; // pinned to an exact commit — frozen by design
446
+ const movableRef = rawRef === "" || rawRef === "(default branch)" ? "HEAD" : rawRef;
447
+ const repo = manifest.tenants[tenant]?.repo ?? tenant;
448
+ const cloneUrl = buildCloneUrl({ base: gitBase, repo, user, token });
449
+ let liveSha = "";
450
+ try {
451
+ const out = execFileSync("git", ["ls-remote", cloneUrl, movableRef], {
452
+ stdio: ["ignore", "pipe", "pipe"],
453
+ }).toString();
454
+ liveSha = (out.split(/\s+/)[0] || "").trim();
455
+ } catch {
456
+ problems.push({ tenant, reason: "unreachable" });
457
+ continue;
458
+ }
459
+ if (!FULL_SHA_RE.test(liveSha)) {
460
+ problems.push({ tenant, reason: "unreachable" });
461
+ continue;
462
+ }
463
+ if (liveSha !== sha) problems.push({ tenant, reason: "stale" });
464
+ }
465
+ return { ok: problems.length === 0, problems };
466
+ }
467
+
468
+ /** Human-readable refusal for a failed `checkPinned` (+ optional `checkFresh`) run (shared by
469
+ * the CLI + the build-time guard). `problems` is checkPinned().problems, optionally concatenated
470
+ * with checkFresh().problems; `expectRef` is echoed into the off-ref explanation when set. */
386
471
  export function describePinnedProblems(problems, /** @type {string|null} */ expectRef = null) {
387
472
  const how = {
388
473
  absent: "not materialized (no tenant content on disk at all)",
@@ -390,6 +475,8 @@ export function describePinnedProblems(problems, /** @type {string|null} */ expe
390
475
  unpinned:
391
476
  "materialized from the tenant repo's DEFAULT branch (no pinned ref) — may be stale",
392
477
  "off-ref": `materialized at a different ref than expected (${expectRef})`,
478
+ stale: "pinned ref's live Gitea tip has moved past the materialized sha — re-materialize",
479
+ unreachable: "could not resolve the tenant repo's live tip (network/auth failure)",
393
480
  };
394
481
  return (
395
482
  "REFUSING: tenant content is not materialized from Gitea at a pinned ref.\n" +
@@ -438,6 +525,7 @@ function parseArgs(argv) {
438
525
  checkPinned: false,
439
526
  expectRef: null,
440
527
  allowUnpinned: false,
528
+ verifyFresh: false,
441
529
  help: false,
442
530
  };
443
531
  for (let i = 0; i < argv.length; i++) {
@@ -449,11 +537,15 @@ function parseArgs(argv) {
449
537
  else if (t === "--check-pinned") a.checkPinned = true;
450
538
  else if (t === "--expect-ref") a.expectRef = argv[++i];
451
539
  else if (t === "--allow-unpinned") a.allowUnpinned = true;
540
+ else if (t === "--verify-fresh") a.verifyFresh = true;
452
541
  else if (t === "--help" || t === "-h") a.help = true;
453
542
  else if (!t.startsWith("--") && !a.tenant) a.tenant = t;
454
543
  else throw new Error(`unknown argument: ${t}`);
455
544
  }
456
545
  if (a.remote && a.all) throw new Error("--remote targets ONE tenant; not valid with --all");
546
+ if (a.verifyFresh && !a.checkPinned) {
547
+ throw new Error("--verify-fresh is only valid with --check-pinned");
548
+ }
457
549
  return a;
458
550
  }
459
551
 
@@ -463,6 +555,8 @@ const USAGE = `materialize-tenant-content — populate tenants/<id>/ from its Gi
463
555
  node scripts/tenant/materialize-tenant-content.mjs --all
464
556
  node scripts/tenant/materialize-tenant-content.mjs --check
465
557
  node scripts/tenant/materialize-tenant-content.mjs --check-pinned [--expect-ref <sha>] [--allow-unpinned]
558
+ node scripts/tenant/materialize-tenant-content.mjs --check-pinned --verify-fresh
559
+ # opt-in NETWORK addition: refuse if a movable-ref stamp's sha is behind Gitea's live tip
466
560
 
467
561
  env: TENANT_GIT_BASE (default ${DEFAULT_GIT_BASE}), TENANT_GIT_USER, TENANT_GIT_TOKEN`;
468
562
 
@@ -486,15 +580,21 @@ function main() {
486
580
  return;
487
581
  }
488
582
  if (args.checkPinned) {
489
- const { ok, problems } = checkPinned(repoRoot, {
583
+ const { problems } = checkPinned(repoRoot, {
490
584
  expectRef: args.expectRef,
491
585
  allowUnpinned: args.allowUnpinned,
492
586
  });
493
- if (!ok) {
494
- console.error(describePinnedProblems(problems, args.expectRef));
587
+ const freshProblems = args.verifyFresh ? checkFresh(repoRoot, { gitBase }).problems : [];
588
+ const allProblems = [...problems, ...freshProblems];
589
+ if (allProblems.length > 0) {
590
+ console.error(describePinnedProblems(allProblems, args.expectRef));
495
591
  process.exit(1);
496
592
  }
497
- console.log("All Gitea-classified tenants materialized from Gitea at a pinned ref.");
593
+ console.log(
594
+ args.verifyFresh
595
+ ? "All Gitea-classified tenants materialized from Gitea at a pinned ref, verified fresh against the live tip."
596
+ : "All Gitea-classified tenants materialized from Gitea at a pinned ref.",
597
+ );
498
598
  return;
499
599
  }
500
600
  const manifest = loadManifest();
@@ -1,159 +0,0 @@
1
- declare module 'astro:content' {
2
- export interface RenderResult {
3
- Content: import('astro/runtime/server/index.js').AstroComponentFactory;
4
- headings: import('astro').MarkdownHeading[];
5
- remarkPluginFrontmatter: Record<string, any>;
6
- }
7
- interface Render {
8
- '.md': Promise<RenderResult>;
9
- }
10
-
11
- export interface RenderedContent {
12
- html: string;
13
- metadata?: {
14
- imagePaths: Array<string>;
15
- [key: string]: unknown;
16
- };
17
- }
18
-
19
- type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
20
-
21
- export type CollectionKey = keyof DataEntryMap;
22
- export type CollectionEntry<C extends CollectionKey> = Flatten<DataEntryMap[C]>;
23
-
24
- type AllValuesOf<T> = T extends any ? T[keyof T] : never;
25
-
26
- export type ReferenceDataEntry<
27
- C extends CollectionKey,
28
- E extends keyof DataEntryMap[C] = string,
29
- > = {
30
- collection: C;
31
- id: E;
32
- };
33
-
34
- export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
35
- collection: C;
36
- id: string;
37
- };
38
-
39
- export function getCollection<C extends keyof DataEntryMap, E extends CollectionEntry<C>>(
40
- collection: C,
41
- filter?: (entry: CollectionEntry<C>) => entry is E,
42
- ): Promise<E[]>;
43
- export function getCollection<C extends keyof DataEntryMap>(
44
- collection: C,
45
- filter?: (entry: CollectionEntry<C>) => unknown,
46
- ): Promise<CollectionEntry<C>[]>;
47
-
48
- export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
49
- collection: C,
50
- filter?: LiveLoaderCollectionFilterType<C>,
51
- ): Promise<
52
- import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
53
- >;
54
-
55
- export function getEntry<
56
- C extends keyof DataEntryMap,
57
- E extends keyof DataEntryMap[C] | (string & {}),
58
- >(
59
- entry: ReferenceDataEntry<C, E>,
60
- ): E extends keyof DataEntryMap[C]
61
- ? Promise<DataEntryMap[C][E]>
62
- : Promise<CollectionEntry<C> | undefined>;
63
- export function getEntry<
64
- C extends keyof DataEntryMap,
65
- E extends keyof DataEntryMap[C] | (string & {}),
66
- >(
67
- collection: C,
68
- id: E,
69
- ): E extends keyof DataEntryMap[C]
70
- ? string extends keyof DataEntryMap[C]
71
- ? Promise<DataEntryMap[C][E]> | undefined
72
- : Promise<DataEntryMap[C][E]>
73
- : Promise<CollectionEntry<C> | undefined>;
74
- export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
75
- collection: C,
76
- filter: string | LiveLoaderEntryFilterType<C>,
77
- ): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
78
-
79
- /** Resolve an array of entry references from the same collection */
80
- export function getEntries<C extends keyof DataEntryMap>(
81
- entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
82
- ): Promise<CollectionEntry<C>[]>;
83
-
84
- export function render<C extends keyof DataEntryMap>(
85
- entry: DataEntryMap[C][string],
86
- ): Promise<RenderResult>;
87
-
88
- export function render<C extends keyof LiveContentConfig['collections']>(
89
- entry: import('astro').LiveDataEntry<LiveLoaderDataType<C>>,
90
- ): Promise<RenderResult>;
91
-
92
- export function reference<
93
- C extends
94
- | keyof DataEntryMap
95
- // Allow generic `string` to avoid excessive type errors in the config
96
- // if `dev` is not running to update as you edit.
97
- // Invalid collection names will be caught at build time.
98
- | (string & {}),
99
- >(
100
- collection: C,
101
- ): import('astro/zod').ZodPipe<
102
- import('astro/zod').ZodString,
103
- import('astro/zod').ZodTransform<
104
- C extends keyof DataEntryMap
105
- ? {
106
- collection: C;
107
- id: string;
108
- }
109
- : never,
110
- string
111
- >
112
- >;
113
-
114
- type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
115
- type InferEntrySchema<C extends keyof DataEntryMap> = import('astro/zod').infer<
116
- ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
117
- >;
118
- type ExtractLoaderConfig<T> = T extends { loader: infer L } ? L : never;
119
- type InferLoaderSchema<
120
- C extends keyof DataEntryMap,
121
- L = ExtractLoaderConfig<ContentConfig['collections'][C]>,
122
- > = L extends { schema: import('astro/zod').ZodSchema }
123
- ? import('astro/zod').infer<L['schema']>
124
- : any;
125
-
126
- type DataEntryMap = {
127
-
128
- };
129
-
130
- type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
131
- infer TData,
132
- infer TEntryFilter,
133
- infer TCollectionFilter,
134
- infer TError
135
- >
136
- ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
137
- : { data: never; entryFilter: never; collectionFilter: never; error: never };
138
- type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
139
- type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
140
- type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
141
- type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
142
-
143
- type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
144
- LiveContentConfig['collections'][C]['schema'] extends undefined
145
- ? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
146
- : import('astro/zod').infer<
147
- Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
148
- >;
149
- type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
150
- ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
151
- type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
152
- ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
153
- type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
154
- LiveContentConfig['collections'][C]['loader']
155
- >;
156
-
157
- export type ContentConfig = never;
158
- export type LiveContentConfig = never;
159
- }
@@ -1 +0,0 @@
1
- /// <reference types="@astrojs/cloudflare/types.d.ts" />
@@ -1,3 +0,0 @@
1
- /// <reference types="astro/client" />
2
- /// <reference path="integrations/_astrojs_cloudflare/cloudflare.d.ts" />
3
- /// <reference path="content.d.ts" />
@@ -1,43 +0,0 @@
1
- #!/bin/sh
2
- basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
- basedir_win="$basedir"
4
- exe=""
5
- msys=""
6
-
7
- case `uname -a` in
8
- *CYGWIN*|*MINGW*|*MSYS*)
9
- if command -v cygpath > /dev/null 2>&1; then
10
- basedir_win=`cygpath -w "$basedir"`
11
- fi
12
- exe=".exe"
13
- msys="true"
14
- ;;
15
- *WSL2*)
16
- if command -v wslpath > /dev/null 2>&1; then
17
- basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
- if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
- basedir_win="$basedir"
20
- else
21
- exe=".exe"
22
- fi
23
- fi
24
- ;;
25
- esac
26
-
27
- if [ -z "$NODE_PATH" ]; then
28
- export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/acorn@8.18.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
- else
30
- export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/acorn@8.18.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
- fi
32
- if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
- exec "$basedir/node.exe" "$basedir_win/../../../../node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/bin/acorn" "$@"
34
- elif [ -x "$basedir/node" ]; then
35
- exec "$basedir/node" "$basedir/../../../../node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/bin/acorn" "$@"
36
- elif command -v node >/dev/null 2>&1; then
37
- exec node "$basedir/../../../../node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/bin/acorn" "$@"
38
- elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
- exec node.exe "$basedir_win/../../../../node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/bin/acorn" "$@"
40
- else
41
- exec node "$basedir/../../../../node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/bin/acorn" "$@"
42
- fi
43
- # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/bin/acorn
@@ -1,43 +0,0 @@
1
- #!/bin/sh
2
- basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
- basedir_win="$basedir"
4
- exe=""
5
- msys=""
6
-
7
- case `uname -a` in
8
- *CYGWIN*|*MINGW*|*MSYS*)
9
- if command -v cygpath > /dev/null 2>&1; then
10
- basedir_win=`cygpath -w "$basedir"`
11
- fi
12
- exe=".exe"
13
- msys="true"
14
- ;;
15
- *WSL2*)
16
- if command -v wslpath > /dev/null 2>&1; then
17
- basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
- if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
- basedir_win="$basedir"
20
- else
21
- exe=".exe"
22
- fi
23
- fi
24
- ;;
25
- esac
26
-
27
- if [ -z "$NODE_PATH" ]; then
28
- export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/astro@7.2.10_@emnapi+core@1.11.1_@emnapi+runtime@1.11.3_@types+node@26.4.1_jiti@2.7.0_tsx@4.23.13_yaml@2.9.0/node_modules/astro/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/astro@7.2.10_@emnapi+core@1.11.1_@emnapi+runtime@1.11.3_@types+node@26.4.1_jiti@2.7.0_tsx@4.23.13_yaml@2.9.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
- else
30
- export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/astro@7.2.10_@emnapi+core@1.11.1_@emnapi+runtime@1.11.3_@types+node@26.4.1_jiti@2.7.0_tsx@4.23.13_yaml@2.9.0/node_modules/astro/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/astro@7.2.10_@emnapi+core@1.11.1_@emnapi+runtime@1.11.3_@types+node@26.4.1_jiti@2.7.0_tsx@4.23.13_yaml@2.9.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
- fi
32
- if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
- exec "$basedir/node.exe" "$basedir_win/../astro/bin/astro.mjs" "$@"
34
- elif [ -x "$basedir/node" ]; then
35
- exec "$basedir/node" "$basedir/../astro/bin/astro.mjs" "$@"
36
- elif command -v node >/dev/null 2>&1; then
37
- exec node "$basedir/../astro/bin/astro.mjs" "$@"
38
- elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
- exec node.exe "$basedir_win/../astro/bin/astro.mjs" "$@"
40
- else
41
- exec node "$basedir/../astro/bin/astro.mjs" "$@"
42
- fi
43
- # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/apps/storefront/node_modules/astro/bin/astro.mjs