@glw907/cairn-cms 0.68.0 → 0.76.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 (177) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/ambient.d.ts +2 -0
  3. package/dist/components/CairnAdmin.svelte.d.ts +2 -7
  4. package/dist/components/ComponentForm.svelte +44 -27
  5. package/dist/components/ComponentInsertDialog.svelte +5 -5
  6. package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
  7. package/dist/components/EditPage.svelte +29 -107
  8. package/dist/components/EditPage.svelte.d.ts +2 -7
  9. package/dist/components/EntryPicker.svelte +117 -0
  10. package/dist/components/EntryPicker.svelte.d.ts +35 -0
  11. package/dist/components/FieldInput.svelte +218 -0
  12. package/dist/components/FieldInput.svelte.d.ts +51 -0
  13. package/dist/components/IconPicker.svelte +2 -2
  14. package/dist/components/IconPicker.svelte.d.ts +2 -0
  15. package/dist/components/LinkPicker.svelte +8 -75
  16. package/dist/components/LinkPicker.svelte.d.ts +4 -5
  17. package/dist/components/MediaHeroField.svelte +8 -5
  18. package/dist/components/MediaHeroField.svelte.d.ts +4 -0
  19. package/dist/components/ObjectGroupField.svelte +54 -0
  20. package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
  21. package/dist/components/ReferenceField.svelte +94 -0
  22. package/dist/components/ReferenceField.svelte.d.ts +27 -0
  23. package/dist/components/RepeatableField.svelte +221 -0
  24. package/dist/components/RepeatableField.svelte.d.ts +53 -0
  25. package/dist/components/cairn-admin.css +4 -0
  26. package/dist/components/preview-doc.js +5 -1
  27. package/dist/components/tidy-validate.js +1 -1
  28. package/dist/content/adapter.js +18 -0
  29. package/dist/content/advisories.d.ts +2 -2
  30. package/dist/content/advisories.js +3 -5
  31. package/dist/content/compose.d.ts +7 -6
  32. package/dist/content/compose.js +26 -20
  33. package/dist/content/concepts.d.ts +21 -15
  34. package/dist/content/concepts.js +55 -32
  35. package/dist/content/field-rules.js +3 -4
  36. package/dist/content/fields.d.ts +49 -1
  37. package/dist/content/fields.js +11 -0
  38. package/dist/content/fieldset.d.ts +31 -10
  39. package/dist/content/fieldset.js +262 -109
  40. package/dist/content/frontmatter-region.d.ts +38 -0
  41. package/dist/content/frontmatter-region.js +75 -0
  42. package/dist/content/frontmatter.d.ts +35 -2
  43. package/dist/content/frontmatter.js +232 -11
  44. package/dist/content/manifest.d.ts +34 -0
  45. package/dist/content/manifest.js +80 -4
  46. package/dist/content/media-refs.d.ts +2 -2
  47. package/dist/content/media-rewrite.js +1 -69
  48. package/dist/content/reference-index.d.ts +56 -0
  49. package/dist/content/reference-index.js +95 -0
  50. package/dist/content/references.d.ts +40 -0
  51. package/dist/content/references.js +0 -0
  52. package/dist/content/standard-schema.d.ts +30 -0
  53. package/dist/content/standard-schema.js +4 -0
  54. package/dist/content/types.d.ts +127 -178
  55. package/dist/delivery/data.d.ts +2 -2
  56. package/dist/delivery/data.js +1 -1
  57. package/dist/delivery/public-routes.d.ts +2 -5
  58. package/dist/delivery/public-routes.js +15 -1
  59. package/dist/delivery/site-descriptors.d.ts +5 -1
  60. package/dist/delivery/site-descriptors.js +8 -3
  61. package/dist/delivery/site-indexes.d.ts +2 -2
  62. package/dist/delivery/site-resolver.d.ts +25 -0
  63. package/dist/delivery/site-resolver.js +49 -0
  64. package/dist/doctor/checks-local.js +6 -11
  65. package/dist/github/backend.d.ts +83 -0
  66. package/dist/github/backend.js +76 -0
  67. package/dist/github/credentials.d.ts +11 -5
  68. package/dist/github/credentials.js +3 -3
  69. package/dist/github/repo.d.ts +8 -19
  70. package/dist/github/repo.js +69 -80
  71. package/dist/github/types.d.ts +1 -1
  72. package/dist/github/types.js +4 -4
  73. package/dist/index.d.ts +16 -12
  74. package/dist/index.js +7 -8
  75. package/dist/islands/index.d.ts +12 -0
  76. package/dist/islands/index.js +83 -0
  77. package/dist/islands/types.d.ts +7 -0
  78. package/dist/islands/types.js +1 -0
  79. package/dist/media/rewrite-plan.d.ts +2 -3
  80. package/dist/media/rewrite-plan.js +2 -3
  81. package/dist/media/usage.d.ts +2 -2
  82. package/dist/media/usage.js +3 -5
  83. package/dist/nav/site-config.d.ts +0 -6
  84. package/dist/nav/site-config.js +6 -4
  85. package/dist/render/component-grammar.js +11 -11
  86. package/dist/render/component-reference.js +5 -3
  87. package/dist/render/component-validate.d.ts +4 -1
  88. package/dist/render/component-validate.js +10 -35
  89. package/dist/render/pipeline.d.ts +0 -6
  90. package/dist/render/pipeline.js +1 -1
  91. package/dist/render/registry.d.ts +34 -34
  92. package/dist/render/registry.js +26 -5
  93. package/dist/render/rehype-dispatch.d.ts +4 -4
  94. package/dist/render/rehype-dispatch.js +36 -11
  95. package/dist/render/remark-directives.js +4 -5
  96. package/dist/render/sanitize-schema.js +1 -1
  97. package/dist/sveltekit/cairn-admin.d.ts +5 -5
  98. package/dist/sveltekit/cairn-admin.js +3 -4
  99. package/dist/sveltekit/content-routes.d.ts +10 -8
  100. package/dist/sveltekit/content-routes.js +269 -181
  101. package/dist/sveltekit/health.d.ts +7 -3
  102. package/dist/sveltekit/health.js +9 -3
  103. package/dist/sveltekit/index.d.ts +1 -1
  104. package/dist/sveltekit/nav-routes.d.ts +6 -5
  105. package/dist/sveltekit/nav-routes.js +22 -20
  106. package/dist/sveltekit/types.d.ts +2 -0
  107. package/dist/vite/index.d.ts +3 -3
  108. package/dist/vite/index.js +17 -8
  109. package/package.json +5 -1
  110. package/src/lib/ambient.ts +7 -0
  111. package/src/lib/components/CairnAdmin.svelte +2 -6
  112. package/src/lib/components/ComponentForm.svelte +48 -27
  113. package/src/lib/components/ComponentInsertDialog.svelte +9 -8
  114. package/src/lib/components/EditPage.svelte +43 -119
  115. package/src/lib/components/EntryPicker.svelte +154 -0
  116. package/src/lib/components/FieldInput.svelte +262 -0
  117. package/src/lib/components/IconPicker.svelte +4 -2
  118. package/src/lib/components/LinkPicker.svelte +10 -81
  119. package/src/lib/components/MediaHeroField.svelte +12 -5
  120. package/src/lib/components/ObjectGroupField.svelte +97 -0
  121. package/src/lib/components/ReferenceField.svelte +126 -0
  122. package/src/lib/components/RepeatableField.svelte +310 -0
  123. package/src/lib/components/preview-doc.ts +5 -1
  124. package/src/lib/components/tidy-validate.ts +1 -1
  125. package/src/lib/content/adapter.ts +21 -0
  126. package/src/lib/content/advisories.ts +4 -7
  127. package/src/lib/content/compose.ts +30 -23
  128. package/src/lib/content/concepts.ts +68 -40
  129. package/src/lib/content/field-rules.ts +3 -4
  130. package/src/lib/content/fields.ts +52 -1
  131. package/src/lib/content/fieldset.ts +291 -128
  132. package/src/lib/content/frontmatter-region.ts +90 -0
  133. package/src/lib/content/frontmatter.ts +231 -15
  134. package/src/lib/content/manifest.ts +101 -4
  135. package/src/lib/content/media-refs.ts +2 -2
  136. package/src/lib/content/media-rewrite.ts +7 -80
  137. package/src/lib/content/reference-index.ts +159 -0
  138. package/src/lib/content/references.ts +0 -0
  139. package/src/lib/content/standard-schema.ts +25 -0
  140. package/src/lib/content/types.ts +128 -195
  141. package/src/lib/delivery/data.ts +2 -2
  142. package/src/lib/delivery/public-routes.ts +17 -3
  143. package/src/lib/delivery/site-descriptors.ts +8 -3
  144. package/src/lib/delivery/site-indexes.ts +2 -2
  145. package/src/lib/delivery/site-resolver.ts +64 -0
  146. package/src/lib/doctor/checks-local.ts +6 -14
  147. package/src/lib/github/backend.ts +161 -0
  148. package/src/lib/github/credentials.ts +10 -7
  149. package/src/lib/github/repo.ts +79 -83
  150. package/src/lib/github/types.ts +5 -5
  151. package/src/lib/index.ts +38 -23
  152. package/src/lib/islands/index.ts +84 -0
  153. package/src/lib/islands/types.ts +11 -0
  154. package/src/lib/media/rewrite-plan.ts +4 -6
  155. package/src/lib/media/usage.ts +4 -7
  156. package/src/lib/nav/site-config.ts +8 -9
  157. package/src/lib/render/component-grammar.ts +10 -10
  158. package/src/lib/render/component-reference.ts +4 -3
  159. package/src/lib/render/component-validate.ts +10 -35
  160. package/src/lib/render/pipeline.ts +1 -7
  161. package/src/lib/render/registry.ts +58 -39
  162. package/src/lib/render/rehype-dispatch.ts +45 -10
  163. package/src/lib/render/remark-directives.ts +4 -5
  164. package/src/lib/render/sanitize-schema.ts +1 -1
  165. package/src/lib/sveltekit/cairn-admin.ts +8 -9
  166. package/src/lib/sveltekit/content-routes.ts +330 -221
  167. package/src/lib/sveltekit/health.ts +13 -6
  168. package/src/lib/sveltekit/index.ts +2 -2
  169. package/src/lib/sveltekit/nav-routes.ts +33 -29
  170. package/src/lib/sveltekit/types.ts +5 -1
  171. package/src/lib/vite/index.ts +20 -11
  172. package/dist/content/schema.d.ts +0 -87
  173. package/dist/content/schema.js +0 -85
  174. package/dist/content/validate.d.ts +0 -17
  175. package/dist/content/validate.js +0 -93
  176. package/src/lib/content/schema.ts +0 -163
  177. package/src/lib/content/validate.ts +0 -90
