@krantho/server 0.0.0-stage → 1.0.1

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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cdn/cloudfront.d.ts +9 -0
  3. package/dist/cdn/cloudfront.d.ts.map +1 -0
  4. package/dist/cdn/cloudfront.js +35 -0
  5. package/dist/cdn/cloudfront.js.map +1 -0
  6. package/dist/cdn/gcp.d.ts +15 -0
  7. package/dist/cdn/gcp.d.ts.map +1 -0
  8. package/dist/cdn/gcp.js +60 -0
  9. package/dist/cdn/gcp.js.map +1 -0
  10. package/dist/cdn/index.d.ts +9 -0
  11. package/dist/cdn/index.d.ts.map +1 -0
  12. package/dist/cdn/index.js +30 -0
  13. package/dist/cdn/index.js.map +1 -0
  14. package/dist/cdn/listener.d.ts +14 -0
  15. package/dist/cdn/listener.d.ts.map +1 -0
  16. package/dist/cdn/listener.js +53 -0
  17. package/dist/cdn/listener.js.map +1 -0
  18. package/dist/cdn/retry.d.ts +3 -0
  19. package/dist/cdn/retry.d.ts.map +1 -0
  20. package/dist/cdn/retry.js +17 -0
  21. package/dist/cdn/retry.js.map +1 -0
  22. package/dist/cdn/types.d.ts +12 -0
  23. package/dist/cdn/types.d.ts.map +1 -0
  24. package/dist/cdn/types.js +2 -0
  25. package/dist/cdn/types.js.map +1 -0
  26. package/dist/cdn/vercel.d.ts +9 -0
  27. package/dist/cdn/vercel.d.ts.map +1 -0
  28. package/dist/cdn/vercel.js +31 -0
  29. package/dist/cdn/vercel.js.map +1 -0
  30. package/dist/content-conflicts.d.ts +18 -0
  31. package/dist/content-conflicts.d.ts.map +1 -0
  32. package/dist/content-conflicts.js +22 -0
  33. package/dist/content-conflicts.js.map +1 -0
  34. package/dist/content-dates.d.ts +35 -0
  35. package/dist/content-dates.d.ts.map +1 -0
  36. package/dist/content-dates.js +85 -0
  37. package/dist/content-dates.js.map +1 -0
  38. package/dist/content-loader.d.ts +37 -0
  39. package/dist/content-loader.d.ts.map +1 -0
  40. package/dist/content-loader.js +223 -0
  41. package/dist/content-loader.js.map +1 -0
  42. package/dist/content-migration.d.ts +101 -0
  43. package/dist/content-migration.d.ts.map +1 -0
  44. package/dist/content-migration.js +291 -0
  45. package/dist/content-migration.js.map +1 -0
  46. package/dist/convert.d.ts +18 -0
  47. package/dist/convert.d.ts.map +1 -0
  48. package/dist/convert.js +152 -0
  49. package/dist/convert.js.map +1 -0
  50. package/dist/file-store.d.ts +11 -0
  51. package/dist/file-store.d.ts.map +1 -0
  52. package/dist/file-store.js +39 -0
  53. package/dist/file-store.js.map +1 -0
  54. package/dist/file-watcher.d.ts +19 -0
  55. package/dist/file-watcher.d.ts.map +1 -0
  56. package/dist/file-watcher.js +99 -0
  57. package/dist/file-watcher.js.map +1 -0
  58. package/dist/handler.d.ts +61 -0
  59. package/dist/handler.d.ts.map +1 -0
  60. package/dist/handler.js +197 -0
  61. package/dist/handler.js.map +1 -0
  62. package/dist/import.d.ts +25 -0
  63. package/dist/import.d.ts.map +1 -0
  64. package/dist/import.js +71 -0
  65. package/dist/import.js.map +1 -0
  66. package/dist/index.d.ts +28 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +19 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/indexnow.d.ts +38 -0
  71. package/dist/indexnow.d.ts.map +1 -0
  72. package/dist/indexnow.js +89 -0
  73. package/dist/indexnow.js.map +1 -0
  74. package/dist/lazy-handler.d.ts +7 -0
  75. package/dist/lazy-handler.d.ts.map +1 -0
  76. package/dist/lazy-handler.js +16 -0
  77. package/dist/lazy-handler.js.map +1 -0
  78. package/dist/llm-seed.d.ts +25 -0
  79. package/dist/llm-seed.d.ts.map +1 -0
  80. package/dist/llm-seed.js +42 -0
  81. package/dist/llm-seed.js.map +1 -0
  82. package/dist/lock-adapters.d.ts +21 -0
  83. package/dist/lock-adapters.d.ts.map +1 -0
  84. package/dist/lock-adapters.js +69 -0
  85. package/dist/lock-adapters.js.map +1 -0
  86. package/dist/manifest.d.ts +15 -0
  87. package/dist/manifest.d.ts.map +1 -0
  88. package/dist/manifest.js +39 -0
  89. package/dist/manifest.js.map +1 -0
  90. package/dist/mime-types.d.ts +3 -0
  91. package/dist/mime-types.d.ts.map +1 -0
  92. package/dist/mime-types.js +15 -0
  93. package/dist/mime-types.js.map +1 -0
  94. package/dist/page-cache-rules.d.ts +41 -0
  95. package/dist/page-cache-rules.d.ts.map +1 -0
  96. package/dist/page-cache-rules.js +94 -0
  97. package/dist/page-cache-rules.js.map +1 -0
  98. package/dist/page-cache-store.d.ts +11 -0
  99. package/dist/page-cache-store.d.ts.map +1 -0
  100. package/dist/page-cache-store.js +46 -0
  101. package/dist/page-cache-store.js.map +1 -0
  102. package/dist/page-request.d.ts +26 -0
  103. package/dist/page-request.d.ts.map +1 -0
  104. package/dist/page-request.js +29 -0
  105. package/dist/page-request.js.map +1 -0
  106. package/dist/persistence.d.ts +36 -0
  107. package/dist/persistence.d.ts.map +1 -0
  108. package/dist/persistence.js +108 -0
  109. package/dist/persistence.js.map +1 -0
  110. package/dist/preview-token.d.ts +9 -0
  111. package/dist/preview-token.d.ts.map +1 -0
  112. package/dist/preview-token.js +14 -0
  113. package/dist/preview-token.js.map +1 -0
  114. package/dist/read-cache.d.ts +17 -0
  115. package/dist/read-cache.d.ts.map +1 -0
  116. package/dist/read-cache.js +33 -0
  117. package/dist/read-cache.js.map +1 -0
  118. package/dist/route-matcher.d.ts +21 -0
  119. package/dist/route-matcher.d.ts.map +1 -0
  120. package/dist/route-matcher.js +75 -0
  121. package/dist/route-matcher.js.map +1 -0
  122. package/dist/secret-vault.d.ts +20 -0
  123. package/dist/secret-vault.d.ts.map +1 -0
  124. package/dist/secret-vault.js +60 -0
  125. package/dist/secret-vault.js.map +1 -0
  126. package/dist/seo-config-io.d.ts +51 -0
  127. package/dist/seo-config-io.d.ts.map +1 -0
  128. package/dist/seo-config-io.js +151 -0
  129. package/dist/seo-config-io.js.map +1 -0
  130. package/dist/seo-seed.d.ts +12 -0
  131. package/dist/seo-seed.d.ts.map +1 -0
  132. package/dist/seo-seed.js +100 -0
  133. package/dist/seo-seed.js.map +1 -0
  134. package/dist/studio-gate.d.ts +25 -0
  135. package/dist/studio-gate.d.ts.map +1 -0
  136. package/dist/studio-gate.js +70 -0
  137. package/dist/studio-gate.js.map +1 -0
  138. package/dist/studio.d.ts +17 -0
  139. package/dist/studio.d.ts.map +1 -0
  140. package/dist/studio.js +110 -0
  141. package/dist/studio.js.map +1 -0
  142. package/dist/webhook-sweep.d.ts +22 -0
  143. package/dist/webhook-sweep.d.ts.map +1 -0
  144. package/dist/webhook-sweep.js +33 -0
  145. package/dist/webhook-sweep.js.map +1 -0
  146. package/package.json +89 -3
  147. package/README.md +0 -3
