@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.
- package/LICENSE +21 -0
- package/dist/cdn/cloudfront.d.ts +9 -0
- package/dist/cdn/cloudfront.d.ts.map +1 -0
- package/dist/cdn/cloudfront.js +35 -0
- package/dist/cdn/cloudfront.js.map +1 -0
- package/dist/cdn/gcp.d.ts +15 -0
- package/dist/cdn/gcp.d.ts.map +1 -0
- package/dist/cdn/gcp.js +60 -0
- package/dist/cdn/gcp.js.map +1 -0
- package/dist/cdn/index.d.ts +9 -0
- package/dist/cdn/index.d.ts.map +1 -0
- package/dist/cdn/index.js +30 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/listener.d.ts +14 -0
- package/dist/cdn/listener.d.ts.map +1 -0
- package/dist/cdn/listener.js +53 -0
- package/dist/cdn/listener.js.map +1 -0
- package/dist/cdn/retry.d.ts +3 -0
- package/dist/cdn/retry.d.ts.map +1 -0
- package/dist/cdn/retry.js +17 -0
- package/dist/cdn/retry.js.map +1 -0
- package/dist/cdn/types.d.ts +12 -0
- package/dist/cdn/types.d.ts.map +1 -0
- package/dist/cdn/types.js +2 -0
- package/dist/cdn/types.js.map +1 -0
- package/dist/cdn/vercel.d.ts +9 -0
- package/dist/cdn/vercel.d.ts.map +1 -0
- package/dist/cdn/vercel.js +31 -0
- package/dist/cdn/vercel.js.map +1 -0
- package/dist/content-conflicts.d.ts +18 -0
- package/dist/content-conflicts.d.ts.map +1 -0
- package/dist/content-conflicts.js +22 -0
- package/dist/content-conflicts.js.map +1 -0
- package/dist/content-dates.d.ts +35 -0
- package/dist/content-dates.d.ts.map +1 -0
- package/dist/content-dates.js +85 -0
- package/dist/content-dates.js.map +1 -0
- package/dist/content-loader.d.ts +37 -0
- package/dist/content-loader.d.ts.map +1 -0
- package/dist/content-loader.js +223 -0
- package/dist/content-loader.js.map +1 -0
- package/dist/content-migration.d.ts +101 -0
- package/dist/content-migration.d.ts.map +1 -0
- package/dist/content-migration.js +291 -0
- package/dist/content-migration.js.map +1 -0
- package/dist/convert.d.ts +18 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +152 -0
- package/dist/convert.js.map +1 -0
- package/dist/file-store.d.ts +11 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +39 -0
- package/dist/file-store.js.map +1 -0
- package/dist/file-watcher.d.ts +19 -0
- package/dist/file-watcher.d.ts.map +1 -0
- package/dist/file-watcher.js +99 -0
- package/dist/file-watcher.js.map +1 -0
- package/dist/handler.d.ts +61 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +197 -0
- package/dist/handler.js.map +1 -0
- package/dist/import.d.ts +25 -0
- package/dist/import.d.ts.map +1 -0
- package/dist/import.js +71 -0
- package/dist/import.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/indexnow.d.ts +38 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +89 -0
- package/dist/indexnow.js.map +1 -0
- package/dist/lazy-handler.d.ts +7 -0
- package/dist/lazy-handler.d.ts.map +1 -0
- package/dist/lazy-handler.js +16 -0
- package/dist/lazy-handler.js.map +1 -0
- package/dist/llm-seed.d.ts +25 -0
- package/dist/llm-seed.d.ts.map +1 -0
- package/dist/llm-seed.js +42 -0
- package/dist/llm-seed.js.map +1 -0
- package/dist/lock-adapters.d.ts +21 -0
- package/dist/lock-adapters.d.ts.map +1 -0
- package/dist/lock-adapters.js +69 -0
- package/dist/lock-adapters.js.map +1 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +39 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mime-types.d.ts +3 -0
- package/dist/mime-types.d.ts.map +1 -0
- package/dist/mime-types.js +15 -0
- package/dist/mime-types.js.map +1 -0
- package/dist/page-cache-rules.d.ts +41 -0
- package/dist/page-cache-rules.d.ts.map +1 -0
- package/dist/page-cache-rules.js +94 -0
- package/dist/page-cache-rules.js.map +1 -0
- package/dist/page-cache-store.d.ts +11 -0
- package/dist/page-cache-store.d.ts.map +1 -0
- package/dist/page-cache-store.js +46 -0
- package/dist/page-cache-store.js.map +1 -0
- package/dist/page-request.d.ts +26 -0
- package/dist/page-request.d.ts.map +1 -0
- package/dist/page-request.js +29 -0
- package/dist/page-request.js.map +1 -0
- package/dist/persistence.d.ts +36 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +108 -0
- package/dist/persistence.js.map +1 -0
- package/dist/preview-token.d.ts +9 -0
- package/dist/preview-token.d.ts.map +1 -0
- package/dist/preview-token.js +14 -0
- package/dist/preview-token.js.map +1 -0
- package/dist/read-cache.d.ts +17 -0
- package/dist/read-cache.d.ts.map +1 -0
- package/dist/read-cache.js +33 -0
- package/dist/read-cache.js.map +1 -0
- package/dist/route-matcher.d.ts +21 -0
- package/dist/route-matcher.d.ts.map +1 -0
- package/dist/route-matcher.js +75 -0
- package/dist/route-matcher.js.map +1 -0
- package/dist/secret-vault.d.ts +20 -0
- package/dist/secret-vault.d.ts.map +1 -0
- package/dist/secret-vault.js +60 -0
- package/dist/secret-vault.js.map +1 -0
- package/dist/seo-config-io.d.ts +51 -0
- package/dist/seo-config-io.d.ts.map +1 -0
- package/dist/seo-config-io.js +151 -0
- package/dist/seo-config-io.js.map +1 -0
- package/dist/seo-seed.d.ts +12 -0
- package/dist/seo-seed.d.ts.map +1 -0
- package/dist/seo-seed.js +100 -0
- package/dist/seo-seed.js.map +1 -0
- package/dist/studio-gate.d.ts +25 -0
- package/dist/studio-gate.d.ts.map +1 -0
- package/dist/studio-gate.js +70 -0
- package/dist/studio-gate.js.map +1 -0
- package/dist/studio.d.ts +17 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +107 -0
- package/dist/studio.js.map +1 -0
- package/dist/webhook-sweep.d.ts +22 -0
- package/dist/webhook-sweep.d.ts.map +1 -0
- package/dist/webhook-sweep.js +33 -0
- package/dist/webhook-sweep.js.map +1 -0
- package/package.json +87 -3
- 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"}
|
package/dist/seo-seed.js
ADDED
|
@@ -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":"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"}
|
|
@@ -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":"AAAA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE1F,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,kBAAkB,EAAE,4BAA4B,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAWhG;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA2B;IAC5D,IAAI,IAA6B,CAAA;IAEjC,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAA;YAC5D,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;gBACjE,OAAO,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;QACD,OAAO,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,kBAAkB,EAAE,CAAA;IACvE,CAAC,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,OAAsB,EAAE,IAAiB;IAC3D,IAAI,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,OAAO,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,kBAAkB,EAAE,CAAA;IACvE,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAA;IACzE,sEAAsE;IACtE,MAAM,UAAU,GACd,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAA;IAE/F,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,kBAAkB,EAAE,CAAA;QAC/E,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAA;IAC9B,CAAC;IAED,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC9E,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,UAAU;QACzB,CAAC,CAAC,8BAA8B,QAAQ,EAAE;QAC1C,CAAC,CAAC,iCAAiC,QAAQ,EAAE,CAAA;IAC/C,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE;KACnD,CAAC,CAAA;AACJ,CAAC;AAED,mDAAmD;AACnD,SAAS,WAAW,CAAC,QAAkB;IACrC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;IACjD,IAAI,cAAc,GAAG,SAAS,CAAA;IAC9B,IAAI,YAAY,EAAE,QAAQ,CAAC,QAAQ,CAAC;QAAE,cAAc,GAAG,YAAY,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;SAC3F,IAAI,YAAY;QAAE,cAAc,GAAG,YAAY,YAAY,EAAE,CAAA;IAClE,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAA;IAC5C,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;AAC1E,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO,IAAI,QAAQ,CAAC,4BAA4B,EAAE;QAChD,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;KAC1C,CAAC,CAAA;AACJ,CAAC"}
|
package/dist/studio.d.ts
ADDED
|
@@ -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"}
|