@@ -29,10 +29,10 @@ export declare function headRow(title: ElementContent[], icon?: Element, level?:
29
29
  export declare function markFirstList(children: ElementContent[]): Element | undefined;
30
30
  /**
31
31
  * Rehype transformer: dispatch each stamped element through its registry `build`
32
- * fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
33
- * carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
34
- * to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
32
+ * fn. Each top-level primitive gets a `data-rise` attribute carrying its
33
+ * document-order index (0, 1, 2, …); the site's CSS maps that ordinal to an
34
+ * entrance delay. The index is inert, so a consumer's sanitize floor can keep
35
35
  * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
36
36
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched.
37
37
  */
38
- export declare function rehypeDispatch(registry: ComponentRegistry, stagger?: boolean): (tree: Root) => void;
38
+ export declare function rehypeDispatch(registry: ComponentRegistry): (tree: Root) => void;
@@ -56,7 +56,7 @@ export function markFirstList(children) {
56
56
  }
57
57
  // Recurse into a node's children, transforming any nested primitive sections
58
58
  // (a grid inside a card, panels inside a split). Nested primitives never carry the
59
- // entrance stagger; only top-level ones do (stamped in the transformer below).
59
+ // entrance ordinal; only top-level ones do (stamped in the transformer below).
60
60
  function transformChildren(children, registry) {
61
61
  return children.map((c) => {
62
62
  if (isElement(c) && c.properties?.dataPrimitive)
@@ -70,11 +70,11 @@ function transformChildren(children, registry) {
70
70
  // 'true'/'false'; everything else is the literal string the author wrote.
71
71
  function readAttributes(node, def) {
72
72
  const out = {};
73
- for (const field of def.attributes ?? []) {
74
- const value = strProp(node, dataAttrProp(field.key));
73
+ for (const [name, field] of Object.entries(def.attributes ?? {})) {
74
+ const value = strProp(node, dataAttrProp(name));
75
75
  if (value == null)
76
76
  continue;
77
- out[field.key] = field.type === 'boolean' ? value === 'true' : value;
77
+ out[name] = field.type === 'boolean' ? value === 'true' : value;
78
78
  }
79
79
  return out;
80
80
  }
@@ -126,6 +126,31 @@ function partitionSlots(node) {
126
126
  },
127
127
  };
128
128
  }
129
+ // Serialize a hydrate component's declared attributes into the island prop payload. A `number` field is
130
+ // coerced from its stamped string to a JSON number here; a `boolean` already arrived as a real boolean
131
+ // from readAttributes (which coerces 'true'/'false' upstream), and every other field stays the literal
132
+ // string the author wrote. The result is JSON.stringify-ed into data-cairn-props and parsed on the client.
133
+ function serializeIslandProps(def, attributes) {
134
+ const out = {};
135
+ for (const [key, value] of Object.entries(attributes)) {
136
+ const type = def.attributes?.[key]?.type;
137
+ out[key] = type === 'number' && typeof value === 'string' ? Number(value) : value;
138
+ }
139
+ return out;
140
+ }
141
+ // Wrap a hydrate component's static fallback in its island boundary. The boundary carries the directive
142
+ // name and the JSON prop payload; a 'visible' island also carries data-cairn-hydrate="visible". The
143
+ // boundary attributes are inert data-* and survive both the sanitize floor (this runs after it) and the
144
+ // sink guard (which strips only style/on*). The fallback is build()'s no-JS, first-paint representation.
145
+ function islandBoundary(name, def, attributes, fallback) {
146
+ const properties = {
147
+ dataCairnIsland: name,
148
+ dataCairnProps: JSON.stringify(serializeIslandProps(def, attributes)),
149
+ };
150
+ if (def.hydrate === 'visible')
151
+ properties.dataCairnHydrate = 'visible';
152
+ return { type: 'element', tagName: 'div', properties, children: [fallback] };
153
+ }
129
154
  function transformNode(node, registry) {
130
155
  node.children = transformChildren(node.children, registry);
131
156
  const name = strProp(node, 'dataPrimitive');
@@ -139,24 +164,24 @@ function transformNode(node, registry) {
139
164
  items: parts.items,
140
165
  node,
141
166
  };
142
- return def.build(ctx);
167
+ const built = def.build(ctx);
168
+ return def.hydrate ? islandBoundary(name, def, ctx.attributes, built) : built;
143
169
  }
144
170
  /**
145
171
  * Rehype transformer: dispatch each stamped element through its registry `build`
146
- * fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
147
- * carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
148
- * to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
172
+ * fn. Each top-level primitive gets a `data-rise` attribute carrying its
173
+ * document-order index (0, 1, 2, …); the site's CSS maps that ordinal to an
174
+ * entrance delay. The index is inert, so a consumer's sanitize floor can keep
149
175
  * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
150
176
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched.
151
177
  */
152
- export function rehypeDispatch(registry, stagger) {
178
+ export function rehypeDispatch(registry) {
153
179
  return (tree) => {
154
180
  let idx = 0;
155
181
  tree.children = tree.children.map((child) => {
156
182
  if (isElement(child) && child.properties?.dataPrimitive) {
157
183
  const el = transformNode(child, registry);
158
- if (stagger)
159
- el.properties = { ...el.properties, dataRise: String(idx++) };
184
+ el.properties = { ...el.properties, dataRise: String(idx++) };
160
185
  return el;
161
186
  }
162
187
  if (isElement(child))
@@ -63,8 +63,7 @@ export function remarkDirectiveStamp(registry) {
63
63
  const def = registry.get(node.name);
64
64
  const attrs = node.attributes ?? {};
65
65
  const role = attrs.role || undefined;
66
- const iconField = registry.iconField(node.name);
67
- const iconKey = iconField?.key ?? 'icon';
66
+ const iconKey = registry.iconField(node.name) ?? 'icon';
68
67
  let icon = attrs[iconKey] || undefined;
69
68
  if (!icon && role)
70
69
  icon = registry.defaultIcon(node.name, role);
@@ -78,10 +77,10 @@ export function remarkDirectiveStamp(registry) {
78
77
  // back to that default the same way a missing one does. data-attr-<key> survives to the
79
78
  // element; build() consumes it and returns a fresh element, so the marker never reaches the
80
79
  // published DOM.
81
- for (const field of def?.attributes ?? []) {
82
- const raw = field === iconField ? icon : attrs[field.key];
80
+ for (const name of Object.keys(def?.attributes ?? {})) {
81
+ const raw = name === iconKey ? icon : attrs[name];
83
82
  if (raw != null)
84
- properties[dataAttrProp(field.key)] = raw;
83
+ properties[dataAttrProp(name)] = raw;
85
84
  }
86
85
  const data = node.data ?? (node.data = {});
87
86
  data.hName = 'div';
@@ -17,7 +17,7 @@ const FIXED_MARKERS = ['dataPrimitive', 'dataSlot', 'dataRole', 'dataRise'];
17
17
  * allowlist but not weaken the core strip.
18
18
  */
19
19
  export function buildSanitizeSchema(registry, extend) {
20
- const attrMarkers = registry.defs.flatMap((d) => (d.attributes ?? []).map((a) => dataAttrProp(a.key)));
20
+ const attrMarkers = registry.defs.flatMap((d) => Object.keys(d.attributes ?? {}).map((key) => dataAttrProp(key)));
21
21
  const markers = [...FIXED_MARKERS, ...attrMarkers];
22
22
  const attributes = defaultSchema.attributes ?? {};
23
23
  // defaultSchema's `a` entry carries a className tuple (`['className', 'data-footnote-backref']`)
@@ -2,7 +2,7 @@ import { type ContentRoutesDeps, type LayoutData, type ListData, type EditData,
2
2
  import { type NavLoadData } from './nav-routes.js';
3
3
  import type { AuthBranding, SendMagicLink } from '../email.js';
4
4
  import type { AuthEnv, Editor } from '../auth/types.js';
5
- import type { GithubKeyEnv } from '../github/credentials.js';
5
+ import type { BackendEnv } from '../github/credentials.js';
6
6
  import type { CairnRuntime } from '../content/types.js';
7
7
  import type { CookieJar, EventBase } from './types.js';
8
8
  /**
@@ -10,19 +10,19 @@ import type { CookieJar, EventBase } from './types.js';
10
10
  * (ContentEvent minus params, which the dispatcher synthesizes, plus RequestContext's cookies
11
11
  * and setHeaders). A real SvelteKit RequestEvent satisfies it.
12
12
  */
13
- export interface AdminEvent extends EventBase<GithubKeyEnv & AuthEnv> {
13
+ export interface AdminEvent extends EventBase<BackendEnv & AuthEnv> {
14
14
  cookies: CookieJar;
15
15
  setHeaders(headers: Record<string, string>): void;
16
16
  }
17
17
  /**
18
18
  * Injectable dependencies. Branding defaults from the runtime's siteName and sender, so a
19
- * site overrides it only to change the magic-link email identity; `send` and `mintToken`
20
- * are the same seams the underlying factories take.
19
+ * site overrides it only to change the magic-link email identity; `send` is the same seam the
20
+ * underlying auth factory takes. The content backend rides `event.locals.backend` (the dev double)
21
+ * or the adapter's provider, so it is not a dep here.
21
22
  */
22
23
  export interface CairnAdminDeps {
23
24
  branding?: AuthBranding;
24
25
  send?: SendMagicLink;
25
- mintToken?: ContentRoutesDeps['mintToken'];
26
26
  /**
27
27
  * Build the Anthropic client for the tidy action. Forwarded to the content routes; a site that
28
28
  * enables tidy injects a stub here to avoid a real network call. Defaults to the real SDK client.
@@ -23,13 +23,12 @@ export function createCairnAdmin(runtime, deps = {}) {
23
23
  };
24
24
  const auth = createAuthRoutes({ branding, send: deps.send });
25
25
  const content = createContentRoutes(runtime, {
26
- mintToken: deps.mintToken,
27
26
  anthropic: deps.anthropic,
28
27
  tidyTimeoutMs: deps.tidyTimeoutMs,
29
28
  });
30
29
  const editors = createEditorRoutes();
31
30
  // The nav surface exists only when the site configures a menu; without one its view is a 404.
32
- const nav = runtime.navMenu ? createNavRoutes(runtime, { mintToken: deps.mintToken }) : null;
31
+ const nav = runtime.navMenu ? createNavRoutes(runtime) : null;
33
32
  /**
34
33
  * Build the event a wrapped content load reads. The catch-all route carries only a rest
35
34
  * param, so `concept` and `id` are synthesized from the parsed view. The override names
@@ -49,8 +48,8 @@ export function createCairnAdmin(runtime, deps = {}) {
49
48
  }
50
49
  /**
51
50
  * Serve the admin view the pathname names, or a 404 for any shape the parser refuses.
52
- * The authed views run the layout load and the view load concurrently; both mint a GitHub
53
- * token, and the installation-token cache coalesces the mints into one signing.
51
+ * The authed views run the layout load and the view load concurrently; both resolve the same
52
+ * backend, and the installation-token cache coalesces their lazy mints into one signing.
54
53
  */
55
54
  async function load(event) {
56
55
  const view = parseAdminPath(event.url.pathname, runtime.concepts);
@@ -1,6 +1,7 @@
1
1
  import { fail } from '@sveltejs/kit';
2
2
  import { type AdvisoryNotice } from '../content/advisories.js';
3
- import { type GithubKeyEnv } from '../github/credentials.js';
3
+ import type { BackendEnv } from '../github/credentials.js';
4
+ import type { Backend } from '../github/backend.js';
4
5
  import { type LinkTarget, type InboundLink } from '../content/manifest.js';
5
6
  import { type GettingStarted } from '../content/getting-started.js';
6
7
  import { type MarkdownReferenceRow } from '../components/markdown-reference.js';
@@ -13,7 +14,7 @@ import type { RepointPlacement, AltPlacement } from '../content/media-rewrite.js
13
14
  import type { BranchRef } from '../media/rewrite-plan.js';
14
15
  import type { BulkDeleteSkip } from '../media/bulk-delete-plan.js';
15
16
  import type { CookieJar, EventBase } from './types.js';
16
- import type { CairnRuntime, FrontmatterField, ResolvedPreview } from '../content/types.js';
17
+ import type { CairnRuntime, NamedField, ResolvedPreview } from '../content/types.js';
17
18
  import type { Role } from '../auth/types.js';
18
19
  export type { AdvisoryNotice, AdvisoryAction } from '../content/advisories.js';
19
20
  /** A sidebar concept entry: just enough to render the nav without shipping validators to the client. */
@@ -90,7 +91,7 @@ export interface EditData {
90
91
  conceptId: string;
91
92
  id: string;
92
93
  label: string;
93
- fields: FrontmatterField[];
94
+ fields: NamedField[];
94
95
  frontmatter: Record<string, unknown>;
95
96
  body: string;
96
97
  title: string;
@@ -259,7 +260,7 @@ export interface HelpData {
259
260
  supportContact?: string;
260
261
  }
261
262
  /** The structural event the content routes read; a real SvelteKit RequestEvent satisfies it. */
262
- export interface ContentEvent extends EventBase<GithubKeyEnv> {
263
+ export interface ContentEvent extends EventBase<BackendEnv> {
263
264
  params: Record<string, string>;
264
265
  /**
265
266
  * SvelteKit's cookie jar. The layout load reads the persisted admin theme and issues the CSRF
@@ -303,10 +304,12 @@ export interface TidyClient {
303
304
  }
304
305
  export interface ContentRoutesDeps {
305
306
  /**
306
- * Mint a GitHub App installation token from the Worker env. Defaults to the real signer.
307
- * A bare string works too; the routes await whatever comes back.
307
+ * Override the resolved content backend. A test injects a live `Backend` (a `makeGithubBackend`
308
+ * over a fetch double, or an in-memory fake) so the read and commit paths run with no real token
309
+ * mint. When set it replaces the per-handler `locals.backend ?? runtime.backend.connect(env)`
310
+ * resolve; a production caller leaves it unset and the dev double rides `event.locals.backend`.
308
311
  */
309
- mintToken?: (env: GithubKeyEnv) => string | Promise<string>;
312
+ backend?: Backend;
310
313
  /**
311
314
  * Build the Anthropic client for the tidy action from the resolved API key. Defaults to the real
312
315
  * SDK client. Injected in tests so `messages.create` is stubbed and no network call (or real key)
@@ -579,5 +582,4 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
579
582
  mediaAltApply: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
580
583
  addDictionaryWord: (event: ContentEvent) => Promise<ReturnType<typeof fail> | DictionaryAddResult>;
581
584
  tidyAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | TidyResult>;
582
- mintToken: (env: GithubKeyEnv) => string | Promise<string>;
583
585
  };