@krantho/server 0.0.0-stage → 1.0.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 (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 +107 -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 +87 -3
  147. package/README.md +0 -3
@@ -0,0 +1,151 @@
1
+ /**
2
+ * SEO configuration export and import, and seeding the stored configuration.
3
+ *
4
+ * Writes per-entity YAML files in the layout `@krantho/config/node` reads
5
+ * definition files from (`readSeoDefinitions`), so production state
6
+ * round-trips into a project's `seo.configDir`.
7
+ *
8
+ * Layout:
9
+ * krantho.seo/
10
+ * ├── site/ ← site-scope entities
11
+ * │ ├── professional-service.yaml
12
+ * │ ├── website.yaml
13
+ * │ ├── founder.person.yaml ← role.type.yaml
14
+ * │ └── _field-mappings.yaml
15
+ * ├── collections/{name}/ ← collection-scope entities
16
+ * └── routes/{slug-encoded}.yaml
17
+ */
18
+ import { mkdir, writeFile } from 'node:fs/promises';
19
+ import { join } from 'node:path';
20
+ import { encodeRouteSlug, readSeoDefinitions } from '@krantho/config/node';
21
+ import { stringify as stringifyYaml } from 'yaml';
22
+ // ---------------------------------------------------------------------------
23
+ // Export — DB → files
24
+ // ---------------------------------------------------------------------------
25
+ /** Export all SEO config from the database to a directory. */
26
+ export async function exportSeoConfig(adapter, targetDir) {
27
+ const configs = await adapter.listConfigs();
28
+ const filesWritten = [];
29
+ let entitiesExported = 0;
30
+ for (const config of configs) {
31
+ const { id, config: data } = config;
32
+ // Route scope: write all entities with index prefix per pattern
33
+ if (id.startsWith('route:')) {
34
+ const routesDir = join(targetDir, 'routes');
35
+ await mkdir(routesDir, { recursive: true });
36
+ const pattern = id.slice('route:'.length);
37
+ const slug = encodeRouteSlug(pattern);
38
+ for (let i = 0; i < data.entities.length; i++) {
39
+ const entity = data.entities[i];
40
+ const prefix = String(i).padStart(2, '0');
41
+ const filePath = join(routesDir, `${slug}--${prefix}-${entityToFilename(entity)}`);
42
+ await writeFile(filePath, stringifyEntityYaml(entity), 'utf-8');
43
+ filesWritten.push(filePath);
44
+ entitiesExported++;
45
+ }
46
+ continue;
47
+ }
48
+ const scopeDir = scopeIdToDir(id, targetDir);
49
+ await mkdir(scopeDir, { recursive: true });
50
+ // Write entity files with index prefix to preserve order
51
+ for (let i = 0; i < data.entities.length; i++) {
52
+ const entity = data.entities[i];
53
+ const prefix = String(i).padStart(2, '0');
54
+ const filename = `${prefix}-${entityToFilename(entity)}`;
55
+ const filePath = join(scopeDir, filename);
56
+ await writeFile(filePath, stringifyEntityYaml(entity), 'utf-8');
57
+ filesWritten.push(filePath);
58
+ entitiesExported++;
59
+ }
60
+ // Write field mappings if present
61
+ if (data.fieldMappings && Object.keys(data.fieldMappings).length > 0) {
62
+ const mappingsPath = join(scopeDir, '_field-mappings.yaml');
63
+ await writeFile(mappingsPath, stringifyYaml(data.fieldMappings), 'utf-8');
64
+ filesWritten.push(mappingsPath);
65
+ }
66
+ }
67
+ return {
68
+ scopesExported: configs.length,
69
+ entitiesExported,
70
+ filesWritten,
71
+ };
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Import — files → DB
75
+ // ---------------------------------------------------------------------------
76
+ /** Import SEO config from a directory into the database. */
77
+ export async function importSeoConfig(adapter, sourceDir, options) {
78
+ const mode = options?.mode ?? 'seed';
79
+ return applySeedConfig(adapter, readSeoDefinitions(sourceDir), mode);
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Seed application — shared by directory loader and programmatic seed
83
+ // ---------------------------------------------------------------------------
84
+ /** Apply a SeoSeedConfig to the database. */
85
+ export async function applySeedConfig(adapter, config, mode = 'seed') {
86
+ let scopesImported = 0;
87
+ let entitiesImported = 0;
88
+ const skipped = [];
89
+ async function seedScope(id, scope, data) {
90
+ if (mode === 'seed') {
91
+ const existing = await adapter.getConfig(id);
92
+ if (existing) {
93
+ skipped.push(id);
94
+ return;
95
+ }
96
+ }
97
+ await adapter.saveConfig({ id, scope, config: data });
98
+ scopesImported++;
99
+ entitiesImported += data.entities.length;
100
+ }
101
+ if (config.site) {
102
+ await seedScope('site', 'site', config.site);
103
+ }
104
+ for (const [name, data] of Object.entries(config.collections ?? {})) {
105
+ await seedScope(`collection:${name}`, 'collection', data);
106
+ }
107
+ for (const [pattern, data] of Object.entries(config.routes ?? {})) {
108
+ await seedScope(`route:${pattern}`, 'route', data);
109
+ }
110
+ return { scopesImported, entitiesImported, skipped, errors: [] };
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // File naming helpers
114
+ // ---------------------------------------------------------------------------
115
+ /** Convert a SeoConfigEntity to a filename. */
116
+ function entityToFilename(entity) {
117
+ const type = toKebabCase(entity['@type']);
118
+ if (entity.role) {
119
+ return `${toKebabCase(entity.role)}.${type}.yaml`;
120
+ }
121
+ return `${type}.yaml`;
122
+ }
123
+ /** Convert a scope config ID to a directory path. */
124
+ function scopeIdToDir(id, baseDir) {
125
+ if (id === 'site')
126
+ return join(baseDir, 'site');
127
+ if (id.startsWith('collection:'))
128
+ return join(baseDir, 'collections', id.slice('collection:'.length));
129
+ if (id.startsWith('route:')) {
130
+ return join(baseDir, 'routes');
131
+ }
132
+ throw new Error(`Unknown scope ID format: ${id}`);
133
+ }
134
+ /** Stringify an entity as YAML. */
135
+ function stringifyEntityYaml(entity) {
136
+ const obj = { '@type': entity['@type'] };
137
+ if (entity['@id'])
138
+ obj['@id'] = entity['@id'];
139
+ if (entity.role)
140
+ obj['role'] = entity.role;
141
+ obj['properties'] = entity.properties;
142
+ return stringifyYaml(obj);
143
+ }
144
+ /** Convert PascalCase to kebab-case. */
145
+ function toKebabCase(str) {
146
+ return str
147
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
148
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
149
+ .toLowerCase();
150
+ }
151
+ //# sourceMappingURL=seo-config-io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-config-io.js","sourceRoot":"","sources":["../src/seo-config-io.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { KranthoConfig } from '@krantho/config';
2
+ import type { ISeoConfigAdapter } from '@krantho/persistence';
3
+ /**
4
+ * Seed the stored SEO configuration from the `seo` section, once per process
5
+ * and database. The handler and the loader both await it before they serve
6
+ * anything, so no page is composed — and cached — from an unseeded database,
7
+ * and the seed has nothing to announce. Seeding is idempotent across
8
+ * processes too: `seed` mode writes only missing rows, and a row is saved
9
+ * whole, so two instances seeding one database write the same rows.
10
+ */
11
+ export declare function seedSeoOnce(config: KranthoConfig, adapter: ISeoConfigAdapter): Promise<void>;
12
+ //# sourceMappingURL=seo-seed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-seed.d.ts","sourceRoot":"","sources":["../src/seo-seed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,iBAAiB,CAAA;AAEhE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAa7D;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB5F"}
@@ -0,0 +1,100 @@
1
+ import { resolve } from 'node:path';
2
+ import { isConfigured } from '@krantho/config';
3
+ import { applySeedConfig } from './seo-config-io.js';
4
+ /**
5
+ * This process's SEO seeds, by database and seed: the API handler and the
6
+ * content loader open their own connections to one database, and each seed
7
+ * runs once between them — concurrent first requests share it, and a later
8
+ * caller does not seed again over edits made since. A failed seed is
9
+ * forgotten, so the next caller retries it.
10
+ */
11
+ const seeds = new Map();
12
+ /**
13
+ * Seed the stored SEO configuration from the `seo` section, once per process
14
+ * and database. The handler and the loader both await it before they serve
15
+ * anything, so no page is composed — and cached — from an unseeded database,
16
+ * and the seed has nothing to announce. Seeding is idempotent across
17
+ * processes too: `seed` mode writes only missing rows, and a row is saved
18
+ * whole, so two instances seeding one database write the same rows.
19
+ */
20
+ export function seedSeoOnce(config, adapter) {
21
+ if (!isConfigured(config, 'seo'))
22
+ return Promise.resolve();
23
+ const plan = seedPlan(config.seo);
24
+ const key = JSON.stringify([
25
+ databaseKey(config.persistence),
26
+ config.seo.definitions,
27
+ config.seo.seedMode,
28
+ plan,
29
+ ]);
30
+ let seeding = seeds.get(key);
31
+ if (!seeding) {
32
+ seeding = seedSeoConfig(config.seo, plan, adapter).catch((err) => {
33
+ seeds.delete(key);
34
+ throw err;
35
+ });
36
+ seeds.set(key, seeding);
37
+ }
38
+ return seeding;
39
+ }
40
+ /** The database a connection opens, whichever connection it is */
41
+ function databaseKey(persistence) {
42
+ switch (persistence.provider) {
43
+ case 'sqlite':
44
+ return `sqlite:${resolve(persistence.sqlite.path)}`;
45
+ case 'postgres':
46
+ return `postgres:${persistence.postgres.url ?? ''}`;
47
+ case 'filesystem':
48
+ return `filesystem:${resolve(persistence.filesystem.contentPath)}`;
49
+ }
50
+ }
51
+ /**
52
+ * Seed SEO config from developer-defined defaults: the definition files the
53
+ * configuration loaded from `configDir` first, then the section's own shapes.
54
+ * Seed mode `seed` (the default) keeps stored rows; `overwrite` replaces them.
55
+ */
56
+ async function seedSeoConfig(seo, plan, adapter) {
57
+ if (seo.configDir) {
58
+ await applySeedConfig(adapter, seo.definitions, seo.seedMode);
59
+ }
60
+ if (plan.site || plan.collections || plan.routes) {
61
+ await applySeedConfig(adapter, plan, seo.seedMode);
62
+ }
63
+ }
64
+ /**
65
+ * The seed the section's own shapes describe:
66
+ * 1. Thin: `identity`, `collections`, `routes` — type lists only
67
+ * 2. Rich: `site`, `richCollections`, `richRoutes` — full entity config, which
68
+ * wins over the thin shape for the same scope
69
+ */
70
+ function seedPlan(seo) {
71
+ const plan = {};
72
+ if (seo.site) {
73
+ plan.site = seo.site;
74
+ }
75
+ else if (seo.identity) {
76
+ plan.site = { entities: [{ '@type': seo.identity, properties: {} }] };
77
+ }
78
+ if (Object.keys(seo.richCollections).length > 0) {
79
+ plan.collections = { ...seo.richCollections };
80
+ }
81
+ if (Object.keys(seo.richRoutes).length > 0) {
82
+ plan.routes = { ...seo.richRoutes };
83
+ }
84
+ for (const [collection, types] of Object.entries(seo.collections)) {
85
+ if (seo.richCollections[collection])
86
+ continue;
87
+ plan.collections ??= {};
88
+ plan.collections[collection] = {
89
+ entities: types.map((t) => ({ '@type': t, properties: {} })),
90
+ };
91
+ }
92
+ for (const [pattern, types] of Object.entries(seo.routes)) {
93
+ if (seo.richRoutes[pattern])
94
+ continue;
95
+ plan.routes ??= {};
96
+ plan.routes[pattern] = { entities: types.map((t) => ({ '@type': t, properties: {} })) };
97
+ }
98
+ return plan;
99
+ }
100
+ //# sourceMappingURL=seo-seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-seed.js","sourceRoot":"","sources":["../src/seo-seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAA;AAE9C;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,MAAqB,EAAE,OAA0B;IAC3E,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;IAC1D,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;QACzB,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,WAAW;QACtB,MAAM,CAAC,GAAG,CAAC,QAAQ;QACnB,IAAI;KACL,CAAC,CAAA;IACF,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACxE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACjB,MAAM,GAAG,CAAA;QACX,CAAC,CAAC,CAAA;QACF,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,kEAAkE;AAClE,SAAS,WAAW,CAAC,WAAyC;IAC5D,QAAQ,WAAW,CAAC,QAAQ,EAAE,CAAC;QAC7B,KAAK,QAAQ;YACX,OAAO,UAAU,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAA;QACrD,KAAK,UAAU;YACb,OAAO,YAAY,WAAW,CAAC,QAAQ,CAAC,GAAG,IAAI,EAAE,EAAE,CAAA;QACrD,KAAK,YAAY;YACf,OAAO,cAAc,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAA;IACtE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,aAAa,CAC1B,GAAe,EACf,IAAmB,EACnB,OAA0B;IAE1B,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QAClB,MAAM,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC/D,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACjD,MAAM,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IACpD,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,GAAe;IAC/B,MAAM,IAAI,GAAkB,EAAE,CAAA;IAE9B,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACb,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAA;IACtB,CAAC;SAAM,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,EAAE,CAAA;IACvE,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,IAAI,CAAC,WAAW,GAAG,EAAE,GAAG,GAAG,CAAC,eAAe,EAAE,CAAA;IAC/C,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,GAAG,CAAC,UAAU,EAAE,CAAA;IACrC,CAAC;IAED,KAAK,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;QAClE,IAAI,GAAG,CAAC,eAAe,CAAC,UAAU,CAAC;YAAE,SAAQ;QAC7C,IAAI,CAAC,WAAW,KAAK,EAAE,CAAA;QACvB,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,GAAG;YAC7B,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC;SAC7D,CAAA;IACH,CAAC;IAED,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,SAAQ;QACrC,IAAI,CAAC,MAAM,KAAK,EAAE,CAAA;QAClB,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAA;IACzF,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { KranthoConfigInput } from '@krantho/config';
2
+ /** A request for a Studio path, reduced to what serving it needs */
3
+ export interface StudioRequest {
4
+ url: URL;
5
+ cookie?: string | undefined;
6
+ }
7
+ /** Serves /studio: the gate, then the SPA files, then the not-built notice */
8
+ export type StudioServer = (request: StudioRequest) => Response;
9
+ /**
10
+ * Create the /studio server shared by every framework adapter.
11
+ *
12
+ * Without configuration, or unless `auth.oidc.studioEntry` is `redirect`, it
13
+ * serves the Studio SPA as-is and the SPA shows its own sign-in page. With
14
+ * `redirect`, Studio is served only with a valid access token. Without one,
15
+ * a browser that holds a refresh token is sent to `GET /api/auth/refresh`,
16
+ * which issues a new token pair and returns it here — or, when the refresh
17
+ * token is no longer valid, clears the session and starts a new sign-in. A
18
+ * browser without either is sent to the OIDC sign-in, which returns it here.
19
+ * Gated responses are private, so a shared cache never answers for the gate.
20
+ * The build's hashed assets are served without the gate: they hold no data,
21
+ * and the SPA fetches chunks lazily — after the access cookie expired, a gated
22
+ * chunk would redirect and break the running Studio.
23
+ */
24
+ export declare function createStudioServer(config?: KranthoConfigInput): StudioServer;
25
+ //# sourceMappingURL=studio-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studio-gate.d.ts","sourceRoot":"","sources":["../src/studio-gate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAItE,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,GAAG,CAAA;IACR,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,aAAa,KAAK,QAAQ,CAAA;AAE/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,YAAY,CAY5E"}
@@ -0,0 +1,70 @@
1
+ import { accessCookieName, parseCookie, refreshCookieName, verifyJwt } from '@krantho/api';
2
+ import { loadConfig } from '@krantho/config/node';
3
+ import { isStudioBuildAsset, STUDIO_NOT_AVAILABLE_MESSAGE, serveStudioAsset } from './studio.js';
4
+ /**
5
+ * Create the /studio server shared by every framework adapter.
6
+ *
7
+ * Without configuration, or unless `auth.oidc.studioEntry` is `redirect`, it
8
+ * serves the Studio SPA as-is and the SPA shows its own sign-in page. With
9
+ * `redirect`, Studio is served only with a valid access token. Without one,
10
+ * a browser that holds a refresh token is sent to `GET /api/auth/refresh`,
11
+ * which issues a new token pair and returns it here — or, when the refresh
12
+ * token is no longer valid, clears the session and starts a new sign-in. A
13
+ * browser without either is sent to the OIDC sign-in, which returns it here.
14
+ * Gated responses are private, so a shared cache never answers for the gate.
15
+ * The build's hashed assets are served without the gate: they hold no data,
16
+ * and the SPA fetches chunks lazily — after the access cookie expired, a gated
17
+ * chunk would redirect and break the running Studio.
18
+ */
19
+ export function createStudioServer(config) {
20
+ let auth;
21
+ return (request) => {
22
+ if (config) {
23
+ auth ??= loadConfig({ code: config, env: process.env }).auth;
24
+ if (auth.mode === 'oidc' && auth.oidc.studioEntry === 'redirect') {
25
+ return serveGated(request, auth);
26
+ }
27
+ }
28
+ return serveStudioAsset(request.url.pathname) ?? studioNotAvailable();
29
+ };
30
+ }
31
+ function serveGated(request, auth) {
32
+ if (isStudioBuildAsset(request.url.pathname)) {
33
+ return serveStudioAsset(request.url.pathname) ?? studioNotAvailable();
34
+ }
35
+ const token = parseCookie(request.cookie, accessCookieName(auth.session));
36
+ // Validation guarantees a JWT secret whenever studioEntry is redirect
37
+ const hasSession = token !== undefined && auth.jwtSecret !== undefined && verifyJwt(token, auth.jwtSecret).valid;
38
+ if (hasSession) {
39
+ const response = serveStudioAsset(request.url.pathname) ?? studioNotAvailable();
40
+ return privateCopy(response);
41
+ }
42
+ const returnTo = encodeURIComponent(request.url.pathname + request.url.search);
43
+ const canRefresh = Boolean(parseCookie(request.cookie, refreshCookieName(auth.session)));
44
+ const location = canRefresh
45
+ ? `/api/auth/refresh?returnTo=${returnTo}`
46
+ : `/api/auth/login/oidc?returnTo=${returnTo}`;
47
+ return new Response(null, {
48
+ status: 302,
49
+ headers: { location, 'cache-control': 'no-store' },
50
+ });
51
+ }
52
+ /** Same response, cacheable by the browser only */
53
+ function privateCopy(response) {
54
+ const headers = new Headers(response.headers);
55
+ const cacheControl = headers.get('cache-control');
56
+ let privateControl = 'private';
57
+ if (cacheControl?.includes('public'))
58
+ privateControl = cacheControl.replace('public', 'private');
59
+ else if (cacheControl)
60
+ privateControl = `private, ${cacheControl}`;
61
+ headers.set('cache-control', privateControl);
62
+ return new Response(response.body, { status: response.status, headers });
63
+ }
64
+ function studioNotAvailable() {
65
+ return new Response(STUDIO_NOT_AVAILABLE_MESSAGE, {
66
+ status: 503,
67
+ headers: { 'content-type': 'text/plain' },
68
+ });
69
+ }
70
+ //# sourceMappingURL=studio-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studio-gate.js","sourceRoot":"","sources":["../src/studio-gate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Error message when Studio SPA is not built — shared across all server adapters */
2
+ export declare const STUDIO_NOT_AVAILABLE_MESSAGE = "Studio not available. Build it with: pnpm --filter @krantho/studio run build";
3
+ /**
4
+ * Whether a path names a file of the Studio build's `assets/` directory. Those
5
+ * files are content-hashed and immutable, and hold no data — the same files
6
+ * ship in the published `@krantho/studio` package.
7
+ */
8
+ export declare function isStudioBuildAsset(requestPath: string): boolean;
9
+ /**
10
+ * Serve admin static assets from @krantho/studio/dist/.
11
+ *
12
+ * Returns a Response for the requested file, or the SPA index.html
13
+ * for client-side routing paths. A missing build asset answers 404, never
14
+ * index.html. Returns null if admin is not available.
15
+ */
16
+ export declare function serveStudioAsset(requestPath: string): Response | null;
17
+ //# sourceMappingURL=studio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"AAKA,qFAAqF;AACrF,eAAO,MAAM,4BAA4B,iFACuC,CAAA;AA6ChF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAgDrE"}
package/dist/studio.js ADDED
@@ -0,0 +1,107 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, extname, join, resolve, sep } from 'node:path';
4
+ import { MIME_TYPES } from './mime-types.js';
5
+ /** Error message when Studio SPA is not built — shared across all server adapters */
6
+ export const STUDIO_NOT_AVAILABLE_MESSAGE = 'Studio not available. Build it with: pnpm --filter @krantho/studio run build';
7
+ let studioDistDir;
8
+ /**
9
+ * Resolve the path to @krantho/studio's dist directory.
10
+ *
11
+ * Resolves from the app's own dependencies first: the app installs @krantho/studio,
12
+ * and resolving from the app root (not from this module) keeps working when a
13
+ * bundler inlines this package. Walking up from cwd covers a workspace checkout,
14
+ * or a deployed function where the SPA was traced under `packages/studio/dist`.
15
+ */
16
+ function getStudioDistDir() {
17
+ studioDistDir ??= resolveStudioDist() ?? findStudioDistFromCwd();
18
+ return studioDistDir;
19
+ }
20
+ function resolveStudioDist() {
21
+ try {
22
+ const require = createRequire(join(process.cwd(), 'package.json'));
23
+ return dirname(require.resolve('@krantho/studio/dist/index.html'));
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ function findStudioDistFromCwd() {
30
+ let dir = process.cwd();
31
+ while (dir !== resolve(dir, '..')) {
32
+ const candidate = join(dir, 'node_modules', '@krantho', 'studio', 'dist', 'index.html');
33
+ if (existsSync(candidate))
34
+ return join(candidate, '..');
35
+ // Also check sibling packages (monorepo symlink layout)
36
+ const sibling = join(dir, 'packages', 'studio', 'dist', 'index.html');
37
+ if (existsSync(sibling))
38
+ return join(sibling, '..');
39
+ dir = resolve(dir, '..');
40
+ }
41
+ throw new Error('@krantho/studio is not installed or not built. Run: pnpm --filter @krantho/studio run build');
42
+ }
43
+ /** Where the Studio build puts its content-hashed JS, CSS and fonts */
44
+ const BUILD_ASSETS_PREFIX = '/studio/assets/';
45
+ /**
46
+ * Whether a path names a file of the Studio build's `assets/` directory. Those
47
+ * files are content-hashed and immutable, and hold no data — the same files
48
+ * ship in the published `@krantho/studio` package.
49
+ */
50
+ export function isStudioBuildAsset(requestPath) {
51
+ return requestPath.startsWith(BUILD_ASSETS_PREFIX);
52
+ }
53
+ /**
54
+ * Serve admin static assets from @krantho/studio/dist/.
55
+ *
56
+ * Returns a Response for the requested file, or the SPA index.html
57
+ * for client-side routing paths. A missing build asset answers 404, never
58
+ * index.html. Returns null if admin is not available.
59
+ */
60
+ export function serveStudioAsset(requestPath) {
61
+ let distDir;
62
+ try {
63
+ distDir = getStudioDistDir();
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ // Strip /studio prefix to get the asset path
69
+ const assetPath = requestPath.replace(/^\/studio\/?/, '') || 'index.html';
70
+ const filePath = resolve(join(distDir, assetPath));
71
+ // Security: prevent path traversal
72
+ const safeRoot = distDir.endsWith(sep) ? distDir : distDir + sep;
73
+ if (!filePath.startsWith(safeRoot) && filePath !== resolve(distDir)) {
74
+ return new Response('Forbidden', { status: 403 });
75
+ }
76
+ try {
77
+ const content = readFileSync(filePath);
78
+ const ext = extname(filePath);
79
+ const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
80
+ // Build assets are content-hashed and immutable; everything else must revalidate
81
+ const cacheControl = isStudioBuildAsset(requestPath)
82
+ ? 'public, max-age=31536000, immutable'
83
+ : 'no-cache';
84
+ return new Response(content, {
85
+ status: 200,
86
+ headers: { 'content-type': contentType, 'cache-control': cacheControl },
87
+ });
88
+ }
89
+ catch {
90
+ // A missing chunk must fail as one; index.html in its place is no module
91
+ if (isStudioBuildAsset(requestPath)) {
92
+ return new Response('Not found', { status: 404, headers: { 'content-type': 'text/plain' } });
93
+ }
94
+ // File not found — serve index.html for SPA client-side routing
95
+ try {
96
+ const indexContent = readFileSync(join(distDir, 'index.html'));
97
+ return new Response(indexContent, {
98
+ status: 200,
99
+ headers: { 'content-type': 'text/html', 'cache-control': 'no-cache' },
100
+ });
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ }
107
+ //# sourceMappingURL=studio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studio.js","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { WebhookConfig } from '@krantho/api';
2
+ import type { IWebhookDeliveryAdapter } from '@krantho/persistence';
3
+ /** The least time between two sweeps for deliveries that are due again */
4
+ export declare const WEBHOOK_SWEEP_INTERVAL_MS = 30000;
5
+ export interface WebhookSweepOptions {
6
+ webhooks: readonly WebhookConfig[];
7
+ /** The delivery log the sweep reads the due deliveries from and records outcomes in */
8
+ deliveries: IWebhookDeliveryAdapter;
9
+ /** Keeps the sweep running until it settles, on hosts that end a request's work with its response */
10
+ waitUntil?: ((work: Promise<unknown>) => void) | undefined;
11
+ /** The clock, in milliseconds; tests replace it */
12
+ now?: (() => number) | undefined;
13
+ }
14
+ /**
15
+ * Retry the deliveries that are due, without a timer: serverless hosts do not
16
+ * keep one alive. The returned function is called for every request; it starts
17
+ * a sweep when none is running and at least {@link WEBHOOK_SWEEP_INTERVAL_MS}
18
+ * has passed since the last one started, and otherwise does nothing. A sweep
19
+ * never blocks a request, and a failing one is logged, never thrown.
20
+ */
21
+ export declare function createWebhookSweep(options: WebhookSweepOptions): () => void;
22
+ //# sourceMappingURL=webhook-sweep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-sweep.d.ts","sourceRoot":"","sources":["../src/webhook-sweep.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAEjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAEnE,0EAA0E;AAC1E,eAAO,MAAM,yBAAyB,QAAS,CAAA;AAE/C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAClC,uFAAuF;IACvF,UAAU,EAAE,uBAAuB,CAAA;IACnC,qGAAqG;IACrG,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IAC1D,mDAAmD;IACnD,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAA;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,IAAI,CAsB3E"}
@@ -0,0 +1,33 @@
1
+ import { processWebhookRetries } from '@krantho/api';
2
+ /** The least time between two sweeps for deliveries that are due again */
3
+ export const WEBHOOK_SWEEP_INTERVAL_MS = 30_000;
4
+ /**
5
+ * Retry the deliveries that are due, without a timer: serverless hosts do not
6
+ * keep one alive. The returned function is called for every request; it starts
7
+ * a sweep when none is running and at least {@link WEBHOOK_SWEEP_INTERVAL_MS}
8
+ * has passed since the last one started, and otherwise does nothing. A sweep
9
+ * never blocks a request, and a failing one is logged, never thrown.
10
+ */
11
+ export function createWebhookSweep(options) {
12
+ const now = options.now ?? Date.now;
13
+ let lastStartedAt = Number.NEGATIVE_INFINITY;
14
+ let running = false;
15
+ return () => {
16
+ const startedAt = now();
17
+ if (running || startedAt - lastStartedAt < WEBHOOK_SWEEP_INTERVAL_MS)
18
+ return;
19
+ lastStartedAt = startedAt;
20
+ running = true;
21
+ // Read the due deliveries after the request's own synchronous work, not before it
22
+ const sweep = Promise.resolve()
23
+ .then(() => processWebhookRetries(options.webhooks, options.deliveries))
24
+ .catch((err) => {
25
+ console.warn('[webhook] Retry sweep failed:', err);
26
+ })
27
+ .finally(() => {
28
+ running = false;
29
+ });
30
+ options.waitUntil?.(sweep);
31
+ };
32
+ }
33
+ //# sourceMappingURL=webhook-sweep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-sweep.js","sourceRoot":"","sources":["../src/webhook-sweep.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAGpD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAA;AAY/C;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAA4B;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IACnC,IAAI,aAAa,GAAG,MAAM,CAAC,iBAAiB,CAAA;IAC5C,IAAI,OAAO,GAAG,KAAK,CAAA;IAEnB,OAAO,GAAG,EAAE;QACV,MAAM,SAAS,GAAG,GAAG,EAAE,CAAA;QACvB,IAAI,OAAO,IAAI,SAAS,GAAG,aAAa,GAAG,yBAAyB;YAAE,OAAM;QAC5E,aAAa,GAAG,SAAS,CAAA;QACzB,OAAO,GAAG,IAAI,CAAA;QAEd,kFAAkF;QAClF,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE;aAC5B,IAAI,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;aACvE,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACtB,OAAO,CAAC,IAAI,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAA;QACpD,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,OAAO,GAAG,KAAK,CAAA;QACjB,CAAC,CAAC,CAAA;QACJ,OAAO,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC"}