@growth-labs/cms 0.5.29 → 0.5.31

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 (151) hide show
  1. package/dist/engine/foundry-dispatch.d.ts +7 -2
  2. package/dist/engine/foundry-dispatch.d.ts.map +1 -1
  3. package/dist/engine/foundry-dispatch.js +24 -6
  4. package/dist/engine/foundry-dispatch.js.map +1 -1
  5. package/dist/engine/fronts-publish-intent.d.ts +174 -0
  6. package/dist/engine/fronts-publish-intent.d.ts.map +1 -0
  7. package/dist/engine/fronts-publish-intent.js +352 -0
  8. package/dist/engine/fronts-publish-intent.js.map +1 -0
  9. package/dist/engine/fronts-publish.d.ts +80 -0
  10. package/dist/engine/fronts-publish.d.ts.map +1 -0
  11. package/dist/engine/fronts-publish.js +286 -0
  12. package/dist/engine/fronts-publish.js.map +1 -0
  13. package/dist/engine/index.d.ts +7 -0
  14. package/dist/engine/index.d.ts.map +1 -1
  15. package/dist/engine/index.js +10 -0
  16. package/dist/engine/index.js.map +1 -1
  17. package/dist/engine/publication-continuity.d.ts +77 -0
  18. package/dist/engine/publication-continuity.d.ts.map +1 -0
  19. package/dist/engine/publication-continuity.js +94 -0
  20. package/dist/engine/publication-continuity.js.map +1 -0
  21. package/dist/engine/publication-receipt/canonical-json.d.ts +34 -0
  22. package/dist/engine/publication-receipt/canonical-json.d.ts.map +1 -0
  23. package/dist/engine/publication-receipt/canonical-json.js +100 -0
  24. package/dist/engine/publication-receipt/canonical-json.js.map +1 -0
  25. package/dist/engine/publication-receipt/receipt.d.ts +316 -0
  26. package/dist/engine/publication-receipt/receipt.d.ts.map +1 -0
  27. package/dist/engine/publication-receipt/receipt.js +131 -0
  28. package/dist/engine/publication-receipt/receipt.js.map +1 -0
  29. package/dist/engine/publication-receipt/signature.d.ts +36 -0
  30. package/dist/engine/publication-receipt/signature.d.ts.map +1 -0
  31. package/dist/engine/publication-receipt/signature.js +79 -0
  32. package/dist/engine/publication-receipt/signature.js.map +1 -0
  33. package/dist/engine/publication.d.ts.map +1 -1
  34. package/dist/engine/publication.js +2 -0
  35. package/dist/engine/publication.js.map +1 -1
  36. package/dist/engine/publisher.d.ts.map +1 -1
  37. package/dist/engine/publisher.js +23 -1
  38. package/dist/engine/publisher.js.map +1 -1
  39. package/dist/engine/source-sections.d.ts +20 -0
  40. package/dist/engine/source-sections.d.ts.map +1 -0
  41. package/dist/engine/source-sections.js +130 -0
  42. package/dist/engine/source-sections.js.map +1 -0
  43. package/dist/engine/validator/checks/source-section-diagnostics.d.ts +7 -0
  44. package/dist/engine/validator/checks/source-section-diagnostics.d.ts.map +1 -0
  45. package/dist/engine/validator/checks/source-section-diagnostics.js +120 -0
  46. package/dist/engine/validator/checks/source-section-diagnostics.js.map +1 -0
  47. package/dist/engine/validator/index.d.ts +10 -0
  48. package/dist/engine/validator/index.d.ts.map +1 -1
  49. package/dist/engine/validator/index.js +4 -0
  50. package/dist/engine/validator/index.js.map +1 -1
  51. package/dist/engine/validator/types.d.ts +1 -1
  52. package/dist/engine/validator/types.d.ts.map +1 -1
  53. package/dist/engine/validator/types.js +10 -0
  54. package/dist/engine/validator/types.js.map +1 -1
  55. package/dist/integration/index.d.ts.map +1 -1
  56. package/dist/integration/index.js +10 -0
  57. package/dist/integration/index.js.map +1 -1
  58. package/dist/providers/types.d.ts +1 -0
  59. package/dist/providers/types.d.ts.map +1 -1
  60. package/dist/routes/content.d.ts.map +1 -1
  61. package/dist/routes/content.js +22 -2
  62. package/dist/routes/content.js.map +1 -1
  63. package/dist/routes/fronts-publish.d.ts +10 -0
  64. package/dist/routes/fronts-publish.d.ts.map +1 -0
  65. package/dist/routes/fronts-publish.js +250 -0
  66. package/dist/routes/fronts-publish.js.map +1 -0
  67. package/dist/routes/index.d.ts +3 -0
  68. package/dist/routes/index.d.ts.map +1 -1
  69. package/dist/routes/index.js +4 -0
  70. package/dist/routes/index.js.map +1 -1
  71. package/dist/routes/surveys.d.ts.map +1 -1
  72. package/dist/routes/surveys.js +13 -1
  73. package/dist/routes/surveys.js.map +1 -1
  74. package/dist/schema/insights-ingest.d.ts +28 -28
  75. package/dist/schema/layout.d.ts +2 -2
  76. package/dist/schema/migrations.d.ts.map +1 -1
  77. package/dist/schema/migrations.js +36 -0
  78. package/dist/schema/migrations.js.map +1 -1
  79. package/dist/schema/portable-text.d.ts +2 -2
  80. package/dist/schema/tables.d.ts +1 -1
  81. package/dist/schema/tables.d.ts.map +1 -1
  82. package/dist/schema/tables.js +2 -0
  83. package/dist/schema/tables.js.map +1 -1
  84. package/dist/schema/types.d.ts +59 -0
  85. package/dist/schema/types.d.ts.map +1 -1
  86. package/dist/schema/types.js.map +1 -1
  87. package/dist/surveys/access.d.ts +2 -0
  88. package/dist/surveys/access.d.ts.map +1 -1
  89. package/dist/surveys/access.js +7 -0
  90. package/dist/surveys/access.js.map +1 -1
  91. package/dist/surveys/errors.d.ts +8 -0
  92. package/dist/surveys/errors.d.ts.map +1 -1
  93. package/dist/surveys/errors.js +12 -0
  94. package/dist/surveys/errors.js.map +1 -1
  95. package/dist/surveys/schema.d.ts +129 -54
  96. package/dist/surveys/schema.d.ts.map +1 -1
  97. package/dist/surveys/schema.js +45 -0
  98. package/dist/surveys/schema.js.map +1 -1
  99. package/dist/ui/api/fronts/publish-callback.d.ts +3 -0
  100. package/dist/ui/api/fronts/publish-callback.d.ts.map +1 -0
  101. package/dist/ui/api/fronts/publish-callback.js +8 -0
  102. package/dist/ui/api/fronts/publish-callback.js.map +1 -0
  103. package/dist/ui/api/fronts/schedule.d.ts +3 -0
  104. package/dist/ui/api/fronts/schedule.d.ts.map +1 -0
  105. package/dist/ui/api/fronts/schedule.js +8 -0
  106. package/dist/ui/api/fronts/schedule.js.map +1 -0
  107. package/dist/ui/editor/ContentForm.d.ts.map +1 -1
  108. package/dist/ui/editor/ContentForm.js +52 -1
  109. package/dist/ui/editor/ContentForm.js.map +1 -1
  110. package/dist/ui/editor/body-warnings.d.ts +27 -0
  111. package/dist/ui/editor/body-warnings.d.ts.map +1 -0
  112. package/dist/ui/editor/body-warnings.js +58 -0
  113. package/dist/ui/editor/body-warnings.js.map +1 -0
  114. package/dist/ui/screens/SurveyResponsesScreen.d.ts +7 -1
  115. package/dist/ui/screens/SurveyResponsesScreen.d.ts.map +1 -1
  116. package/dist/ui/screens/SurveyResponsesScreen.js +141 -32
  117. package/dist/ui/screens/SurveyResponsesScreen.js.map +1 -1
  118. package/migrations/0026_fronts_publish.sql +31 -0
  119. package/migrations/0027_fronts_publish_intent.sql +44 -0
  120. package/package.json +1 -1
  121. package/src/engine/foundry-dispatch.ts +22 -6
  122. package/src/engine/fronts-publish-intent.ts +676 -0
  123. package/src/engine/fronts-publish.ts +417 -0
  124. package/src/engine/index.ts +60 -0
  125. package/src/engine/publication-continuity.ts +140 -0
  126. package/src/engine/publication-receipt/canonical-json.ts +123 -0
  127. package/src/engine/publication-receipt/receipt.ts +160 -0
  128. package/src/engine/publication-receipt/signature.ts +112 -0
  129. package/src/engine/publication.ts +2 -0
  130. package/src/engine/publisher.ts +23 -1
  131. package/src/engine/source-sections.ts +153 -0
  132. package/src/engine/validator/checks/source-section-diagnostics.ts +187 -0
  133. package/src/engine/validator/index.ts +12 -0
  134. package/src/engine/validator/types.ts +19 -0
  135. package/src/integration/index.ts +10 -0
  136. package/src/providers/types.ts +1 -1
  137. package/src/routes/content.ts +23 -2
  138. package/src/routes/fronts-publish.ts +292 -0
  139. package/src/routes/index.ts +8 -0
  140. package/src/routes/surveys.ts +12 -1
  141. package/src/schema/migrations.ts +36 -0
  142. package/src/schema/tables.ts +2 -0
  143. package/src/schema/types.ts +61 -0
  144. package/src/surveys/access.ts +9 -0
  145. package/src/surveys/errors.ts +12 -0
  146. package/src/surveys/schema.ts +47 -0
  147. package/src/ui/api/fronts/publish-callback.ts +18 -0
  148. package/src/ui/api/fronts/schedule.ts +18 -0
  149. package/src/ui/editor/ContentForm.tsx +93 -0
  150. package/src/ui/editor/body-warnings.ts +78 -0
  151. package/src/ui/screens/SurveyResponsesScreen.tsx +369 -121