@@ -0,0 +1,89 @@
1
+ import { documentPath, pageCollection } from '@krantho/core';
2
+ /** The shared IndexNow endpoint; participating engines (Bing among them) share submissions */
3
+ export const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow';
4
+ /** IndexNow accepts at most this many URLs in one submission */
5
+ const MAX_URLS_PER_SUBMISSION = 10_000;
6
+ /**
7
+ * Tell IndexNow that these URLs changed (added, updated or removed). Every URL
8
+ * must be on the site's host. Resolves with the HTTP status of each request
9
+ * when IndexNow accepted them all (200, or 202 while it validates the key),
10
+ * and throws otherwise.
11
+ */
12
+ export async function submitIndexNow(site, urls, fetchFn = fetch) {
13
+ const origin = new URL(site.baseUrl);
14
+ const statuses = [];
15
+ for (let start = 0; start < urls.length; start += MAX_URLS_PER_SUBMISSION) {
16
+ const response = await fetchFn(INDEXNOW_ENDPOINT, {
17
+ method: 'POST',
18
+ headers: { 'content-type': 'application/json; charset=utf-8' },
19
+ body: JSON.stringify({
20
+ host: origin.host,
21
+ key: site.key,
22
+ keyLocation: `${origin.origin}/${site.key}.txt`,
23
+ urlList: urls.slice(start, start + MAX_URLS_PER_SUBMISSION),
24
+ }),
25
+ });
26
+ if (response.status !== 200 && response.status !== 202) {
27
+ throw new Error(`IndexNow rejected ${urls.length} URLs: HTTP ${response.status}`);
28
+ }
29
+ statuses.push(response.status);
30
+ }
31
+ return statuses;
32
+ }
33
+ /** One log line for an accepted submission: how many URLs, IndexNow's answer, and the URLs */
34
+ export function describeSubmission(urls, statuses) {
35
+ const count = urls.length === 1 ? '1 URL' : `${urls.length} URLs`;
36
+ return `IndexNow: submitted ${count}, HTTP ${statuses.join(', ')}\n${urls.map((url) => ` ${url}`).join('\n')}`;
37
+ }
38
+ /**
39
+ * The pages a change touched: a document's page when its published version
40
+ * changed (a draft save touches none), every page in the sitemap after a
41
+ * site-wide change such as the startup import.
42
+ */
43
+ export function changedUrlsFor(baseUrl, collections, listSitemap) {
44
+ const origin = baseUrl.replace(/\/$/, '');
45
+ return async (change) => {
46
+ if (change.kind === 'site')
47
+ return listSitemap();
48
+ if (change.kind !== 'document' || !change.live)
49
+ return [];
50
+ const path = documentPath(pageCollection(collections, change.collection), change.slug);
51
+ return path === null ? [] : [`${origin}${path}`];
52
+ };
53
+ }
54
+ /** Local development serves the configured site from a machine nobody indexes */
55
+ function isLocal(baseUrl) {
56
+ const host = new URL(baseUrl).hostname;
57
+ return (host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host.endsWith('.localhost'));
58
+ }
59
+ /**
60
+ * A change listener that notifies IndexNow of the pages a committed write
61
+ * touched, when `seo.indexNow.key` is set, the site is not on a local host and
62
+ * the server runs in production: a development server configured for the real
63
+ * site would otherwise announce its draft pages under the site's domain.
64
+ * The submission runs alongside the response (`waitUntil` on serverless hosts)
65
+ * and never fails the write; every submission is logged with IndexNow's answer.
66
+ */
67
+ export function indexNowChangeListener(config, nodeEnv, changedUrls, waitUntil, fetchFn = fetch) {
68
+ const key = config.seo.indexNow.key;
69
+ const baseUrl = config.site.baseUrl;
70
+ if (!key || isLocal(baseUrl) || nodeEnv !== 'production')
71
+ return null;
72
+ return (change) => {
73
+ const work = changedUrls(change)
74
+ .then(async (urls) => {
75
+ if (urls.length === 0)
76
+ return;
77
+ const statuses = await submitIndexNow({ baseUrl, key }, urls, fetchFn);
78
+ console.log(`[indexnow] ${describeSubmission(urls, statuses)}`);
79
+ })
80
+ .catch((error) => console.error('[indexnow] notification failed:', error));
81
+ try {
82
+ waitUntil?.(work);
83
+ }
84
+ catch (error) {
85
+ console.error('[indexnow] waitUntil rejected the notification:', error);
86
+ }
87
+ };
88
+ }
89
+ //# sourceMappingURL=indexnow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { KranthoHandler, KranthoHandlerOptions } from './handler.js';
2
+ /**
3
+ * Create a lazy-initialized handler singleton. First call creates, subsequent
4
+ * calls return cached. A failed creation is not cached — the next call retries.
5
+ */
6
+ export declare function createLazyHandler(options: KranthoHandlerOptions): () => Promise<KranthoHandler>;
7
+ //# sourceMappingURL=lazy-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lazy-handler.d.ts","sourceRoot":"","sources":["../src/lazy-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAGzE;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAS/F"}
@@ -0,0 +1,16 @@
1
+ import { createKranthoHandler } from './handler.js';
2
+ /**
3
+ * Create a lazy-initialized handler singleton. First call creates, subsequent
4
+ * calls return cached. A failed creation is not cached — the next call retries.
5
+ */
6
+ export function createLazyHandler(options) {
7
+ let promise = null;
8
+ return () => {
9
+ promise ??= createKranthoHandler(options).catch((err) => {
10
+ promise = null;
11
+ throw err;
12
+ });
13
+ return promise;
14
+ };
15
+ }
16
+ //# sourceMappingURL=lazy-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lazy-handler.js","sourceRoot":"","sources":["../src/lazy-handler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAEnD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA8B;IAC9D,IAAI,OAAO,GAAmC,IAAI,CAAA;IAClD,OAAO,GAAG,EAAE;QACV,OAAO,KAAK,oBAAoB,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YAC/D,OAAO,GAAG,IAAI,CAAA;YACd,MAAM,GAAG,CAAA;QACX,CAAC,CAAC,CAAA;QACF,OAAO,OAAO,CAAA;IAChB,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { KranthoConfig, KranthoConfigInput } from '@krantho/config';
2
+ /** The slice of the site settings store seeding uses */
3
+ interface SettingsStore {
4
+ getSetting(key: string): Promise<string | null>;
5
+ setSetting(key: string, value: string): Promise<void>;
6
+ }
7
+ /**
8
+ * Whether boot should seed the stored LLM settings: the app's code passed an
9
+ * `llm` block, or the environment chose a provider and supplied that provider's
10
+ * own key. Any other `llm.*` variable alone is not enough — a lone credential
11
+ * for a provider nobody chose must not pick one silently, and a provider
12
+ * without a credential would stand in the way of a key supplied later.
13
+ */
14
+ export declare function shouldSeedLlm(code: KranthoConfigInput, config: KranthoConfig): boolean;
15
+ /**
16
+ * Seed LLM config from developer-defined defaults: the provider, model and base URL.
17
+ * Idempotent — a provider is stored only while none is; a key without a chosen
18
+ * provider seeds no provider. Admin overrides via Setup › AI provider are always preserved.
19
+ * No credential is ever stored: the API reads each provider's key, and the
20
+ * subscription token of the claude-code provider, from the configuration per request,
21
+ * so a key the deployment injects stays out of the database.
22
+ */
23
+ export declare function seedLlmConfig(config: KranthoConfig, adapter: SettingsStore): Promise<void>;
24
+ export {};
25
+ //# sourceMappingURL=llm-seed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-seed.d.ts","sourceRoot":"","sources":["../src/llm-seed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGxE,wDAAwD;AACxD,UAAU,aAAa;IACrB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACtD;AAOD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAKtF;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAShG"}
@@ -0,0 +1,42 @@
1
+ import { storedBaseUrlSetting, storedModelSetting } from '@krantho/api';
2
+ import { llmCredentialLeaf } from '@krantho/config';
3
+ /** Whether the operator or the developer chose the provider, rather than the default standing */
4
+ function providerChosen(config) {
5
+ return config.sources.get('llm.provider') !== 'default';
6
+ }
7
+ /**
8
+ * Whether boot should seed the stored LLM settings: the app's code passed an
9
+ * `llm` block, or the environment chose a provider and supplied that provider's
10
+ * own key. Any other `llm.*` variable alone is not enough — a lone credential
11
+ * for a provider nobody chose must not pick one silently, and a provider
12
+ * without a credential would stand in the way of a key supplied later.
13
+ */
14
+ export function shouldSeedLlm(code, config) {
15
+ if (code.llm !== undefined)
16
+ return true;
17
+ if (config.sources.get('llm.provider') !== 'env')
18
+ return false;
19
+ const leaf = llmCredentialLeaf(config.llm, config.llm.provider);
20
+ return leaf?.type === 'api-key' && config.sources.get(leaf.path) === 'env';
21
+ }
22
+ /**
23
+ * Seed LLM config from developer-defined defaults: the provider, model and base URL.
24
+ * Idempotent — a provider is stored only while none is; a key without a chosen
25
+ * provider seeds no provider. Admin overrides via Setup › AI provider are always preserved.
26
+ * No credential is ever stored: the API reads each provider's key, and the
27
+ * subscription token of the claude-code provider, from the configuration per request,
28
+ * so a key the deployment injects stays out of the database.
29
+ */
30
+ export async function seedLlmConfig(config, adapter) {
31
+ if (await adapter.getSetting('llm.provider'))
32
+ return; // Already configured — don't overwrite
33
+ const { llm } = config;
34
+ if (providerChosen(config)) {
35
+ await adapter.setSetting('llm.provider', llm.provider);
36
+ if (llm.model)
37
+ await adapter.setSetting(storedModelSetting(llm.provider), llm.model);
38
+ if (llm.baseUrl)
39
+ await adapter.setSetting(storedBaseUrlSetting(llm.provider), llm.baseUrl);
40
+ }
41
+ }
42
+ //# sourceMappingURL=llm-seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-seed.js","sourceRoot":"","sources":["../src/llm-seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAQnD,iGAAiG;AACjG,SAAS,cAAc,CAAC,MAAqB;IAC3C,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,SAAS,CAAA;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAwB,EAAE,MAAqB;IAC3E,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACvC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,KAAK;QAAE,OAAO,KAAK,CAAA;IAC9D,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC/D,OAAO,IAAI,EAAE,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,CAAA;AAC5E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAqB,EAAE,OAAsB;IAC/E,IAAI,MAAM,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC;QAAE,OAAM,CAAC,uCAAuC;IAE5F,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAAA;IACtB,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,OAAO,CAAC,UAAU,CAAC,cAAc,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;QACtD,IAAI,GAAG,CAAC,KAAK;YAAE,MAAM,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAA;QACpF,IAAI,GAAG,CAAC,OAAO;YAAE,MAAM,OAAO,CAAC,UAAU,CAAC,oBAAoB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;IAC5F,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { ILockAdapter, IMediaFolderStorage, IMediaMetadataAdapter, IPageCacheStore, IRetentionAdapter, ISeoConfigAdapter, ISiteSettingsAdapter, IWebhookDeliveryAdapter } from '@krantho/persistence';
2
+ export interface LockAdapterResult {
3
+ lockAdapter: ILockAdapter | undefined;
4
+ webhookDeliveryAdapter: IWebhookDeliveryAdapter | undefined;
5
+ retentionAdapter: IRetentionAdapter | undefined;
6
+ siteSettingsAdapter: ISiteSettingsAdapter | undefined;
7
+ seoConfigAdapter: ISeoConfigAdapter | undefined;
8
+ mediaAdapter: IMediaMetadataAdapter | undefined;
9
+ mediaFolderAdapter: IMediaFolderStorage | undefined;
10
+ pageCacheStore: IPageCacheStore | undefined;
11
+ }
12
+ /**
13
+ * Create lock, webhook delivery, retention, site settings, SEO config, media
14
+ * metadata and media folder adapters, and the shared page cache store, from a
15
+ * raw database handle.
16
+ *
17
+ * Detects whether the handle is SQLite (has `prepare` method) or Postgres
18
+ * (a callable function with `begin` method) and loads the appropriate adapter package.
19
+ */
20
+ export declare function createLockAdapters(lockDb: unknown | undefined, persistenceType?: string | undefined): Promise<LockAdapterResult>;
21
+ //# sourceMappingURL=lock-adapters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-adapters.d.ts","sourceRoot":"","sources":["../src/lock-adapters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACxB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,YAAY,GAAG,SAAS,CAAA;IACrC,sBAAsB,EAAE,uBAAuB,GAAG,SAAS,CAAA;IAC3D,gBAAgB,EAAE,iBAAiB,GAAG,SAAS,CAAA;IAC/C,mBAAmB,EAAE,oBAAoB,GAAG,SAAS,CAAA;IACrD,gBAAgB,EAAE,iBAAiB,GAAG,SAAS,CAAA;IAC/C,YAAY,EAAE,qBAAqB,GAAG,SAAS,CAAA;IAC/C,kBAAkB,EAAE,mBAAmB,GAAG,SAAS,CAAA;IACnD,cAAc,EAAE,eAAe,GAAG,SAAS,CAAA;CAC5C;AAaD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAe5B"}
@@ -0,0 +1,69 @@
1
+ const EMPTY = {
2
+ lockAdapter: undefined,
3
+ webhookDeliveryAdapter: undefined,
4
+ retentionAdapter: undefined,
5
+ siteSettingsAdapter: undefined,
6
+ seoConfigAdapter: undefined,
7
+ mediaAdapter: undefined,
8
+ mediaFolderAdapter: undefined,
9
+ pageCacheStore: undefined,
10
+ };
11
+ /**
12
+ * Create lock, webhook delivery, retention, site settings, SEO config, media
13
+ * metadata and media folder adapters, and the shared page cache store, from a
14
+ * raw database handle.
15
+ *
16
+ * Detects whether the handle is SQLite (has `prepare` method) or Postgres
17
+ * (a callable function with `begin` method) and loads the appropriate adapter package.
18
+ */
19
+ export async function createLockAdapters(lockDb, persistenceType) {
20
+ if (!lockDb)
21
+ return EMPTY;
22
+ // Postgres: the sql object is a function (tagged template) with a `begin` method
23
+ if (persistenceType === 'postgres') {
24
+ return createPostgresAdapters(lockDb);
25
+ }
26
+ // SQLite: the db object has a `prepare` method
27
+ const db = lockDb;
28
+ if (typeof db.prepare === 'function') {
29
+ return createSqliteAdapters(lockDb);
30
+ }
31
+ return EMPTY;
32
+ }
33
+ async function createSqliteAdapters(lockDb) {
34
+ try {
35
+ const mod = (await import('@krantho/persistence-sqlite'));
36
+ return {
37
+ lockAdapter: new mod.SqliteLockAdapter(lockDb),
38
+ webhookDeliveryAdapter: new mod.SqliteWebhookDeliveryAdapter(lockDb),
39
+ retentionAdapter: new mod.SqliteRetentionAdapter(lockDb),
40
+ siteSettingsAdapter: new mod.SqliteSiteSettingsAdapter(lockDb),
41
+ seoConfigAdapter: new mod.SqliteSeoConfigAdapter(lockDb),
42
+ mediaAdapter: new mod.SqliteMediaAdapter(lockDb),
43
+ mediaFolderAdapter: new mod.SqliteMediaFolderAdapter(lockDb),
44
+ pageCacheStore: new mod.SqlitePageCacheStore(lockDb),
45
+ };
46
+ }
47
+ catch {
48
+ return EMPTY;
49
+ }
50
+ }
51
+ /**
52
+ * The Postgres package already loaded for the main adapter, so a failure here is
53
+ * a real error. It propagates rather than leaving locks, settings and SEO config
54
+ * silently disabled on a Postgres site.
55
+ */
56
+ async function createPostgresAdapters(lockDb) {
57
+ const mod = (await import('@krantho/persistence-postgres'));
58
+ return {
59
+ lockAdapter: new mod.PostgresLockAdapter(lockDb),
60
+ webhookDeliveryAdapter: new mod.PostgresWebhookDeliveryAdapter(lockDb),
61
+ retentionAdapter: new mod.PostgresRetentionAdapter(lockDb),
62
+ siteSettingsAdapter: new mod.PostgresSiteSettingsAdapter(lockDb),
63
+ seoConfigAdapter: new mod.PostgresSeoConfigAdapter(lockDb),
64
+ mediaAdapter: new mod.PostgresMediaAdapter(lockDb),
65
+ mediaFolderAdapter: new mod.PostgresMediaFolderAdapter(lockDb),
66
+ pageCacheStore: new mod.PostgresPageCacheStore(lockDb),
67
+ };
68
+ }
69
+ //# sourceMappingURL=lock-adapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-adapters.js","sourceRoot":"","sources":["../src/lock-adapters.ts"],"names":[],"mappings":"AAsBA,MAAM,KAAK,GAAsB;IAC/B,WAAW,EAAE,SAAS;IACtB,sBAAsB,EAAE,SAAS;IACjC,gBAAgB,EAAE,SAAS;IAC3B,mBAAmB,EAAE,SAAS;IAC9B,gBAAgB,EAAE,SAAS;IAC3B,YAAY,EAAE,SAAS;IACvB,kBAAkB,EAAE,SAAS;IAC7B,cAAc,EAAE,SAAS;CAC1B,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAA2B,EAC3B,eAAoC;IAEpC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAEzB,iFAAiF;IACjF,IAAI,eAAe,KAAK,UAAU,EAAE,CAAC;QACnC,OAAO,sBAAsB,CAAC,MAAM,CAAC,CAAA;IACvC,CAAC;IAED,+CAA+C;IAC/C,MAAM,EAAE,GAAG,MAA+B,CAAA;IAC1C,IAAI,OAAO,EAAE,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QACrC,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAA;IACrC,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,MAAe;IACjD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,6BAA6B,CAAC,CASvD,CAAA;QACD,OAAO;YACL,WAAW,EAAE,IAAI,GAAG,CAAC,iBAAiB,CAAC,MAAM,CAAC;YAC9C,sBAAsB,EAAE,IAAI,GAAG,CAAC,4BAA4B,CAAC,MAAM,CAAC;YACpE,gBAAgB,EAAE,IAAI,GAAG,CAAC,sBAAsB,CAAC,MAAM,CAAC;YACxD,mBAAmB,EAAE,IAAI,GAAG,CAAC,yBAAyB,CAAC,MAAM,CAAC;YAC9D,gBAAgB,EAAE,IAAI,GAAG,CAAC,sBAAsB,CAAC,MAAM,CAAC;YACxD,YAAY,EAAE,IAAI,GAAG,CAAC,kBAAkB,CAAC,MAAM,CAAC;YAChD,kBAAkB,EAAE,IAAI,GAAG,CAAC,wBAAwB,CAAC,MAAM,CAAC;YAC5D,cAAc,EAAE,IAAI,GAAG,CAAC,oBAAoB,CAAC,MAAM,CAAC;SACrD,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,sBAAsB,CAAC,MAAe;IACnD,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,+BAA+B,CAAC,CASzD,CAAA;IACD,OAAO;QACL,WAAW,EAAE,IAAI,GAAG,CAAC,mBAAmB,CAAC,MAAM,CAAC;QAChD,sBAAsB,EAAE,IAAI,GAAG,CAAC,8BAA8B,CAAC,MAAM,CAAC;QACtE,gBAAgB,EAAE,IAAI,GAAG,CAAC,wBAAwB,CAAC,MAAM,CAAC;QAC1D,mBAAmB,EAAE,IAAI,GAAG,CAAC,2BAA2B,CAAC,MAAM,CAAC;QAChE,gBAAgB,EAAE,IAAI,GAAG,CAAC,wBAAwB,CAAC,MAAM,CAAC;QAC1D,YAAY,EAAE,IAAI,GAAG,CAAC,oBAAoB,CAAC,MAAM,CAAC;QAClD,kBAAkB,EAAE,IAAI,GAAG,CAAC,0BAA0B,CAAC,MAAM,CAAC;QAC9D,cAAc,EAAE,IAAI,GAAG,CAAC,sBAAsB,CAAC,MAAM,CAAC;KACvD,CAAA;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { ContentManifest } from '@krantho/persistence';
2
+ /** The file in the content directory that versions the repository's content */
3
+ export declare const CONTENT_MANIFEST_FILE = "manifest.json";
4
+ /**
5
+ * Write `<contentDir>/manifest.json`: the content's version and each markdown
6
+ * file's SHA-256, keyed by its path relative to the content directory, and the git commit the
7
+ * build ran at when it is known: the baseline a pull of production's edits starts from.
8
+ *
9
+ * Run at build time. A deployment whose manifest version is higher than the
10
+ * database's imports the repository's new and changed files on startup (see
11
+ * `runContentMigration`), so the version must grow with every content change —
12
+ * `contentVersion` derives one from git.
13
+ */
14
+ export declare function generateManifest(contentDir: string, version: number, commit?: string | undefined): Promise<ContentManifest>;
15
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAG3D,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,kBAAkB,CAAA;AAEpD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAC1B,OAAO,CAAC,eAAe,CAAC,CAyB1B"}
@@ -0,0 +1,39 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { contentDateKey } from './content-dates.js';
5
+ /** The file in the content directory that versions the repository's content */
6
+ export const CONTENT_MANIFEST_FILE = 'manifest.json';
7
+ /**
8
+ * Write `<contentDir>/manifest.json`: the content's version and each markdown
9
+ * file's SHA-256, keyed by its path relative to the content directory, and the git commit the
10
+ * build ran at when it is known: the baseline a pull of production's edits starts from.
11
+ *
12
+ * Run at build time. A deployment whose manifest version is higher than the
13
+ * database's imports the repository's new and changed files on startup (see
14
+ * `runContentMigration`), so the version must grow with every content change —
15
+ * `contentVersion` derives one from git.
16
+ */
17
+ export async function generateManifest(contentDir, version, commit) {
18
+ const files = {};
19
+ const entries = await readdir(contentDir, { withFileTypes: true, recursive: true });
20
+ for (const entry of entries) {
21
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
22
+ continue;
23
+ const filePath = join(entry.parentPath, entry.name);
24
+ const content = await readFile(filePath, 'utf-8');
25
+ files[contentDateKey(contentDir, filePath)] = {
26
+ checksum: createHash('sha256').update(content).digest('hex'),
27
+ size: Buffer.byteLength(content, 'utf-8'),
28
+ };
29
+ }
30
+ const manifest = {
31
+ version,
32
+ ...(commit === undefined ? {} : { commit }),
33
+ generatedAt: new Date().toISOString(),
34
+ files,
35
+ };
36
+ await writeFile(join(contentDir, CONTENT_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
37
+ return manifest;
38
+ }
39
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEnD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,eAAe,CAAA;AAEpD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAAkB,EAClB,OAAe,EACf,MAA2B;IAE3B,MAAM,KAAK,GAA6B,EAAE,CAAA;IAC1C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACnF,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,SAAQ;QAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QACnD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QACjD,KAAK,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,GAAG;YAC5C,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YAC5D,IAAI,EAAE,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC;SAC1C,CAAA;IACH,CAAC;IAED,MAAM,QAAQ,GAAoB;QAChC,OAAO;QACP,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;QAC3C,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACrC,KAAK;KACN,CAAA;IACD,MAAM,SAAS,CACb,IAAI,CAAC,UAAU,EAAE,qBAAqB,CAAC,EACvC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EACxC,OAAO,CACR,CAAA;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Common MIME type mappings for static file serving */
2
+ export declare const MIME_TYPES: Record<string, string>;
3
+ //# sourceMappingURL=mime-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mime-types.d.ts","sourceRoot":"","sources":["../src/mime-types.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAY7C,CAAA"}
@@ -0,0 +1,15 @@
1
+ /** Common MIME type mappings for static file serving */
2
+ export const MIME_TYPES = {
3
+ '.html': 'text/html',
4
+ '.js': 'application/javascript',
5
+ '.css': 'text/css',
6
+ '.json': 'application/json',
7
+ '.png': 'image/png',
8
+ '.jpg': 'image/jpeg',
9
+ '.svg': 'image/svg+xml',
10
+ '.ico': 'image/x-icon',
11
+ '.woff2': 'font/woff2',
12
+ '.woff': 'font/woff',
13
+ '.txt': 'text/plain',
14
+ };
15
+ //# sourceMappingURL=mime-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mime-types.js","sourceRoot":"","sources":["../src/mime-types.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,CAAC,MAAM,UAAU,GAA2B;IAChD,OAAO,EAAE,WAAW;IACpB,KAAK,EAAE,wBAAwB;IAC/B,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,kBAAkB;IAC3B,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,YAAY;IACtB,OAAO,EAAE,WAAW;IACpB,MAAM,EAAE,YAAY;CACrB,CAAA"}
@@ -0,0 +1,41 @@
1
+ import type { KranthoConfigInput } from '@krantho/config';
2
+ /**
3
+ * The page cache rules every framework adapter serves pages by.
4
+ *
5
+ * A `GET` or `HEAD` outside `/api` and `/studio` renders inside a page request
6
+ * scope. With a verified signed preview token it reads drafts and answers
7
+ * never stored and never indexed. Otherwise it reads the published version
8
+ * and, under `cache.pages: 'cdn'`, a `200` or `404` that sets no cookie and
9
+ * whose own `Cache-Control` is not `private`, `no-store` or `no-cache` is
10
+ * cached at the CDN, tagged with every read that rendered it. Everything else
11
+ * renders outside a scope and gets no headers.
12
+ */
13
+ /** The request a page is rendered for */
14
+ export interface PageCacheRequest {
15
+ /** Absolute, or a path with its query string */
16
+ url: string | URL;
17
+ method: string;
18
+ }
19
+ /** What the rules need to know about a rendered page's response */
20
+ export interface RenderedPageInfo {
21
+ status: number;
22
+ /**
23
+ * One of the response's own headers by name, in any case — several values
24
+ * joined with `, ` — or null when it has none
25
+ */
26
+ header: (name: string) => string | null;
27
+ }
28
+ /** A rendered page and the headers the rules add to it */
29
+ export interface ServedPage<T> {
30
+ value: T;
31
+ headers: Record<string, string>;
32
+ }
33
+ /**
34
+ * Render one page under the page cache rules.
35
+ *
36
+ * Start every content read inside `render`: a read started before it, or one
37
+ * that outlives it, records no tags. Never call `runPageRequest` inside
38
+ * `render` — a nested scope's tags do not reach this one.
39
+ */
40
+ export declare function servePageRequest<T>(request: PageCacheRequest, config: KranthoConfigInput, render: () => Promise<T>, inspect: (value: T) => RenderedPageInfo): Promise<ServedPage<T>>;
41
+ //# sourceMappingURL=page-cache-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-cache-rules.d.ts","sourceRoot":"","sources":["../src/page-cache-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAMxE;;;;;;;;;;GAUG;AAEH,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;CACxC;AAED,0DAA0D;AAC1D,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAA;IACR,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAWD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,gBAAgB,GACtC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAYxB"}
@@ -0,0 +1,94 @@
1
+ import { loadConfig } from '@krantho/config/node';
2
+ import { cdnCacheHeaders, previewHeaders } from '@krantho/core';
3
+ import { runPageRequest } from './page-request.js';
4
+ import { verifyPreviewRequest } from './preview-token.js';
5
+ /** `Cache-Control` directives by which a page keeps itself out of any shared cache */
6
+ const UNCACHEABLE_DIRECTIVES = new Set(['private', 'no-store', 'no-cache']);
7
+ /** Paths the krantho API and Studio own — never a page */
8
+ const KRANTHO_PREFIXES = ['/api', '/studio'];
9
+ /** Only a path needs a base; its origin is never used */
10
+ const URL_BASE = 'http://localhost';
11
+ /**
12
+ * Render one page under the page cache rules.
13
+ *
14
+ * Start every content read inside `render`: a read started before it, or one
15
+ * that outlives it, records no tags. Never call `runPageRequest` inside
16
+ * `render` — a nested scope's tags do not reach this one.
17
+ */
18
+ export async function servePageRequest(request, config, render, inspect) {
19
+ const url = requestUrl(request.url);
20
+ if (!isPageRequest(url, request.method))
21
+ return { value: await render(), headers: {} };
22
+ const resolved = loadConfig({ code: config, env: process.env });
23
+ if (verifyPreviewRequest(url, resolved)) {
24
+ const { value } = await runPageRequest({ preview: true }, render);
25
+ return { value, headers: { ...previewHeaders(), 'Referrer-Policy': 'no-referrer' } };
26
+ }
27
+ const { value, tags } = await runPageRequest({ preview: false }, render);
28
+ return { value, headers: publishedPageHeaders(resolved, inspect(value), tags) };
29
+ }
30
+ /**
31
+ * A path is appended to the base, never resolved against it: resolving
32
+ * would read `//studio/x` as the host `admin` and the path `/x`.
33
+ */
34
+ function requestUrl(url) {
35
+ if (typeof url === 'string' && url.startsWith('/'))
36
+ return new URL(`${URL_BASE}${url}`);
37
+ return new URL(url);
38
+ }
39
+ /** A `GET` or `HEAD` for a path the krantho API and Studio do not own */
40
+ function isPageRequest(url, method) {
41
+ const verb = method.toUpperCase();
42
+ if (verb !== 'GET' && verb !== 'HEAD')
43
+ return false;
44
+ return !isKranthoPath(url.pathname);
45
+ }
46
+ /**
47
+ * Whether a path belongs to the API or Studio, however it is spelled:
48
+ * percent-encoded, in any case, or with repeated slashes. A path that does not
49
+ * decode is treated as theirs, so it is never cached as a page.
50
+ */
51
+ function isKranthoPath(pathname) {
52
+ let path;
53
+ try {
54
+ path = decodeURIComponent(pathname);
55
+ }
56
+ catch {
57
+ return true;
58
+ }
59
+ const normal = path.replace(/\/{2,}/g, '/').toLowerCase();
60
+ return KRANTHO_PREFIXES.some((prefix) => normal === prefix || normal.startsWith(`${prefix}/`));
61
+ }
62
+ function publishedPageHeaders(config, page, tags) {
63
+ const { pages, cdn } = config.cache;
64
+ if (pages !== 'cdn' || cdn.provider === 'none')
65
+ return {};
66
+ if (page.status !== 200 && page.status !== 404)
67
+ return {};
68
+ if (!isShareable(page))
69
+ return {};
70
+ return cdnCacheHeaders({
71
+ tags,
72
+ provider: cdn.provider,
73
+ sMaxAgeSec: cdn.sMaxAgeSec,
74
+ staleWhileRevalidateSec: cdn.staleWhileRevalidateSec,
75
+ tagHeader: cdn.tagHeader,
76
+ });
77
+ }
78
+ /**
79
+ * Whether a page's own response lets a shared cache keep it: it sets no
80
+ * cookie, and its own `Cache-Control`, if any, says none of `private`,
81
+ * `no-store` or `no-cache` — a page that depends on the visitor's cookie or
82
+ * credentials says so there, and that header is left as the page set it.
83
+ */
84
+ function isShareable(page) {
85
+ if (page.header('set-cookie') !== null)
86
+ return false;
87
+ const cacheControl = page.header('cache-control');
88
+ if (cacheControl === null)
89
+ return true;
90
+ return !cacheControl
91
+ .split(',')
92
+ .some((directive) => UNCACHEABLE_DIRECTIVES.has(directive.split('=')[0]?.trim().toLowerCase() ?? ''));
93
+ }
94
+ //# sourceMappingURL=page-cache-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-cache-rules.js","sourceRoot":"","sources":["../src/page-cache-rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { PersistenceSection } from '@krantho/config';
2
+ import type { IPageCacheStore } from '@krantho/persistence';
3
+ /**
4
+ * Open the shared page cache store of a site's database.
5
+ *
6
+ * Opens the database as the server does — the adapter runs the migrations —
7
+ * and returns its page cache store. Throws when the backend keeps no page
8
+ * cache: the filesystem provider has no database to share between instances.
9
+ */
10
+ export declare function openPageCacheStore(persistence: PersistenceSection): Promise<IPageCacheStore>;
11
+ //# sourceMappingURL=page-cache-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-cache-store.d.ts","sourceRoot":"","sources":["../src/page-cache-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAkB,kBAAkB,EAAe,MAAM,iBAAiB,CAAA;AAEtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAwB3D;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,kBAAkB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAkB1B"}
@@ -0,0 +1,46 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { DEFAULTS } from '@krantho/config';
4
+ import { createLockAdapters } from './lock-adapters.js';
5
+ import { createAdapter } from './persistence.js';
6
+ /**
7
+ * The site's default locale the adapter is opened with. The page cache reads no
8
+ * content and runs without the site's configuration, so it has no locale to
9
+ * give: an empty one matches no document, where a guessed locale would read
10
+ * some other language's content.
11
+ */
12
+ const NO_CONTENT_SITE = { defaultLocale: '' };
13
+ /**
14
+ * Content status defaults the adapter is opened with. The page cache reads no
15
+ * content, so the values only satisfy the adapter's options.
16
+ */
17
+ function contentDefaults() {
18
+ const status = DEFAULTS.content?.defaultStatus;
19
+ if (status?.filesystem === undefined) {
20
+ throw new Error('The configuration defaults name no content.defaultStatus');
21
+ }
22
+ return { defaultStatus: { filesystem: status.filesystem } };
23
+ }
24
+ /**
25
+ * Open the shared page cache store of a site's database.
26
+ *
27
+ * Opens the database as the server does — the adapter runs the migrations —
28
+ * and returns its page cache store. Throws when the backend keeps no page
29
+ * cache: the filesystem provider has no database to share between instances.
30
+ */
31
+ export async function openPageCacheStore(persistence) {
32
+ if (persistence.provider === 'filesystem') {
33
+ throw new Error('The shared page cache needs a database, but persistence.provider is "filesystem". ' +
34
+ 'Use sqlite or postgres, or set cache.shared to "none".');
35
+ }
36
+ if (persistence.provider === 'sqlite') {
37
+ await mkdir(dirname(persistence.sqlite.path), { recursive: true });
38
+ }
39
+ const { lockDb } = await createAdapter(persistence, NO_CONTENT_SITE, contentDefaults(), new Map());
40
+ const { pageCacheStore } = await createLockAdapters(lockDb, persistence.provider);
41
+ if (!pageCacheStore) {
42
+ throw new Error(`The ${persistence.provider} persistence backend could not open the shared page cache store.`);
43
+ }
44
+ return pageCacheStore;
45
+ }
46
+ //# sourceMappingURL=page-cache-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-cache-store.js","sourceRoot":"","sources":["../src/page-cache-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAEhD;;;;;GAKG;AACH,MAAM,eAAe,GAAuC,EAAE,aAAa,EAAE,EAAE,EAAE,CAAA;AAEjF;;;GAGG;AACH,SAAS,eAAe;IACtB,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC9C,IAAI,MAAM,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;IAC7E,CAAC;IACD,OAAO,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,CAAA;AAC7D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,WAA+B;IAE/B,IAAI,WAAW,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CACb,oFAAoF;YAClF,wDAAwD,CAC3D,CAAA;IACH,CAAC;IACD,IAAI,WAAW,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACtC,MAAM,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACpE,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,eAAe,EAAE,eAAe,EAAE,EAAE,IAAI,GAAG,EAAE,CAAC,CAAA;IAClG,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,kBAAkB,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjF,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,OAAO,WAAW,CAAC,QAAQ,kEAAkE,CAC9F,CAAA;IACH,CAAC;IACD,OAAO,cAAc,CAAA;AACvB,CAAC"}