@volter/twin-cloudflare 0.1.0 → 0.1.2
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/dist/src/cloudflare-common.js +2 -2
- package/dist/src/cloudflare-provisioning.js +16 -15
- package/dist/src/cloudflare-twin.js +10 -10
- package/dist/src/cloudflare-worker-assets.js +4 -3
- package/package.json +4 -4
- package/src/cloudflare-common.ts +2 -2
- package/src/cloudflare-provisioning.ts +16 -15
- package/src/cloudflare-twin.ts +10 -10
- package/src/cloudflare-worker-assets.ts +4 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// cloudflare-twin.ts) and the provisioning slice (Workers/R2/Hyperdrive/Queues in
|
|
4
4
|
// cloudflare-provisioning.ts). Pure helpers over the @volter/world-core kernel — no routes here.
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
-
import { applyTwinWrite, projectResources } from '@volter/world-core';
|
|
6
|
+
import { applyTwinWrite, projectResources, worldNow } from '@volter/world-core';
|
|
7
7
|
export const SERVICE = 'cloudflare';
|
|
8
8
|
export const CLOUDFLARE_RESOURCE_TYPES = [
|
|
9
9
|
'account',
|
|
@@ -133,7 +133,7 @@ export function callerEmail(req) {
|
|
|
133
133
|
}
|
|
134
134
|
/** One entry in an account's audit log, attributed to the calling token's user. */
|
|
135
135
|
export async function audit(req, accountID, entry) {
|
|
136
|
-
const when = req.occurredAt ??
|
|
136
|
+
const when = req.occurredAt ?? worldNow();
|
|
137
137
|
const id = deterministicHex(`audit:${accountID}:${when}:${entry.resource.type}:${entry.resource.id}:${rows('audit_log', req.root).length}`, 32);
|
|
138
138
|
const email = callerEmail(req);
|
|
139
139
|
await write('audit_log', id, 'audit_log.record', { account_id: accountID, when, action: { type: entry.type, result: true, info: '' },
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// pack's generic conventions (1004 validation, 1001 not found) everywhere else — see README
|
|
18
18
|
// "Official grounding". No live probe was possible (no Cloudflare account); the OpenAPI + SDK
|
|
19
19
|
// are the honest grounding ceiling for this slice.
|
|
20
|
+
import { worldNow } from '@volter/world-core';
|
|
20
21
|
import { createHash } from 'node:crypto';
|
|
21
22
|
import { deterministicHex, failure, headers as lowerHeaders, isResponse, mint, noRoute, paginate, parseBody, readOnly, row, rows, callerEmail, audit, scopeMissing, success, uuidFrom, write, } from "./cloudflare-common.js";
|
|
22
23
|
import { catchUpR2CustomDomains, connectR2CustomDomain, createR2TempCredential, r2BearerToken, r2Credential, r2ParentCovers, removeR2CustomDomain, r2CustomDomains, TEMP_PERMISSIONS, updateR2CustomDomain } from "./cloudflare-r2.js";
|
|
@@ -768,7 +769,7 @@ function buildUpload(req, url, accountID, scriptName, mode) {
|
|
|
768
769
|
* UUID (the shape wrangler prints as "Current Version ID"). */
|
|
769
770
|
async function writeVersion(req, accountID, scriptName, scriptTag, number, content, annotations) {
|
|
770
771
|
const id = mint('worker_version', `${accountID}:${scriptName}:${scriptTag}:${number}`, req.root);
|
|
771
|
-
const at = req.occurredAt ??
|
|
772
|
+
const at = req.occurredAt ?? worldNow();
|
|
772
773
|
await pwrite('worker_version', id, 'worker_version.upload', {
|
|
773
774
|
account_id: accountID, script_name: scriptName, script_tag: scriptTag, number, ...content,
|
|
774
775
|
source: deploySource(req), annotations, author_email: callerEmail(req) || null, created_on: at, modified_on: at, deleted: false,
|
|
@@ -779,7 +780,7 @@ function deploymentRecord(req, script, versions, annotations) {
|
|
|
779
780
|
const count = Array.isArray(script.deployments) ? script.deployments.length : 0;
|
|
780
781
|
return {
|
|
781
782
|
id: uuidFrom(`${String(script.tag)}:deployment:${count}`),
|
|
782
|
-
created_on: req.occurredAt ??
|
|
783
|
+
created_on: req.occurredAt ?? worldNow(),
|
|
783
784
|
source: deploySource(req),
|
|
784
785
|
strategy: 'percentage',
|
|
785
786
|
versions,
|
|
@@ -803,7 +804,7 @@ async function uploadScript(req, url, accountID, scriptName) {
|
|
|
803
804
|
const { existing, content } = built;
|
|
804
805
|
const tag = String(existing?.tag ?? mintScriptTag(accountID, scriptName, req.root));
|
|
805
806
|
const number = Number(existing?.version_count ?? 0) + 1;
|
|
806
|
-
const at = req.occurredAt ??
|
|
807
|
+
const at = req.occurredAt ?? worldNow();
|
|
807
808
|
// All validation passed — now perform the writes (migrations, the version, then the script).
|
|
808
809
|
await applyMigrationPlan(accountID, scriptName, built.migrationOps, req);
|
|
809
810
|
const version = await writeVersion(req, accountID, scriptName, tag, number, content, built.annotations);
|
|
@@ -989,7 +990,7 @@ async function createDeployment(req, accountID, script) {
|
|
|
989
990
|
modules: null,
|
|
990
991
|
version_id: live.id,
|
|
991
992
|
deployments: [...deployments, deployment],
|
|
992
|
-
modified_on: req.occurredAt ??
|
|
993
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
993
994
|
}, req);
|
|
994
995
|
return success(deployment);
|
|
995
996
|
}
|
|
@@ -1025,7 +1026,7 @@ async function patchScriptSettings(req, accountID, script) {
|
|
|
1025
1026
|
...(body.tags !== undefined ? { tags: body.tags ?? [] } : {}),
|
|
1026
1027
|
...(body.tail_consumers !== undefined ? { tail_consumers: body.tail_consumers } : {}),
|
|
1027
1028
|
...(body.observability !== undefined ? { observability: body.observability } : {}),
|
|
1028
|
-
modified_on: req.occurredAt ??
|
|
1029
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
1029
1030
|
}, req);
|
|
1030
1031
|
return success(settingsView(scriptRow(accountID, String(script.name), req.root)));
|
|
1031
1032
|
}
|
|
@@ -1237,7 +1238,7 @@ async function workersRoute(req, method, url, parts, accountID) {
|
|
|
1237
1238
|
}
|
|
1238
1239
|
if (!Array.isArray(body))
|
|
1239
1240
|
return failure(400, 1004, 'Cron trigger update requires an array of {cron} objects');
|
|
1240
|
-
const at = req.occurredAt ??
|
|
1241
|
+
const at = req.occurredAt ?? worldNow();
|
|
1241
1242
|
const schedules = [];
|
|
1242
1243
|
for (const value of body) {
|
|
1243
1244
|
const item = value;
|
|
@@ -1546,7 +1547,7 @@ async function r2Route(req, method, url, parts, accountID) {
|
|
|
1546
1547
|
if (accountBuckets().some((item) => item.name === body.name)) {
|
|
1547
1548
|
return failure(409, 10004, 'The bucket you tried to create already exists, and you own it.');
|
|
1548
1549
|
}
|
|
1549
|
-
const at = req.occurredAt ??
|
|
1550
|
+
const at = req.occurredAt ?? worldNow();
|
|
1550
1551
|
await pwrite('r2_bucket', `${accountID}:${body.name}`, 'r2_bucket.create', {
|
|
1551
1552
|
account_id: accountID,
|
|
1552
1553
|
name: body.name,
|
|
@@ -1582,7 +1583,7 @@ async function r2Route(req, method, url, parts, accountID) {
|
|
|
1582
1583
|
await pwrite('r2_bucket', `${accountID}:${bucketName}`, 'r2_bucket.delete', { account_id: accountID, deleted: true }, req);
|
|
1583
1584
|
// "The specified domain will also remain connected to R2 until you remove it or delete the bucket." (public buckets)
|
|
1584
1585
|
for (const d of r2CustomDomains(req.root).filter((x) => x.account_id === accountID && x.bucket === bucketName))
|
|
1585
|
-
await removeR2CustomDomain(d.id, req.root, req.occurredAt ??
|
|
1586
|
+
await removeR2CustomDomain(d.id, req.root, req.occurredAt ?? worldNow());
|
|
1586
1587
|
return success({});
|
|
1587
1588
|
}
|
|
1588
1589
|
return noRoute();
|
|
@@ -1617,7 +1618,7 @@ async function r2TempCredentialsRoute(req, accountID) {
|
|
|
1617
1618
|
const objects = list(body.objects);
|
|
1618
1619
|
if (!prefixes || !objects)
|
|
1619
1620
|
return failure(400, 1004, 'prefixes and objects must be lists of strings');
|
|
1620
|
-
const c = await createR2TempCredential({ accountId: accountID, bucket: body.bucket, parent, permission, ttlSeconds: ttl, prefixes, objects }, req.root, req.occurredAt ??
|
|
1621
|
+
const c = await createR2TempCredential({ accountId: accountID, bucket: body.bucket, parent, permission, ttlSeconds: ttl, prefixes, objects }, req.root, req.occurredAt ?? worldNow());
|
|
1621
1622
|
return success({ accessKeyId: c.access_key_id, secretAccessKey: c.secret_access_key, sessionToken: c.session_token });
|
|
1622
1623
|
}
|
|
1623
1624
|
// R2 custom domains (https://developers.cloudflare.com/api/resources/r2/subresources/buckets/subresources/domains/subresources/custom/):
|
|
@@ -1628,7 +1629,7 @@ async function r2TempCredentialsRoute(req, accountID) {
|
|
|
1628
1629
|
// and 1001 not-found conventions.
|
|
1629
1630
|
const TLS_VERSIONS = new Set(['1.0', '1.1', '1.2', '1.3']);
|
|
1630
1631
|
async function r2CustomDomainRoute(req, method, parts, accountID, bucketName) {
|
|
1631
|
-
const at = req.occurredAt ??
|
|
1632
|
+
const at = req.occurredAt ?? worldNow();
|
|
1632
1633
|
await catchUpR2CustomDomains(req.root, at);
|
|
1633
1634
|
const mine = () => r2CustomDomains(req.root).filter((d) => d.account_id === accountID && d.bucket === bucketName);
|
|
1634
1635
|
const full = (d) => ({ domain: d.domain, enabled: d.enabled, status: { ownership: d.ownership, ssl: d.ssl }, minTLS: d.min_tls, ...(d.ciphers ? { ciphers: d.ciphers } : {}), zoneId: d.zone_id, zoneName: d.zone_name });
|
|
@@ -1844,7 +1845,7 @@ async function hyperdriveRoute(req, method, url, parts, accountID) {
|
|
|
1844
1845
|
const checked = validateWrite(body);
|
|
1845
1846
|
if (isResponse(checked))
|
|
1846
1847
|
return checked;
|
|
1847
|
-
const at = req.occurredAt ??
|
|
1848
|
+
const at = req.occurredAt ?? worldNow();
|
|
1848
1849
|
const id = mint('hyperdrive_config', `${accountID}:${body.name}`, req.root);
|
|
1849
1850
|
await pwrite('hyperdrive_config', id, 'hyperdrive_config.create', {
|
|
1850
1851
|
account_id: accountID,
|
|
@@ -1884,7 +1885,7 @@ async function hyperdriveRoute(req, method, url, parts, accountID) {
|
|
|
1884
1885
|
caching: body.caching ?? null,
|
|
1885
1886
|
mtls: body.mtls ?? null,
|
|
1886
1887
|
origin_connection_limit: typeof body.origin_connection_limit === 'number' ? body.origin_connection_limit : null,
|
|
1887
|
-
modified_on: req.occurredAt ??
|
|
1888
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
1888
1889
|
}, req);
|
|
1889
1890
|
return success(hyperdriveView(row('hyperdrive_config', hyperdriveID, req.root)));
|
|
1890
1891
|
}
|
|
@@ -1926,7 +1927,7 @@ async function hyperdriveRoute(req, method, url, parts, accountID) {
|
|
|
1926
1927
|
...(body.caching !== undefined ? { caching: body.caching } : {}),
|
|
1927
1928
|
...(body.mtls !== undefined ? { mtls: body.mtls } : {}),
|
|
1928
1929
|
...(body.origin_connection_limit !== undefined ? { origin_connection_limit: body.origin_connection_limit } : {}),
|
|
1929
|
-
modified_on: req.occurredAt ??
|
|
1930
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
1930
1931
|
}, req);
|
|
1931
1932
|
return success(hyperdriveView(row('hyperdrive_config', hyperdriveID, req.root)));
|
|
1932
1933
|
}
|
|
@@ -1984,7 +1985,7 @@ async function queuesRoute(req, method, parts, accountID) {
|
|
|
1984
1985
|
if (accountQueues().some((item) => item.queue_name === body.queue_name)) {
|
|
1985
1986
|
return failure(400, 1004, `A queue named "${body.queue_name}" already exists on this account`);
|
|
1986
1987
|
}
|
|
1987
|
-
const at = req.occurredAt ??
|
|
1988
|
+
const at = req.occurredAt ?? worldNow();
|
|
1988
1989
|
const id = mint('queue', `${accountID}:${body.queue_name}`, req.root);
|
|
1989
1990
|
await pwrite('queue', id, 'queue.create', {
|
|
1990
1991
|
account_id: accountID, queue_name: body.queue_name, created_on: at, modified_on: at, deleted: false,
|
|
@@ -2037,7 +2038,7 @@ async function queuesRoute(req, method, parts, accountID) {
|
|
|
2037
2038
|
// A Cloudflare queue has at most one consumer (worker and http_pull are mutually exclusive).
|
|
2038
2039
|
if (queueConsumers().length > 0)
|
|
2039
2040
|
return failure(400, 1004, 'This queue already has a consumer; delete it before adding another');
|
|
2040
|
-
const at = req.occurredAt ??
|
|
2041
|
+
const at = req.occurredAt ?? worldNow();
|
|
2041
2042
|
const id = mint('queue_consumer', `${queueID}:${String(body.script_name ?? 'http_pull')}`, req.root);
|
|
2042
2043
|
await pwrite('queue_consumer', id, 'queue_consumer.create', {
|
|
2043
2044
|
account_id: accountID,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// api-schemas OpenAPI 4.0.0 (commit 2ac8369e9b63dccacee1a2284e95bb819f05b307) and official
|
|
10
10
|
// cloudflare-typescript SDK (commit faaaf89ed8064a9fb54de538ec3e89487f1302b0; npm cloudflare@7.1.0).
|
|
11
11
|
import { isIP } from 'node:net';
|
|
12
|
-
import { resolveSubjectId } from '@volter/world-core';
|
|
12
|
+
import { resolveSubjectId, worldNow } from '@volter/world-core';
|
|
13
13
|
import { CLOUDFLARE_RESOURCE_TYPES, deterministicHex, failure, headers, isResponse, mint, noRoute, paginate, parseBody, readOnly, row, rows, SERVICE, scopeMissing, success, write, callerEmail, audit, } from "./cloudflare-common.js";
|
|
14
14
|
import { CLOUDFLARE_PROVISIONING_SURFACE, handleProvisioningRoute } from "./cloudflare-provisioning.js";
|
|
15
15
|
export { CLOUDFLARE_RESOURCE_TYPES } from "./cloudflare-common.js";
|
|
@@ -138,7 +138,7 @@ async function bootstrap(req, body) {
|
|
|
138
138
|
const blocked = readOnly(req);
|
|
139
139
|
if (blocked)
|
|
140
140
|
return blocked;
|
|
141
|
-
const observedAt = req.occurredAt ??
|
|
141
|
+
const observedAt = req.occurredAt ?? worldNow();
|
|
142
142
|
const accounts = Array.isArray(body.accounts) ? body.accounts : [];
|
|
143
143
|
const zones = Array.isArray(body.zones) ? body.zones : [];
|
|
144
144
|
for (const value of accounts) {
|
|
@@ -200,7 +200,7 @@ async function bootstrap(req, body) {
|
|
|
200
200
|
const item = (value ?? {});
|
|
201
201
|
if (typeof item.email !== 'string' || typeof item.token !== 'string' || !item.token)
|
|
202
202
|
return failure(400, 1004, 'Bootstrap user_tokens require an email and a token');
|
|
203
|
-
await write('user_token', item.token, 'user_token.observe', { token: item.token, email: item.email, created_on: req.occurredAt ??
|
|
203
|
+
await write('user_token', item.token, 'user_token.observe', { token: item.token, email: item.email, created_on: req.occurredAt ?? worldNow() }, req);
|
|
204
204
|
}
|
|
205
205
|
return success({ accounts: accounts.length, zones: zones.length, members: members.length, user_tokens: userTokens.length });
|
|
206
206
|
}
|
|
@@ -350,7 +350,7 @@ async function zoneSettingRoute(req, method, parts) {
|
|
|
350
350
|
if (typeof body.value !== 'string' || !def.values.includes(body.value))
|
|
351
351
|
return failure(400, 1007, `Invalid value for zone setting ${parts[3]}`);
|
|
352
352
|
const before = view().value;
|
|
353
|
-
await write('zone_setting', id, 'zone_setting.update', { zone_id: parts[1], setting: parts[3], value: body.value, modified_on: req.occurredAt ??
|
|
353
|
+
await write('zone_setting', id, 'zone_setting.update', { zone_id: parts[1], setting: parts[3], value: body.value, modified_on: req.occurredAt ?? worldNow() }, req);
|
|
354
354
|
await audit(req, String(zone.account_id), { type: 'update', resource: { type: 'zone_setting', id: parts[3] }, oldValue: before, newValue: body.value, metadata: { zone_id: parts[1], zone_name: zone.name, setting: parts[3] } });
|
|
355
355
|
return success(view());
|
|
356
356
|
}
|
|
@@ -384,7 +384,7 @@ export async function handleCloudflareTwinRequest(req) {
|
|
|
384
384
|
if (!/^[^@\s]+@[^@\s]+$/.test(parts[2]))
|
|
385
385
|
return failure(400, 1001, 'A valid email is required');
|
|
386
386
|
const token = `cf-${deterministicHex(`token:${parts[2]}:${rows('user_token', req.root).length}`, 40)}`;
|
|
387
|
-
await write('user_token', token, 'user_token.create', { token, email: parts[2], created_on: req.occurredAt ??
|
|
387
|
+
await write('user_token', token, 'user_token.create', { token, email: parts[2], created_on: req.occurredAt ?? worldNow() }, req);
|
|
388
388
|
// A user's token is logged in each account the user is a member of.
|
|
389
389
|
for (const m of rows('account_member', req.root).filter((x) => x.email === parts[2]))
|
|
390
390
|
await audit({ ...req, headers: { authorization: `Bearer ${token}` } }, String(m.account_id), { type: 'create', resource: { type: 'token', id: token.slice(0, 12) }, newValue: { owner: parts[2] } });
|
|
@@ -490,7 +490,7 @@ export async function handleCloudflareTwinRequest(req) {
|
|
|
490
490
|
return failure(400, 1004, 'tunnel_secret must be canonical base64 encoding of at least 32 bytes');
|
|
491
491
|
}
|
|
492
492
|
const id = mint('cloudflare_tunnel', `${accountID}:${body.name}`, req.root);
|
|
493
|
-
const createdAt = req.occurredAt ??
|
|
493
|
+
const createdAt = req.occurredAt ?? worldNow();
|
|
494
494
|
await write('cloudflare_tunnel', id, 'tunnel.create', {
|
|
495
495
|
account_id: accountID,
|
|
496
496
|
name: body.name,
|
|
@@ -537,7 +537,7 @@ export async function handleCloudflareTwinRequest(req) {
|
|
|
537
537
|
const blocked = readOnly(req);
|
|
538
538
|
if (blocked)
|
|
539
539
|
return blocked;
|
|
540
|
-
const deletedAt = req.occurredAt ??
|
|
540
|
+
const deletedAt = req.occurredAt ?? worldNow();
|
|
541
541
|
await write('cloudflare_tunnel', tunnelID, 'tunnel.delete', { account_id: accountID, deleted: true, deleted_at: deletedAt }, req);
|
|
542
542
|
return success(tunnelView(row('cloudflare_tunnel', tunnelID, req.root, true)));
|
|
543
543
|
}
|
|
@@ -580,7 +580,7 @@ export async function handleCloudflareTwinRequest(req) {
|
|
|
580
580
|
tunnel_id: tunnelID,
|
|
581
581
|
source: 'cloudflare',
|
|
582
582
|
version: Number(current?.version ?? 0) + 1,
|
|
583
|
-
created_at: current?.created_at ?? (req.occurredAt ??
|
|
583
|
+
created_at: current?.created_at ?? (req.occurredAt ?? worldNow()),
|
|
584
584
|
config,
|
|
585
585
|
}, req);
|
|
586
586
|
return success(configurationView(row('tunnel_configuration', id, req.root)));
|
|
@@ -620,7 +620,7 @@ export async function handleCloudflareTwinRequest(req) {
|
|
|
620
620
|
if (invalid)
|
|
621
621
|
return invalid;
|
|
622
622
|
const id = mint('dns_record', `${zoneID}:${body.type}:${body.name}:${JSON.stringify(body.content ?? body.data)}`, req.root);
|
|
623
|
-
const at = req.occurredAt ??
|
|
623
|
+
const at = req.occurredAt ?? worldNow();
|
|
624
624
|
await write('dns_record', id, 'dns_record.create', {
|
|
625
625
|
zone_id: zoneID,
|
|
626
626
|
zone_name: zone.name,
|
|
@@ -687,7 +687,7 @@ export async function handleCloudflareTwinRequest(req) {
|
|
|
687
687
|
const invalid = dnsValidation(merged, zoneID, req.root, recordID);
|
|
688
688
|
if (invalid)
|
|
689
689
|
return invalid;
|
|
690
|
-
const at = req.occurredAt ??
|
|
690
|
+
const at = req.occurredAt ?? worldNow();
|
|
691
691
|
await write('dns_record', recordID, method === 'PUT' ? 'dns_record.replace' : 'dns_record.update', {
|
|
692
692
|
zone_id: zoneID,
|
|
693
693
|
name: merged.name,
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// (wrangler reads `wrangler_single_asset_uploads` from it; the twin never sets it, so the bulk
|
|
25
25
|
// multipart path is the one driven) and the twin verifies the signature, purpose, account,
|
|
26
26
|
// and expiry on redemption.
|
|
27
|
+
import { worldNow } from '@volter/world-core';
|
|
27
28
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
28
29
|
import { failure, headers as lowerHeaders, isResponse, mint, row, rows, success, write, } from "./cloudflare-common.js";
|
|
29
30
|
const TOKEN_KEY = 'volter-cloudflare-twin:workers-assets';
|
|
@@ -60,7 +61,7 @@ function readToken(token) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
function epochSeconds(req) {
|
|
63
|
-
return Math.floor(Date.parse(req.occurredAt ??
|
|
64
|
+
return Math.floor(Date.parse(req.occurredAt ?? worldNow()) / 1000);
|
|
64
65
|
}
|
|
65
66
|
function assetID(accountID, hash) {
|
|
66
67
|
return `${accountID}:${hash}`;
|
|
@@ -108,7 +109,7 @@ export async function createAssetsUploadSession(req, accountID, scriptName, body
|
|
|
108
109
|
const buckets = [];
|
|
109
110
|
for (let index = 0; index < pending.length; index += ASSET_BUCKET_SIZE)
|
|
110
111
|
buckets.push(pending.slice(index, index + ASSET_BUCKET_SIZE));
|
|
111
|
-
const at = req.occurredAt ??
|
|
112
|
+
const at = req.occurredAt ?? worldNow();
|
|
112
113
|
const id = mint('worker_asset_upload_session', `${accountID}:${scriptName}:${JSON.stringify(entries)}`, req.root);
|
|
113
114
|
await write('worker_asset_upload_session', id, 'worker_asset_upload_session.create', {
|
|
114
115
|
account_id: accountID, script_name: scriptName, manifest: entries, pending, created_on: at, deleted: false,
|
|
@@ -162,7 +163,7 @@ export async function uploadAssets(req, accountID, url, parseParts) {
|
|
|
162
163
|
const contentType = part.contentType && part.contentType !== 'application/null' ? part.contentType : null;
|
|
163
164
|
accepted.push({ hash: part.name, size, contentType });
|
|
164
165
|
}
|
|
165
|
-
const at = req.occurredAt ??
|
|
166
|
+
const at = req.occurredAt ?? worldNow();
|
|
166
167
|
for (const asset of accepted) {
|
|
167
168
|
if (row('worker_asset', assetID(accountID, asset.hash), req.root))
|
|
168
169
|
continue; // content-addressed: already held
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-cloudflare",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local Cloudflare API twin for accounts, zones, Cloudflare Tunnels, tunnel configurations and zone DNS records, built on @volter/world-core.",
|
|
5
5
|
"author": "Volter (https://github.com/volter-ai)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -35,17 +35,17 @@
|
|
|
35
35
|
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@volter/world-ui": "0.1.
|
|
38
|
+
"@volter/world-ui": "0.1.2",
|
|
39
39
|
"react": "^19.2.7",
|
|
40
40
|
"react-dom": "^19.2.7"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@volter/world-core": "2.0.
|
|
43
|
+
"@volter/world-core": "2.0.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/bun": "^1.2.20",
|
|
47
47
|
"@types/node": "^24.0.0",
|
|
48
|
-
"@volter/world-core": "2.0.
|
|
48
|
+
"@volter/world-core": "2.0.2",
|
|
49
49
|
"@volter/world-tooling": "0.1.0",
|
|
50
50
|
"cloudflare": "7.1.0",
|
|
51
51
|
"typescript": "^5.9.0"
|
package/src/cloudflare-common.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// cloudflare-twin.ts) and the provisioning slice (Workers/R2/Hyperdrive/Queues in
|
|
4
4
|
// cloudflare-provisioning.ts). Pure helpers over the @volter/world-core kernel — no routes here.
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
-
import { applyTwinWrite, projectResources } from '@volter/world-core';
|
|
6
|
+
import { applyTwinWrite, projectResources, worldNow } from '@volter/world-core';
|
|
7
7
|
|
|
8
8
|
export const SERVICE = 'cloudflare';
|
|
9
9
|
|
|
@@ -174,7 +174,7 @@ export function callerEmail(req: CloudflareRequest): string {
|
|
|
174
174
|
|
|
175
175
|
/** One entry in an account's audit log, attributed to the calling token's user. */
|
|
176
176
|
export async function audit(req: CloudflareRequest, accountID: string, entry: { type: string; resource: { type: string; id: string }; oldValue?: unknown; newValue?: unknown; metadata?: Record<string, unknown> }): Promise<void> {
|
|
177
|
-
const when = req.occurredAt ??
|
|
177
|
+
const when = req.occurredAt ?? worldNow();
|
|
178
178
|
const id = deterministicHex(`audit:${accountID}:${when}:${entry.resource.type}:${entry.resource.id}:${rows('audit_log', req.root).length}`, 32);
|
|
179
179
|
const email = callerEmail(req);
|
|
180
180
|
await write('audit_log', id, 'audit_log.record', { account_id: accountID, when, action: { type: entry.type, result: true, info: '' },
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// pack's generic conventions (1004 validation, 1001 not found) everywhere else — see README
|
|
18
18
|
// "Official grounding". No live probe was possible (no Cloudflare account); the OpenAPI + SDK
|
|
19
19
|
// are the honest grounding ceiling for this slice.
|
|
20
|
+
import { worldNow } from '@volter/world-core';
|
|
20
21
|
import { createHash } from 'node:crypto';
|
|
21
22
|
import {
|
|
22
23
|
deterministicHex,
|
|
@@ -807,7 +808,7 @@ function buildUpload(req: CloudflareRequest, url: URL, accountID: string, script
|
|
|
807
808
|
* UUID (the shape wrangler prints as "Current Version ID"). */
|
|
808
809
|
async function writeVersion(req: CloudflareRequest, accountID: string, scriptName: string, scriptTag: string, number: number, content: VersionContent, annotations: Body): Promise<Row> {
|
|
809
810
|
const id = mint('worker_version', `${accountID}:${scriptName}:${scriptTag}:${number}`, req.root);
|
|
810
|
-
const at = req.occurredAt ??
|
|
811
|
+
const at = req.occurredAt ?? worldNow();
|
|
811
812
|
await pwrite('worker_version', id, 'worker_version.upload', {
|
|
812
813
|
account_id: accountID, script_name: scriptName, script_tag: scriptTag, number, ...content,
|
|
813
814
|
source: deploySource(req), annotations, author_email: callerEmail(req) || null, created_on: at, modified_on: at, deleted: false,
|
|
@@ -819,7 +820,7 @@ function deploymentRecord(req: CloudflareRequest, script: { tag: unknown; deploy
|
|
|
819
820
|
const count = Array.isArray(script.deployments) ? script.deployments.length : 0;
|
|
820
821
|
return {
|
|
821
822
|
id: uuidFrom(`${String(script.tag)}:deployment:${count}`),
|
|
822
|
-
created_on: req.occurredAt ??
|
|
823
|
+
created_on: req.occurredAt ?? worldNow(),
|
|
823
824
|
source: deploySource(req),
|
|
824
825
|
strategy: 'percentage',
|
|
825
826
|
versions,
|
|
@@ -841,7 +842,7 @@ async function uploadScript(req: CloudflareRequest, url: URL, accountID: string,
|
|
|
841
842
|
const { existing, content } = built;
|
|
842
843
|
const tag = String(existing?.tag ?? mintScriptTag(accountID, scriptName, req.root));
|
|
843
844
|
const number = Number(existing?.version_count ?? 0) + 1;
|
|
844
|
-
const at = req.occurredAt ??
|
|
845
|
+
const at = req.occurredAt ?? worldNow();
|
|
845
846
|
// All validation passed — now perform the writes (migrations, the version, then the script).
|
|
846
847
|
await applyMigrationPlan(accountID, scriptName, built.migrationOps, req);
|
|
847
848
|
const version = await writeVersion(req, accountID, scriptName, tag, number, content, built.annotations);
|
|
@@ -1023,7 +1024,7 @@ async function createDeployment(req: CloudflareRequest, accountID: string, scrip
|
|
|
1023
1024
|
modules: null,
|
|
1024
1025
|
version_id: live.id,
|
|
1025
1026
|
deployments: [...deployments, deployment],
|
|
1026
|
-
modified_on: req.occurredAt ??
|
|
1027
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
1027
1028
|
}, req);
|
|
1028
1029
|
return success(deployment);
|
|
1029
1030
|
}
|
|
@@ -1057,7 +1058,7 @@ async function patchScriptSettings(req: CloudflareRequest, accountID: string, sc
|
|
|
1057
1058
|
...(body.tags !== undefined ? { tags: body.tags ?? [] } : {}),
|
|
1058
1059
|
...(body.tail_consumers !== undefined ? { tail_consumers: body.tail_consumers } : {}),
|
|
1059
1060
|
...(body.observability !== undefined ? { observability: body.observability } : {}),
|
|
1060
|
-
modified_on: req.occurredAt ??
|
|
1061
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
1061
1062
|
}, req);
|
|
1062
1063
|
return success(settingsView(scriptRow(accountID, String(script.name), req.root)!));
|
|
1063
1064
|
}
|
|
@@ -1249,7 +1250,7 @@ async function workersRoute(req: CloudflareRequest, method: string, url: URL, pa
|
|
|
1249
1250
|
let body: unknown;
|
|
1250
1251
|
try { body = req.body ? JSON.parse(req.body) : undefined; } catch { return failure(400, 1004, 'Invalid request body'); }
|
|
1251
1252
|
if (!Array.isArray(body)) return failure(400, 1004, 'Cron trigger update requires an array of {cron} objects');
|
|
1252
|
-
const at = req.occurredAt ??
|
|
1253
|
+
const at = req.occurredAt ?? worldNow();
|
|
1253
1254
|
const schedules: Body[] = [];
|
|
1254
1255
|
for (const value of body) {
|
|
1255
1256
|
const item = value as Body | null;
|
|
@@ -1517,7 +1518,7 @@ async function r2Route(req: CloudflareRequest, method: string, url: URL, parts:
|
|
|
1517
1518
|
if (accountBuckets().some((item) => item.name === body.name)) {
|
|
1518
1519
|
return failure(409, 10004, 'The bucket you tried to create already exists, and you own it.');
|
|
1519
1520
|
}
|
|
1520
|
-
const at = req.occurredAt ??
|
|
1521
|
+
const at = req.occurredAt ?? worldNow();
|
|
1521
1522
|
await pwrite('r2_bucket', `${accountID}:${body.name}`, 'r2_bucket.create', {
|
|
1522
1523
|
account_id: accountID,
|
|
1523
1524
|
name: body.name,
|
|
@@ -1545,7 +1546,7 @@ async function r2Route(req: CloudflareRequest, method: string, url: URL, parts:
|
|
|
1545
1546
|
if (rows('r2_object', req.root).some((o) => o.bucket_id === `${accountID}:${bucketName}`)) return failure(409, 10008, 'Cannot delete bucket that contains objects.');
|
|
1546
1547
|
await pwrite('r2_bucket', `${accountID}:${bucketName}`, 'r2_bucket.delete', { account_id: accountID, deleted: true }, req);
|
|
1547
1548
|
// "The specified domain will also remain connected to R2 until you remove it or delete the bucket." (public buckets)
|
|
1548
|
-
for (const d of r2CustomDomains(req.root).filter((x) => x.account_id === accountID && x.bucket === bucketName)) await removeR2CustomDomain(d.id, req.root, req.occurredAt ??
|
|
1549
|
+
for (const d of r2CustomDomains(req.root).filter((x) => x.account_id === accountID && x.bucket === bucketName)) await removeR2CustomDomain(d.id, req.root, req.occurredAt ?? worldNow());
|
|
1549
1550
|
return success({});
|
|
1550
1551
|
}
|
|
1551
1552
|
return noRoute();
|
|
@@ -1573,7 +1574,7 @@ async function r2TempCredentialsRoute(req: CloudflareRequest, accountID: string)
|
|
|
1573
1574
|
const prefixes = list(body.prefixes);
|
|
1574
1575
|
const objects = list(body.objects);
|
|
1575
1576
|
if (!prefixes || !objects) return failure(400, 1004, 'prefixes and objects must be lists of strings');
|
|
1576
|
-
const c = await createR2TempCredential({ accountId: accountID, bucket: body.bucket, parent, permission, ttlSeconds: ttl, prefixes, objects }, req.root, req.occurredAt ??
|
|
1577
|
+
const c = await createR2TempCredential({ accountId: accountID, bucket: body.bucket, parent, permission, ttlSeconds: ttl, prefixes, objects }, req.root, req.occurredAt ?? worldNow());
|
|
1577
1578
|
return success({ accessKeyId: c.access_key_id, secretAccessKey: c.secret_access_key, sessionToken: c.session_token });
|
|
1578
1579
|
}
|
|
1579
1580
|
|
|
@@ -1585,7 +1586,7 @@ async function r2TempCredentialsRoute(req: CloudflareRequest, accountID: string)
|
|
|
1585
1586
|
// and 1001 not-found conventions.
|
|
1586
1587
|
const TLS_VERSIONS = new Set(['1.0', '1.1', '1.2', '1.3']);
|
|
1587
1588
|
async function r2CustomDomainRoute(req: CloudflareRequest, method: string, parts: string[], accountID: string, bucketName: string): Promise<CloudflareResponse> {
|
|
1588
|
-
const at = req.occurredAt ??
|
|
1589
|
+
const at = req.occurredAt ?? worldNow();
|
|
1589
1590
|
await catchUpR2CustomDomains(req.root, at);
|
|
1590
1591
|
const mine = () => r2CustomDomains(req.root).filter((d) => d.account_id === accountID && d.bucket === bucketName);
|
|
1591
1592
|
const full = (d: R2CustomDomain): Body => ({ domain: d.domain, enabled: d.enabled, status: { ownership: d.ownership, ssl: d.ssl }, minTLS: d.min_tls, ...(d.ciphers ? { ciphers: d.ciphers } : {}), zoneId: d.zone_id, zoneName: d.zone_name });
|
|
@@ -1755,7 +1756,7 @@ async function hyperdriveRoute(req: CloudflareRequest, method: string, url: URL,
|
|
|
1755
1756
|
if (isResponse(body)) return body;
|
|
1756
1757
|
const checked = validateWrite(body);
|
|
1757
1758
|
if (isResponse(checked)) return checked;
|
|
1758
|
-
const at = req.occurredAt ??
|
|
1759
|
+
const at = req.occurredAt ?? worldNow();
|
|
1759
1760
|
const id = mint('hyperdrive_config', `${accountID}:${body.name}`, req.root);
|
|
1760
1761
|
await pwrite('hyperdrive_config', id, 'hyperdrive_config.create', {
|
|
1761
1762
|
account_id: accountID,
|
|
@@ -1789,7 +1790,7 @@ async function hyperdriveRoute(req: CloudflareRequest, method: string, url: URL,
|
|
|
1789
1790
|
caching: body.caching ?? null,
|
|
1790
1791
|
mtls: body.mtls ?? null,
|
|
1791
1792
|
origin_connection_limit: typeof body.origin_connection_limit === 'number' ? body.origin_connection_limit : null,
|
|
1792
|
-
modified_on: req.occurredAt ??
|
|
1793
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
1793
1794
|
}, req);
|
|
1794
1795
|
return success(hyperdriveView(row('hyperdrive_config', hyperdriveID, req.root)!));
|
|
1795
1796
|
}
|
|
@@ -1824,7 +1825,7 @@ async function hyperdriveRoute(req: CloudflareRequest, method: string, url: URL,
|
|
|
1824
1825
|
...(body.caching !== undefined ? { caching: body.caching } : {}),
|
|
1825
1826
|
...(body.mtls !== undefined ? { mtls: body.mtls } : {}),
|
|
1826
1827
|
...(body.origin_connection_limit !== undefined ? { origin_connection_limit: body.origin_connection_limit } : {}),
|
|
1827
|
-
modified_on: req.occurredAt ??
|
|
1828
|
+
modified_on: req.occurredAt ?? worldNow(),
|
|
1828
1829
|
}, req);
|
|
1829
1830
|
return success(hyperdriveView(row('hyperdrive_config', hyperdriveID, req.root)!));
|
|
1830
1831
|
}
|
|
@@ -1873,7 +1874,7 @@ async function queuesRoute(req: CloudflareRequest, method: string, parts: string
|
|
|
1873
1874
|
if (accountQueues().some((item) => item.queue_name === body.queue_name)) {
|
|
1874
1875
|
return failure(400, 1004, `A queue named "${body.queue_name}" already exists on this account`);
|
|
1875
1876
|
}
|
|
1876
|
-
const at = req.occurredAt ??
|
|
1877
|
+
const at = req.occurredAt ?? worldNow();
|
|
1877
1878
|
const id = mint('queue', `${accountID}:${body.queue_name}`, req.root);
|
|
1878
1879
|
await pwrite('queue', id, 'queue.create', {
|
|
1879
1880
|
account_id: accountID, queue_name: body.queue_name, created_on: at, modified_on: at, deleted: false,
|
|
@@ -1916,7 +1917,7 @@ async function queuesRoute(req: CloudflareRequest, method: string, parts: string
|
|
|
1916
1917
|
}
|
|
1917
1918
|
// A Cloudflare queue has at most one consumer (worker and http_pull are mutually exclusive).
|
|
1918
1919
|
if (queueConsumers().length > 0) return failure(400, 1004, 'This queue already has a consumer; delete it before adding another');
|
|
1919
|
-
const at = req.occurredAt ??
|
|
1920
|
+
const at = req.occurredAt ?? worldNow();
|
|
1920
1921
|
const id = mint('queue_consumer', `${queueID}:${String(body.script_name ?? 'http_pull')}`, req.root);
|
|
1921
1922
|
await pwrite('queue_consumer', id, 'queue_consumer.create', {
|
|
1922
1923
|
account_id: accountID,
|
package/src/cloudflare-twin.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// api-schemas OpenAPI 4.0.0 (commit 2ac8369e9b63dccacee1a2284e95bb819f05b307) and official
|
|
10
10
|
// cloudflare-typescript SDK (commit faaaf89ed8064a9fb54de538ec3e89487f1302b0; npm cloudflare@7.1.0).
|
|
11
11
|
import { isIP } from 'node:net';
|
|
12
|
-
import { resolveSubjectId } from '@volter/world-core';
|
|
12
|
+
import { resolveSubjectId, worldNow } from '@volter/world-core';
|
|
13
13
|
import {
|
|
14
14
|
CLOUDFLARE_RESOURCE_TYPES,
|
|
15
15
|
deterministicHex,
|
|
@@ -169,7 +169,7 @@ function auditLogRoute(req: CloudflareRequest, method: string, url: URL, parts:
|
|
|
169
169
|
async function bootstrap(req: CloudflareRequest, body: Record<string, unknown>): Promise<CloudflareResponse> {
|
|
170
170
|
const blocked = readOnly(req);
|
|
171
171
|
if (blocked) return blocked;
|
|
172
|
-
const observedAt = req.occurredAt ??
|
|
172
|
+
const observedAt = req.occurredAt ?? worldNow();
|
|
173
173
|
const accounts = Array.isArray(body.accounts) ? body.accounts : [];
|
|
174
174
|
const zones = Array.isArray(body.zones) ? body.zones : [];
|
|
175
175
|
for (const value of accounts) {
|
|
@@ -224,7 +224,7 @@ async function bootstrap(req: CloudflareRequest, body: Record<string, unknown>):
|
|
|
224
224
|
for (const value of userTokens) {
|
|
225
225
|
const item = (value ?? {}) as Record<string, unknown>;
|
|
226
226
|
if (typeof item.email !== 'string' || typeof item.token !== 'string' || !item.token) return failure(400, 1004, 'Bootstrap user_tokens require an email and a token');
|
|
227
|
-
await write('user_token', item.token, 'user_token.observe', { token: item.token, email: item.email, created_on: req.occurredAt ??
|
|
227
|
+
await write('user_token', item.token, 'user_token.observe', { token: item.token, email: item.email, created_on: req.occurredAt ?? worldNow() }, req);
|
|
228
228
|
}
|
|
229
229
|
return success({ accounts: accounts.length, zones: zones.length, members: members.length, user_tokens: userTokens.length });
|
|
230
230
|
}
|
|
@@ -343,7 +343,7 @@ async function zoneSettingRoute(req: CloudflareRequest, method: string, parts: s
|
|
|
343
343
|
const body = parseBody(req); if (isResponse(body)) return body;
|
|
344
344
|
if (typeof body.value !== 'string' || !def.values.includes(body.value)) return failure(400, 1007, `Invalid value for zone setting ${parts[3]}`);
|
|
345
345
|
const before = view().value;
|
|
346
|
-
await write('zone_setting', id, 'zone_setting.update', { zone_id: parts[1], setting: parts[3], value: body.value, modified_on: req.occurredAt ??
|
|
346
|
+
await write('zone_setting', id, 'zone_setting.update', { zone_id: parts[1], setting: parts[3], value: body.value, modified_on: req.occurredAt ?? worldNow() }, req);
|
|
347
347
|
await audit(req, String(zone.account_id), { type: 'update', resource: { type: 'zone_setting', id: parts[3]! }, oldValue: before, newValue: body.value, metadata: { zone_id: parts[1], zone_name: zone.name, setting: parts[3] } });
|
|
348
348
|
return success(view());
|
|
349
349
|
}
|
|
@@ -372,7 +372,7 @@ export async function handleCloudflareTwinRequest(req: CloudflareRequest): Promi
|
|
|
372
372
|
const blocked = readOnly(req); if (blocked) return blocked;
|
|
373
373
|
if (!/^[^@\s]+@[^@\s]+$/.test(parts[2])) return failure(400, 1001, 'A valid email is required');
|
|
374
374
|
const token = `cf-${deterministicHex(`token:${parts[2]}:${rows('user_token', req.root).length}`, 40)}`;
|
|
375
|
-
await write('user_token', token, 'user_token.create', { token, email: parts[2], created_on: req.occurredAt ??
|
|
375
|
+
await write('user_token', token, 'user_token.create', { token, email: parts[2], created_on: req.occurredAt ?? worldNow() }, req);
|
|
376
376
|
// A user's token is logged in each account the user is a member of.
|
|
377
377
|
for (const m of rows('account_member', req.root).filter((x) => x.email === parts[2])) await audit({ ...req, headers: { authorization: `Bearer ${token}` } }, String(m.account_id), { type: 'create', resource: { type: 'token', id: token.slice(0, 12) }, newValue: { owner: parts[2] } });
|
|
378
378
|
return success({ token, email: parts[2] });
|
|
@@ -464,7 +464,7 @@ export async function handleCloudflareTwinRequest(req: CloudflareRequest): Promi
|
|
|
464
464
|
if (!validTunnelSecret(body.tunnel_secret)) return failure(400, 1004, 'tunnel_secret must be canonical base64 encoding of at least 32 bytes');
|
|
465
465
|
}
|
|
466
466
|
const id = mint('cloudflare_tunnel', `${accountID}:${body.name}`, req.root);
|
|
467
|
-
const createdAt = req.occurredAt ??
|
|
467
|
+
const createdAt = req.occurredAt ?? worldNow();
|
|
468
468
|
await write('cloudflare_tunnel', id, 'tunnel.create', {
|
|
469
469
|
account_id: accountID,
|
|
470
470
|
name: body.name,
|
|
@@ -507,7 +507,7 @@ export async function handleCloudflareTwinRequest(req: CloudflareRequest): Promi
|
|
|
507
507
|
if (parts.length === 4 && method === 'DELETE') {
|
|
508
508
|
const blocked = readOnly(req);
|
|
509
509
|
if (blocked) return blocked;
|
|
510
|
-
const deletedAt = req.occurredAt ??
|
|
510
|
+
const deletedAt = req.occurredAt ?? worldNow();
|
|
511
511
|
await write('cloudflare_tunnel', tunnelID, 'tunnel.delete', { account_id: accountID, deleted: true, deleted_at: deletedAt }, req);
|
|
512
512
|
return success(tunnelView(row('cloudflare_tunnel', tunnelID, req.root, true)!));
|
|
513
513
|
}
|
|
@@ -544,7 +544,7 @@ export async function handleCloudflareTwinRequest(req: CloudflareRequest): Promi
|
|
|
544
544
|
tunnel_id: tunnelID,
|
|
545
545
|
source: 'cloudflare',
|
|
546
546
|
version: Number(current?.version ?? 0) + 1,
|
|
547
|
-
created_at: current?.created_at ?? (req.occurredAt ??
|
|
547
|
+
created_at: current?.created_at ?? (req.occurredAt ?? worldNow()),
|
|
548
548
|
config,
|
|
549
549
|
}, req);
|
|
550
550
|
return success(configurationView(row('tunnel_configuration', id, req.root)!));
|
|
@@ -577,7 +577,7 @@ export async function handleCloudflareTwinRequest(req: CloudflareRequest): Promi
|
|
|
577
577
|
const invalid = dnsValidation(body, zoneID, req.root);
|
|
578
578
|
if (invalid) return invalid;
|
|
579
579
|
const id = mint('dns_record', `${zoneID}:${body.type}:${body.name}:${JSON.stringify(body.content ?? body.data)}`, req.root);
|
|
580
|
-
const at = req.occurredAt ??
|
|
580
|
+
const at = req.occurredAt ?? worldNow();
|
|
581
581
|
await write('dns_record', id, 'dns_record.create', {
|
|
582
582
|
zone_id: zoneID,
|
|
583
583
|
zone_name: zone.name,
|
|
@@ -636,7 +636,7 @@ export async function handleCloudflareTwinRequest(req: CloudflareRequest): Promi
|
|
|
636
636
|
if (typeof merged.name === 'string') merged.name = merged.name.toLowerCase();
|
|
637
637
|
const invalid = dnsValidation(merged, zoneID, req.root, recordID);
|
|
638
638
|
if (invalid) return invalid;
|
|
639
|
-
const at = req.occurredAt ??
|
|
639
|
+
const at = req.occurredAt ?? worldNow();
|
|
640
640
|
await write('dns_record', recordID, method === 'PUT' ? 'dns_record.replace' : 'dns_record.update', {
|
|
641
641
|
zone_id: zoneID,
|
|
642
642
|
name: merged.name,
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// (wrangler reads `wrangler_single_asset_uploads` from it; the twin never sets it, so the bulk
|
|
25
25
|
// multipart path is the one driven) and the twin verifies the signature, purpose, account,
|
|
26
26
|
// and expiry on redemption.
|
|
27
|
+
import { worldNow } from '@volter/world-core';
|
|
27
28
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
28
29
|
import {
|
|
29
30
|
failure,
|
|
@@ -79,7 +80,7 @@ function readToken(token: string): TokenClaims | null {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function epochSeconds(req: CloudflareRequest): number {
|
|
82
|
-
return Math.floor(Date.parse(req.occurredAt ??
|
|
83
|
+
return Math.floor(Date.parse(req.occurredAt ?? worldNow()) / 1000);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function assetID(accountID: string, hash: string): string {
|
|
@@ -128,7 +129,7 @@ export async function createAssetsUploadSession(req: CloudflareRequest, accountI
|
|
|
128
129
|
const pending = [...new Set(Object.values(entries).map((entry) => entry.hash))].filter((hash) => !held.has(hash)).sort();
|
|
129
130
|
const buckets: string[][] = [];
|
|
130
131
|
for (let index = 0; index < pending.length; index += ASSET_BUCKET_SIZE) buckets.push(pending.slice(index, index + ASSET_BUCKET_SIZE));
|
|
131
|
-
const at = req.occurredAt ??
|
|
132
|
+
const at = req.occurredAt ?? worldNow();
|
|
132
133
|
const id = mint('worker_asset_upload_session', `${accountID}:${scriptName}:${JSON.stringify(entries)}`, req.root);
|
|
133
134
|
await write('worker_asset_upload_session', id, 'worker_asset_upload_session.create', {
|
|
134
135
|
account_id: accountID, script_name: scriptName, manifest: entries, pending, created_on: at, deleted: false,
|
|
@@ -179,7 +180,7 @@ export async function uploadAssets(req: CloudflareRequest, accountID: string, ur
|
|
|
179
180
|
const contentType = part.contentType && part.contentType !== 'application/null' ? part.contentType : null;
|
|
180
181
|
accepted.push({ hash: part.name, size, contentType });
|
|
181
182
|
}
|
|
182
|
-
const at = req.occurredAt ??
|
|
183
|
+
const at = req.occurredAt ?? worldNow();
|
|
183
184
|
for (const asset of accepted) {
|
|
184
185
|
if (row('worker_asset', assetID(accountID, asset.hash), req.root)) continue; // content-addressed: already held
|
|
185
186
|
await write('worker_asset', assetID(accountID, asset.hash), 'worker_asset.upload', {
|