@@ -0,0 +1,94 @@
1
+ // Publication continuity decisions (#342).
2
+ //
3
+ // Extracted from the Warfronts publish bridge so every site shares one rule set
4
+ // instead of each re-deriving it. These are the decisions a continuity run makes
5
+ // before it touches the database: is this item retired, did the published
6
+ // revision drift from the archive, and does a live probe response mean success.
7
+ //
8
+ // All pure. The bridge's DB, network and filesystem work stays site-owned; only
9
+ // the judgement moves here.
10
+ /** An item the archive has taken down rather than published. */
11
+ export function isRetiredItem(item) {
12
+ return item?.metadata?.originalStatus === 'archived';
13
+ }
14
+ /**
15
+ * Compare the fields a reader actually sees against the published revision.
16
+ *
17
+ * The site renders from the published revision payload, so an edit to any field
18
+ * this does not examine is classified "already in sync", skipped, and never
19
+ * reaches a reader — while the run reports success. That has stranded real work
20
+ * twice: fourteen restored article bodies, and a `featured` flag flip, both
21
+ * merged, both reporting `failed=0`, neither ever visible.
22
+ *
23
+ * Comparison is exact rather than a length or prefix heuristic. The entire class
24
+ * of bug being prevented is a cheap test that silently misses a real edit: a
25
+ * false "stale" costs one idempotent republish, a false "in sync" loses the edit.
26
+ *
27
+ * NOT COMPARED: tags. The revision stores them canonicalized and sorted while
28
+ * the archive carries raw frontmatter strings, and the engine exports no
29
+ * canonicalizer to reuse. Detecting a field this cannot also REPAIR would be
30
+ * worse than not detecting it — tag writes go through the engine, so a tag-only
31
+ * drift would republish unchanged and stay stale on every run, forever.
32
+ */
33
+ export function payloadDrift(item, row) {
34
+ // The engine stores an absent body as a single space, so normalize the same
35
+ // way before comparing or the first resync of a body-less article would
36
+ // churn forever.
37
+ const body = item.bodyMarkdown ?? ' ';
38
+ const title = String(item.title ?? '');
39
+ const description = item.description ?? null;
40
+ const featured = item.featured ? 1 : 0;
41
+ const heroImageId = item.heroImageId ?? null;
42
+ const visibility = item.visibility === 'premium' ? 'premium' : 'free';
43
+ const channel = item.primaryCategory ?? null;
44
+ const fields = [];
45
+ if (row.rev_title !== title)
46
+ fields.push('title');
47
+ if ((row.rev_description ?? null) !== description)
48
+ fields.push('description');
49
+ if (Number(row.rev_featured ?? 0) !== featured)
50
+ fields.push('featured');
51
+ if ((row.rev_hero_image_id ?? null) !== heroImageId)
52
+ fields.push('heroImage');
53
+ if ((row.rev_visibility ?? 'free') !== visibility)
54
+ fields.push('visibility');
55
+ if ((row.rev_channel ?? null) !== channel)
56
+ fields.push('channel');
57
+ // A revision predating this check can lack the key entirely; treat an absent
58
+ // body as drift rather than silently matching.
59
+ if ((row.rev_body_markdown ?? null) !== body)
60
+ fields.push('body');
61
+ return {
62
+ drifted: fields.length > 0,
63
+ fields,
64
+ title,
65
+ description,
66
+ featured,
67
+ heroImageId,
68
+ visibility,
69
+ channel,
70
+ body,
71
+ bodyDrifted: fields.includes('body'),
72
+ };
73
+ }
74
+ /**
75
+ * Whether a live probe response means the publication succeeded.
76
+ *
77
+ * LIVE → 200 only.
78
+ * RETIRED → 301/308 (redirected to a surviving twin) or 404 (gone, with no
79
+ * honest target to redirect to).
80
+ *
81
+ * The invariant: **a retired article must never answer 200.** That is the
82
+ * stale-row symptom a teardown exists to remove, and it is the only status this
83
+ * rejects for a retirement. Demanding 200 for retirements instead would fail the
84
+ * run forever on exactly the articles the archive deliberately took down.
85
+ *
86
+ * 301 and 308 are both permanent; a server may answer GET with one and other
87
+ * methods with the other, so both are accepted.
88
+ */
89
+ export function acceptsProbeStatus(status, retired) {
90
+ if (!retired)
91
+ return status === 200;
92
+ return status === 301 || status === 308 || status === 404;
93
+ }
94
+ //# sourceMappingURL=publication-continuity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publication-continuity.js","sourceRoot":"","sources":["../../src/engine/publication-continuity.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,EAAE;AACF,gFAAgF;AAChF,iFAAiF;AACjF,0EAA0E;AAC1E,gFAAgF;AAChF,EAAE;AACF,gFAAgF;AAChF,4BAA4B;AAiD5B,gEAAgE;AAChE,MAAM,UAAU,aAAa,CAAC,IAA8C;IAC3E,OAAO,IAAI,EAAE,QAAQ,EAAE,cAAc,KAAK,UAAU,CAAA;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,YAAY,CAC3B,IAA2B,EAC3B,GAA2B;IAE3B,4EAA4E;IAC5E,wEAAwE;IACxE,iBAAiB;IACjB,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,IAAI,GAAG,CAAA;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;IACtC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAA;IAC5C,MAAM,QAAQ,GAAU,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7C,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAA;IAC5C,MAAM,UAAU,GAAuB,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAA;IACzF,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,IAAI,IAAI,CAAA;IAE5C,MAAM,MAAM,GAA2B,EAAE,CAAA;IACzC,IAAI,GAAG,CAAC,SAAS,KAAK,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACjD,IAAI,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,KAAK,WAAW;QAAE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAC7E,IAAI,MAAM,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,CAAC,KAAK,QAAQ;QAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACvE,IAAI,CAAC,GAAG,CAAC,iBAAiB,IAAI,IAAI,CAAC,KAAK,WAAW;QAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC7E,IAAI,CAAC,GAAG,CAAC,cAAc,IAAI,MAAM,CAAC,KAAK,UAAU;QAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IAC5E,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,OAAO;QAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACjE,6EAA6E;IAC7E,+CAA+C;IAC/C,IAAI,CAAC,GAAG,CAAC,iBAAiB,IAAI,IAAI,CAAC,KAAK,IAAI;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAEjE,OAAO;QACN,OAAO,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC;QAC1B,MAAM;QACN,KAAK;QACL,WAAW;QACX,QAAQ;QACR,WAAW;QACX,UAAU;QACV,OAAO;QACP,IAAI;QACJ,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;KACpC,CAAA;AACF,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc,EAAE,OAAgB;IAClE,IAAI,CAAC,OAAO;QAAE,OAAO,MAAM,KAAK,GAAG,CAAA;IACnC,OAAO,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,CAAA;AAC1D,CAAC"}
@@ -0,0 +1,34 @@
1
+ export declare class CanonicalJsonError extends Error {
2
+ readonly code: 'non_finite_number' | 'unsupported_type' | 'circular_reference';
3
+ constructor(code: CanonicalJsonError['code'], message: string);
4
+ }
5
+ type JsonPrimitive = string | number | boolean | null;
6
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
7
+ [key: string]: JsonValue;
8
+ };
9
+ /**
10
+ * Serialize `value` per RFC 8785.
11
+ *
12
+ * Three things do the work, and each is load-bearing:
13
+ *
14
+ * 1. Object keys sort by UTF-16 code unit. JavaScript's default string sort is
15
+ * already a UTF-16 code-unit comparison, which is exactly what §3.2.3
16
+ * requires — so `.sort()` here is the specified behaviour, not a shortcut.
17
+ * 2. Numbers use ECMAScript `Number::toString`, which `JSON.stringify` applies.
18
+ * §3.2.2.3 defers to that same algorithm, so the platform is authoritative
19
+ * and reimplementing it would only introduce drift.
20
+ * 3. Strings use `JSON.stringify`'s escaping, which since ES2019 emits
21
+ * well-formed output (lone surrogates escaped rather than passed through) —
22
+ * §3.2.2.2's requirement.
23
+ *
24
+ * Non-finite numbers throw rather than becoming `null` as `JSON.stringify`
25
+ * would. A receipt containing `NaN` is a bug, and silently signing `null` in
26
+ * its place would make that bug undetectable downstream.
27
+ */
28
+ export declare function canonicalJson(value: JsonValue): string;
29
+ /** Canonical UTF-8 bytes, which are what gets hashed and signed. */
30
+ export declare function canonicalJsonBytes(value: JsonValue): Uint8Array;
31
+ /** Lowercase hex SHA-256 of the canonical bytes. */
32
+ export declare function canonicalJsonSha256(value: JsonValue): Promise<string>;
33
+ export {};
34
+ //# sourceMappingURL=canonical-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-json.d.ts","sourceRoot":"","sources":["../../../src/engine/publication-receipt/canonical-json.ts"],"names":[],"mappings":"AAYA,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAA;gBAElE,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM;CAK7D;AAED,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AACrD,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAElF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAEtD;AA4DD,oEAAoE;AACpE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAE/D;AAED,oDAAoD;AACpD,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAU3E"}
@@ -0,0 +1,100 @@
1
+ // RFC 8785 JSON Canonicalization Scheme (#342).
2
+ //
3
+ // A publication receipt is hashed and signed, and the same bytes must be
4
+ // reproducible by Foundry from the same object. Canonicalization is what makes
5
+ // that true: `JSON.stringify` alone is not stable, because object key order
6
+ // follows insertion order rather than a defined sort.
7
+ //
8
+ // Deliberately implemented rather than pulled from a dependency. The whole
9
+ // value here is that both sides agree byte-for-byte, so the algorithm should be
10
+ // readable, pinned, and covered by the RFC's own vectors rather than tracking
11
+ // someone else's semver.
12
+ export class CanonicalJsonError extends Error {
13
+ code;
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.name = 'CanonicalJsonError';
17
+ this.code = code;
18
+ }
19
+ }
20
+ /**
21
+ * Serialize `value` per RFC 8785.
22
+ *
23
+ * Three things do the work, and each is load-bearing:
24
+ *
25
+ * 1. Object keys sort by UTF-16 code unit. JavaScript's default string sort is
26
+ * already a UTF-16 code-unit comparison, which is exactly what §3.2.3
27
+ * requires — so `.sort()` here is the specified behaviour, not a shortcut.
28
+ * 2. Numbers use ECMAScript `Number::toString`, which `JSON.stringify` applies.
29
+ * §3.2.2.3 defers to that same algorithm, so the platform is authoritative
30
+ * and reimplementing it would only introduce drift.
31
+ * 3. Strings use `JSON.stringify`'s escaping, which since ES2019 emits
32
+ * well-formed output (lone surrogates escaped rather than passed through) —
33
+ * §3.2.2.2's requirement.
34
+ *
35
+ * Non-finite numbers throw rather than becoming `null` as `JSON.stringify`
36
+ * would. A receipt containing `NaN` is a bug, and silently signing `null` in
37
+ * its place would make that bug undetectable downstream.
38
+ */
39
+ export function canonicalJson(value) {
40
+ return write(value, new Set());
41
+ }
42
+ function write(value, seen) {
43
+ if (value === null)
44
+ return 'null';
45
+ switch (typeof value) {
46
+ case 'boolean':
47
+ return value ? 'true' : 'false';
48
+ case 'number': {
49
+ if (!Number.isFinite(value)) {
50
+ throw new CanonicalJsonError('non_finite_number', `Cannot canonicalize non-finite number: ${String(value)}`);
51
+ }
52
+ // -0 serializes as "0" per RFC 8785 §3.2.2.3; JSON.stringify agrees.
53
+ return JSON.stringify(value);
54
+ }
55
+ case 'string':
56
+ return JSON.stringify(value);
57
+ case 'object':
58
+ break;
59
+ default:
60
+ throw new CanonicalJsonError('unsupported_type', `Cannot canonicalize value of type ${typeof value}`);
61
+ }
62
+ const object = value;
63
+ if (seen.has(object)) {
64
+ throw new CanonicalJsonError('circular_reference', 'Cannot canonicalize a circular structure');
65
+ }
66
+ seen.add(object);
67
+ try {
68
+ if (Array.isArray(value)) {
69
+ return `[${value.map((entry) => write(entry, seen)).join(',')}]`;
70
+ }
71
+ const record = value;
72
+ // Undefined-valued keys are omitted, matching JSON.stringify. RFC 8785
73
+ // has no representation for them.
74
+ const keys = Object.keys(record)
75
+ .filter((key) => record[key] !== undefined)
76
+ .sort();
77
+ const members = keys.map((key) => `${JSON.stringify(key)}:${write(record[key], seen)}`);
78
+ return `{${members.join(',')}}`;
79
+ }
80
+ finally {
81
+ seen.delete(object);
82
+ }
83
+ }
84
+ /** Canonical UTF-8 bytes, which are what gets hashed and signed. */
85
+ export function canonicalJsonBytes(value) {
86
+ return new TextEncoder().encode(canonicalJson(value));
87
+ }
88
+ /** Lowercase hex SHA-256 of the canonical bytes. */
89
+ export async function canonicalJsonSha256(value) {
90
+ // `Uint8Array.from` yields an ArrayBuffer-backed copy. TextEncoder returns
91
+ // `Uint8Array<ArrayBufferLike>`, which may be SharedArrayBuffer-backed and
92
+ // so is not a `BufferSource`. Same idiom as sha256Hex in
93
+ // @growth-labs/monitoring's site-agent signature helper.
94
+ const copy = Uint8Array.from(canonicalJsonBytes(value));
95
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', copy);
96
+ return Array.from(new Uint8Array(digest))
97
+ .map((byte) => byte.toString(16).padStart(2, '0'))
98
+ .join('');
99
+ }
100
+ //# sourceMappingURL=canonical-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-json.js","sourceRoot":"","sources":["../../../src/engine/publication-receipt/canonical-json.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,316 @@
1
+ import { z } from 'zod';
2
+ export declare const SITE_PUBLICATION_RECEIPT_SCHEMA: "site.publication.receipt.v1";
3
+ export declare const SITE_PUBLICATION_RECEIPT_AUDIENCE: "foundry:site-publication-receipt";
4
+ export declare const publicationProbeSchema: z.ZodObject<{
5
+ /** What was probed, e.g. `html` or `markdown`. */
6
+ kind: z.ZodEnum<["html", "markdown"]>;
7
+ url: z.ZodString;
8
+ status: z.ZodNumber;
9
+ /** SHA-256 of the exact bytes observed, so Foundry can compare. */
10
+ digest: z.ZodString;
11
+ }, "strict", z.ZodTypeAny, {
12
+ status: number;
13
+ kind: "html" | "markdown";
14
+ url: string;
15
+ digest: string;
16
+ }, {
17
+ status: number;
18
+ kind: "html" | "markdown";
19
+ url: string;
20
+ digest: string;
21
+ }>;
22
+ export declare const publicationProjectionCountsSchema: z.ZodObject<{
23
+ seo: z.ZodNumber;
24
+ search: z.ZodNumber;
25
+ sitemap: z.ZodNumber;
26
+ feed: z.ZodNumber;
27
+ }, "strict", z.ZodTypeAny, {
28
+ search: number;
29
+ seo: number;
30
+ sitemap: number;
31
+ feed: number;
32
+ }, {
33
+ search: number;
34
+ seo: number;
35
+ sitemap: number;
36
+ feed: number;
37
+ }>;
38
+ export declare const sitePublicationReceiptBodySchema: z.ZodEffects<z.ZodObject<{
39
+ schema: z.ZodLiteral<"site.publication.receipt.v1">;
40
+ audience: z.ZodLiteral<"foundry:site-publication-receipt">;
41
+ /** Success and failure are both signed; a silent failure is unverifiable. */
42
+ outcome: z.ZodEnum<["success", "failure"]>;
43
+ /** Identity of the publishing property (the site). */
44
+ property: z.ZodObject<{
45
+ id: z.ZodString;
46
+ origin: z.ZodString;
47
+ }, "strict", z.ZodTypeAny, {
48
+ id: string;
49
+ origin: string;
50
+ }, {
51
+ id: string;
52
+ origin: string;
53
+ }>;
54
+ /** Identity of the thing published, stable across revisions. */
55
+ content: z.ZodObject<{
56
+ stableId: z.ZodString;
57
+ slug: z.ZodString;
58
+ revisionId: z.ZodString;
59
+ }, "strict", z.ZodTypeAny, {
60
+ slug: string;
61
+ revisionId: string;
62
+ stableId: string;
63
+ }, {
64
+ slug: string;
65
+ revisionId: string;
66
+ stableId: string;
67
+ }>;
68
+ /** What produced it. */
69
+ source: z.ZodObject<{
70
+ commit: z.ZodString;
71
+ foundryHandoffId: z.ZodString;
72
+ foundryRunId: z.ZodString;
73
+ }, "strict", z.ZodTypeAny, {
74
+ commit: string;
75
+ foundryHandoffId: string;
76
+ foundryRunId: string;
77
+ }, {
78
+ commit: string;
79
+ foundryHandoffId: string;
80
+ foundryRunId: string;
81
+ }>;
82
+ /** Logical database identity — never a connection string or credential. */
83
+ database: z.ZodObject<{
84
+ logicalId: z.ZodString;
85
+ }, "strict", z.ZodTypeAny, {
86
+ logicalId: string;
87
+ }, {
88
+ logicalId: string;
89
+ }>;
90
+ /** Hash of the public media manifest, not the media itself. */
91
+ publicMediaManifestHash: z.ZodString;
92
+ projections: z.ZodObject<{
93
+ seo: z.ZodNumber;
94
+ search: z.ZodNumber;
95
+ sitemap: z.ZodNumber;
96
+ feed: z.ZodNumber;
97
+ }, "strict", z.ZodTypeAny, {
98
+ search: number;
99
+ seo: number;
100
+ sitemap: number;
101
+ feed: number;
102
+ }, {
103
+ search: number;
104
+ seo: number;
105
+ sitemap: number;
106
+ feed: number;
107
+ }>;
108
+ probes: z.ZodArray<z.ZodObject<{
109
+ /** What was probed, e.g. `html` or `markdown`. */
110
+ kind: z.ZodEnum<["html", "markdown"]>;
111
+ url: z.ZodString;
112
+ status: z.ZodNumber;
113
+ /** SHA-256 of the exact bytes observed, so Foundry can compare. */
114
+ digest: z.ZodString;
115
+ }, "strict", z.ZodTypeAny, {
116
+ status: number;
117
+ kind: "html" | "markdown";
118
+ url: string;
119
+ digest: string;
120
+ }, {
121
+ status: number;
122
+ kind: "html" | "markdown";
123
+ url: string;
124
+ digest: string;
125
+ }>, "many">;
126
+ /** How to undo this publication. */
127
+ rollback: z.ZodObject<{
128
+ pointer: z.ZodString;
129
+ }, "strict", z.ZodTypeAny, {
130
+ pointer: string;
131
+ }, {
132
+ pointer: string;
133
+ }>;
134
+ /** Replay identity. Supplied by the caller and persisted, never generated here. */
135
+ nonce: z.ZodString;
136
+ issuedAt: z.ZodString;
137
+ /** Present only when `outcome` is `failure`, and bounded. */
138
+ failureReason: z.ZodOptional<z.ZodString>;
139
+ }, "strict", z.ZodTypeAny, {
140
+ rollback: {
141
+ pointer: string;
142
+ };
143
+ source: {
144
+ commit: string;
145
+ foundryHandoffId: string;
146
+ foundryRunId: string;
147
+ };
148
+ content: {
149
+ slug: string;
150
+ revisionId: string;
151
+ stableId: string;
152
+ };
153
+ audience: "foundry:site-publication-receipt";
154
+ schema: "site.publication.receipt.v1";
155
+ outcome: "success" | "failure";
156
+ property: {
157
+ id: string;
158
+ origin: string;
159
+ };
160
+ database: {
161
+ logicalId: string;
162
+ };
163
+ publicMediaManifestHash: string;
164
+ projections: {
165
+ search: number;
166
+ seo: number;
167
+ sitemap: number;
168
+ feed: number;
169
+ };
170
+ probes: {
171
+ status: number;
172
+ kind: "html" | "markdown";
173
+ url: string;
174
+ digest: string;
175
+ }[];
176
+ nonce: string;
177
+ issuedAt: string;
178
+ failureReason?: string | undefined;
179
+ }, {
180
+ rollback: {
181
+ pointer: string;
182
+ };
183
+ source: {
184
+ commit: string;
185
+ foundryHandoffId: string;
186
+ foundryRunId: string;
187
+ };
188
+ content: {
189
+ slug: string;
190
+ revisionId: string;
191
+ stableId: string;
192
+ };
193
+ audience: "foundry:site-publication-receipt";
194
+ schema: "site.publication.receipt.v1";
195
+ outcome: "success" | "failure";
196
+ property: {
197
+ id: string;
198
+ origin: string;
199
+ };
200
+ database: {
201
+ logicalId: string;
202
+ };
203
+ publicMediaManifestHash: string;
204
+ projections: {
205
+ search: number;
206
+ seo: number;
207
+ sitemap: number;
208
+ feed: number;
209
+ };
210
+ probes: {
211
+ status: number;
212
+ kind: "html" | "markdown";
213
+ url: string;
214
+ digest: string;
215
+ }[];
216
+ nonce: string;
217
+ issuedAt: string;
218
+ failureReason?: string | undefined;
219
+ }>, {
220
+ rollback: {
221
+ pointer: string;
222
+ };
223
+ source: {
224
+ commit: string;
225
+ foundryHandoffId: string;
226
+ foundryRunId: string;
227
+ };
228
+ content: {
229
+ slug: string;
230
+ revisionId: string;
231
+ stableId: string;
232
+ };
233
+ audience: "foundry:site-publication-receipt";
234
+ schema: "site.publication.receipt.v1";
235
+ outcome: "success" | "failure";
236
+ property: {
237
+ id: string;
238
+ origin: string;
239
+ };
240
+ database: {
241
+ logicalId: string;
242
+ };
243
+ publicMediaManifestHash: string;
244
+ projections: {
245
+ search: number;
246
+ seo: number;
247
+ sitemap: number;
248
+ feed: number;
249
+ };
250
+ probes: {
251
+ status: number;
252
+ kind: "html" | "markdown";
253
+ url: string;
254
+ digest: string;
255
+ }[];
256
+ nonce: string;
257
+ issuedAt: string;
258
+ failureReason?: string | undefined;
259
+ }, {
260
+ rollback: {
261
+ pointer: string;
262
+ };
263
+ source: {
264
+ commit: string;
265
+ foundryHandoffId: string;
266
+ foundryRunId: string;
267
+ };
268
+ content: {
269
+ slug: string;
270
+ revisionId: string;
271
+ stableId: string;
272
+ };
273
+ audience: "foundry:site-publication-receipt";
274
+ schema: "site.publication.receipt.v1";
275
+ outcome: "success" | "failure";
276
+ property: {
277
+ id: string;
278
+ origin: string;
279
+ };
280
+ database: {
281
+ logicalId: string;
282
+ };
283
+ publicMediaManifestHash: string;
284
+ projections: {
285
+ search: number;
286
+ seo: number;
287
+ sitemap: number;
288
+ feed: number;
289
+ };
290
+ probes: {
291
+ status: number;
292
+ kind: "html" | "markdown";
293
+ url: string;
294
+ digest: string;
295
+ }[];
296
+ nonce: string;
297
+ issuedAt: string;
298
+ failureReason?: string | undefined;
299
+ }>;
300
+ export type SitePublicationReceiptBody = z.infer<typeof sitePublicationReceiptBodySchema>;
301
+ export declare class PublicationReceiptError extends Error {
302
+ readonly code: 'invalid_body' | 'invalid_key';
303
+ constructor(code: PublicationReceiptError['code'], message: string);
304
+ }
305
+ /**
306
+ * Validate a receipt body and return its canonical RFC 8785 form.
307
+ *
308
+ * Validation happens before canonicalization on purpose: signing an unvalidated
309
+ * body would produce a verifiable signature over a malformed claim, which is
310
+ * strictly worse than refusing, because downstream would trust it.
311
+ */
312
+ export declare function canonicalizeReceiptBody(body: unknown): {
313
+ body: SitePublicationReceiptBody;
314
+ canonical: string;
315
+ };
316
+ //# sourceMappingURL=receipt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt.d.ts","sourceRoot":"","sources":["../../../src/engine/publication-receipt/receipt.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,eAAO,MAAM,+BAA+B,EAAG,6BAAsC,CAAA;AACrF,eAAO,MAAM,iCAAiC,EAAG,kCAA2C,CAAA;AAoB5F,eAAO,MAAM,sBAAsB;IAEjC,kDAAkD;;;;IAIlD,mEAAmE;;;;;;;;;;;;EAG3D,CAAA;AAEV,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;EAOpC,CAAA;AAEV,eAAO,MAAM,gCAAgC;;;IAK3C,6EAA6E;;IAG7E,sDAAsD;;;;;;;;;;;IAGtD,gEAAgE;;;;;;;;;;;;;;IAShE,wBAAwB;;;;;;;;;;;;;;IASxB,2EAA2E;;;;;;;;IAG3E,+DAA+D;;;;;;;;;;;;;;;;;;;QAlD/D,kDAAkD;;;;QAIlD,mEAAmE;;;;;;;;;;;;;IAoDnE,oCAAoC;;;;;;;;IAGpC,mFAAmF;;;IAInF,6DAA6D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB5D,CAAA;AAEH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAA;AAEzF,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,aAAa,CAAA;gBAEjC,IAAI,EAAE,uBAAuB,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM;CAKlE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG;IACvD,IAAI,EAAE,0BAA0B,CAAA;IAChC,SAAS,EAAE,MAAM,CAAA;CACjB,CAcA"}