@tokenoftrust/storefront-runner 4.14.0 → 4.15.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.
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { TenantConfig, TenantResolver } from "@tot/public-runtime";
|
|
19
19
|
import { withPlatformPolicy } from "@/config/tenants";
|
|
20
|
-
import { TENANT_RECORD_PREFIX } from "@/config/storedTenantRecord";
|
|
20
|
+
import { TENANT_RECORD_PREFIX, getStoredTenantRecord } from "@/config/storedTenantRecord";
|
|
21
|
+
import { mergeStoredTenantRecord } from "@tot/public-runtime/tenant-record-merge";
|
|
21
22
|
|
|
22
23
|
const DOMAIN_PREFIX = "domain:";
|
|
23
24
|
const TENANT_PREFIX = TENANT_RECORD_PREFIX;
|
|
@@ -76,16 +77,25 @@ export class KvTenantResolver implements TenantResolver {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* Register (or update) a tenant in KV. Idempotent:
|
|
80
|
-
*
|
|
81
|
-
*
|
|
80
|
+
* Register (or update) a tenant in KV. Idempotent: merges the config into the stored
|
|
81
|
+
* `tenant:<id>` record (provisioned keys win, stored keys the config does not carry survive;
|
|
82
|
+
* see `@tot/public-runtime/tenant-record-merge`) and writes a domain->id index entry for each
|
|
83
|
+
* claimed hostname. The stored record is read uncached. A stored record that is not a JSON
|
|
84
|
+
* object throws before any write. Pass `makeDefault` to point the unknown-host fallback at
|
|
85
|
+
* this tenant. Call this from onboarding/migration.
|
|
82
86
|
*/
|
|
83
87
|
export async function putTenant(
|
|
84
88
|
kv: KVNamespace,
|
|
85
89
|
config: TenantConfig,
|
|
86
90
|
opts: { makeDefault?: boolean } = {},
|
|
87
91
|
): Promise<void> {
|
|
88
|
-
|
|
92
|
+
const key = TENANT_PREFIX + config.tenant_id;
|
|
93
|
+
const record = mergeStoredTenantRecord(
|
|
94
|
+
key,
|
|
95
|
+
await getStoredTenantRecord(kv, config.tenant_id),
|
|
96
|
+
config as unknown as Record<string, unknown>,
|
|
97
|
+
);
|
|
98
|
+
await kv.put(key, record);
|
|
89
99
|
for (const domain of config.domains) {
|
|
90
100
|
const host = cleanHost(domain);
|
|
91
101
|
if (host) await kv.put(DOMAIN_PREFIX + host, config.tenant_id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"./svg-sanitize": "./src/svg-sanitize.ts",
|
|
27
27
|
"./incremental-sha256": "./src/incremental-sha256.ts",
|
|
28
28
|
"./image-limits": "./src/image-limits.mjs",
|
|
29
|
+
"./tenant-record-merge": "./src/tenant-record-merge.mjs",
|
|
29
30
|
"./image-release-gate": "./src/image-release-gate.ts",
|
|
30
31
|
"./pool-asset-headers": "./src/pool-asset-headers.ts",
|
|
31
32
|
"./catalog-files": "./src/catalog-files.ts",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function mergeTenantRecord(
|
|
2
|
+
stored: Record<string, unknown>,
|
|
3
|
+
provisioned: Record<string, unknown>,
|
|
4
|
+
): Record<string, unknown>;
|
|
5
|
+
export declare function mergeStoredTenantRecord(
|
|
6
|
+
key: string,
|
|
7
|
+
storedRaw: string | null | undefined,
|
|
8
|
+
provisioned: Record<string, unknown>,
|
|
9
|
+
): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge a provisioned tenant config into the `tenant:<id>` KV record already stored.
|
|
3
|
+
*
|
|
4
|
+
* Provisioners carry only the fields they own. The stored record also holds fields written by
|
|
5
|
+
* other paths (`storefront.mode`, `addOns.largeImages`), so a provisioner must read the stored
|
|
6
|
+
* record, merge, and write the result; it never replaces the record wholesale.
|
|
7
|
+
*
|
|
8
|
+
* Contract:
|
|
9
|
+
* - the provisioned config wins for every key it carries;
|
|
10
|
+
* - plain objects merge recursively; arrays and scalars from the provisioned config replace;
|
|
11
|
+
* - keys the provisioned config does not carry are preserved;
|
|
12
|
+
* - `__proto__`, `constructor` and `prototype` keys are dropped at every level;
|
|
13
|
+
* - no stored record: the provisioned config is written as given;
|
|
14
|
+
* - a stored value that is not a JSON object (unparseable, array, scalar, empty string) throws,
|
|
15
|
+
* naming the key, and the caller writes nothing.
|
|
16
|
+
*
|
|
17
|
+
* KV has no transaction. A provisioner that reads, merges and writes while the admin add-ons
|
|
18
|
+
* switch writes the same record can still lose one of the two writes; the merge narrows that
|
|
19
|
+
* window to one read-to-write interval and cannot close it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
23
|
+
|
|
24
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
25
|
+
function isPlainObject(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Record<string, unknown>} stored
|
|
31
|
+
* @param {Record<string, unknown>} provisioned
|
|
32
|
+
* @returns {Record<string, unknown>}
|
|
33
|
+
*/
|
|
34
|
+
export function mergeTenantRecord(stored, provisioned) {
|
|
35
|
+
/** @type {Record<string, unknown>} */
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
38
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
39
|
+
out[key] = value;
|
|
40
|
+
}
|
|
41
|
+
for (const [key, value] of Object.entries(provisioned)) {
|
|
42
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
43
|
+
const existing = out[key];
|
|
44
|
+
out[key] = isPlainObject(value) && isPlainObject(existing) ? mergeTenantRecord(existing, value) : value;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The JSON to write at `key` for `provisioned`, given the raw stored value (`null` when absent).
|
|
51
|
+
* @param {string} key
|
|
52
|
+
* @param {string | null | undefined} storedRaw
|
|
53
|
+
* @param {Record<string, unknown>} provisioned
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
export function mergeStoredTenantRecord(key, storedRaw, provisioned) {
|
|
57
|
+
if (storedRaw === null || storedRaw === undefined) return JSON.stringify(provisioned);
|
|
58
|
+
let stored;
|
|
59
|
+
try {
|
|
60
|
+
stored = JSON.parse(storedRaw);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(`stored KV record ${key} is not valid JSON; refusing to overwrite it`);
|
|
63
|
+
}
|
|
64
|
+
if (!isPlainObject(stored)) {
|
|
65
|
+
throw new Error(`stored KV record ${key} is not a JSON object; refusing to overwrite it`);
|
|
66
|
+
}
|
|
67
|
+
return JSON.stringify(mergeTenantRecord(stored, provisioned));
|
|
68
|
+
}
|