@substrat-run/adapter-cloudflare 0.53.0 → 0.55.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/dist/control-plane-do.d.ts +61 -1
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +147 -0
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +12 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +99 -3
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +25 -0
- package/dist/scope-do.js.map +1 -1
- package/package.json +4 -4
package/dist/host.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { accessLogEntry, adminLogEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, connectionGrant, connectionSecret, systemGrant, entitlementGrant, entitlementGrantInput, instant, meterReading, subjectRef, createConnectionInput, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, platformRequest, } from '@substrat-run/contracts';
|
|
1
|
+
import { accessLogEntry, adminLogEntry, opsFailureEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, connectionGrant, connectionSecret, systemGrant, entitlementGrant, entitlementGrantInput, instant, meterReading, subjectRef, createConnectionInput, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, platformRequest, } from '@substrat-run/contracts';
|
|
2
2
|
import { normalizeHostname, toRouteTarget } from './route-resolver.js';
|
|
3
|
-
import { attachmentBlobKey, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, backoffAt, resolveRetryPolicy, unconfiguredSecretBox, } from '@substrat-run/kernel';
|
|
3
|
+
import { attachmentBlobKey, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, backoffAt, resolveRetryPolicy, unconfiguredSecretBox, createSubjectKeys, } from '@substrat-run/kernel';
|
|
4
4
|
import { tenantStoreDatabaseName } from './d1.js';
|
|
5
5
|
import { blobStoreBucketName, r2TenantBlobStore } from './r2.js';
|
|
6
6
|
/** DO row → contract shape. Never reads the secrets table — that is the split. */
|
|
@@ -1949,6 +1949,42 @@ export class CloudflareScopeHost {
|
|
|
1949
1949
|
backupRef: opts?.backupRef ?? null,
|
|
1950
1950
|
});
|
|
1951
1951
|
},
|
|
1952
|
+
// -- subject erasure (#37) ----------------------------------------------
|
|
1953
|
+
sealSubjectPayloads: async (actor, tenantId, scopeId, items) => {
|
|
1954
|
+
await this.assertScope(tenantId, scopeId);
|
|
1955
|
+
const sealed = await this.subjectKeysFor(tenantId, scopeId).sealMany(items);
|
|
1956
|
+
await this.recordAccess(actor, 'sealSubjectPayloads', { tenantId, scopeId }, { subjects: new Set(items.map((i) => i.subjectId)).size }, sealed.filter((s) => s !== null).length);
|
|
1957
|
+
return sealed;
|
|
1958
|
+
},
|
|
1959
|
+
openSubjectPayloads: async (actor, tenantId, scopeId, items) => {
|
|
1960
|
+
await this.assertScope(tenantId, scopeId);
|
|
1961
|
+
const opened = await this.subjectKeysFor(tenantId, scopeId).openMany(items);
|
|
1962
|
+
await this.recordAccess(actor, 'openSubjectPayloads', { tenantId, scopeId }, { subjects: new Set(items.map((i) => i.subjectId)).size }, opened.filter((o) => o !== null).length);
|
|
1963
|
+
return opened;
|
|
1964
|
+
},
|
|
1965
|
+
shredSubject: async (actor, tenantId, scopeId, subjectId) => {
|
|
1966
|
+
await this.assertScope(tenantId, scopeId);
|
|
1967
|
+
// Redact the live spine FIRST, destroy the key LAST. Both halves are idempotent and
|
|
1968
|
+
// a crash between them converges on retry, so the order is decided by which
|
|
1969
|
+
// half-done state harms the person: dying after the redaction leaves ciphertext in
|
|
1970
|
+
// a backup that no key opens; destroying the key first would leave their PII in the
|
|
1971
|
+
// live database while the audit log already claims they were erased.
|
|
1972
|
+
const eventsRedacted = await this.scopeStub(scopeId).redactSubject(subjectId);
|
|
1973
|
+
const at = new Date().toISOString();
|
|
1974
|
+
const { existed } = await this.subjectKeysFor(tenantId, scopeId).destroy(subjectId, at);
|
|
1975
|
+
const receipt = subjectShredReceipt.parse({
|
|
1976
|
+
subjectId,
|
|
1977
|
+
eventsRedacted,
|
|
1978
|
+
keyDestroyed: existed,
|
|
1979
|
+
tombstoned: true,
|
|
1980
|
+
});
|
|
1981
|
+
// BOTH logs, deliberately: the admin log because this is a mutation, the access log
|
|
1982
|
+
// because it destroys evidence. An erasure is the one action where "who asked for
|
|
1983
|
+
// this to disappear" is itself part of the record.
|
|
1984
|
+
await this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId }, null, receipt);
|
|
1985
|
+
await this.recordAccess(actor, 'shredSubject', { tenantId, scopeId }, { subjectId }, eventsRedacted);
|
|
1986
|
+
return receipt;
|
|
1987
|
+
},
|
|
1952
1988
|
grantEntitlement: async (actor, tenantId, entitlementKey, plan) => {
|
|
1953
1989
|
const input = entitlementGrantInput.parse(plan ?? {});
|
|
1954
1990
|
const result = await this.cp.grantEntitlement(tenantId, entitlementKey, input, actor);
|
|
@@ -2227,7 +2263,10 @@ export class CloudflareScopeHost {
|
|
|
2227
2263
|
pruneAccessLog: async (actor, limit) => {
|
|
2228
2264
|
const pruned = await this.cp.pruneAccessLog(limit);
|
|
2229
2265
|
if (pruned > 0) {
|
|
2230
|
-
|
|
2266
|
+
// The payload is the APPLIED state, so it belongs in `after` (contracts'
|
|
2267
|
+
// adminLogEntry: before = prior state, after = the applied payload) — the
|
|
2268
|
+
// same shape as drainAccessLog's row above (#557).
|
|
2269
|
+
await this.recordAdmin(actor, 'pruneAccessLog', { tenantId: null }, null, { pruned });
|
|
2231
2270
|
}
|
|
2232
2271
|
return pruned;
|
|
2233
2272
|
},
|
|
@@ -2252,6 +2291,41 @@ export class CloudflareScopeHost {
|
|
|
2252
2291
|
await this.recordAccess(actor, 'auditLog', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
|
|
2253
2292
|
return rows.map((r) => adminLogEntry.parse(r));
|
|
2254
2293
|
},
|
|
2294
|
+
recordOpsFailure: async (entry) => {
|
|
2295
|
+
await this.cp.recordOpsFailure({
|
|
2296
|
+
id: ulid(),
|
|
2297
|
+
actor: entry.actor,
|
|
2298
|
+
operation: entry.operation,
|
|
2299
|
+
stage: entry.stage ?? null,
|
|
2300
|
+
tenant_id: entry.tenantId ?? null,
|
|
2301
|
+
scope_id: entry.scopeId ?? null,
|
|
2302
|
+
vertical: entry.vertical ?? null,
|
|
2303
|
+
status: entry.status ?? null,
|
|
2304
|
+
// Bounded here, not trusted from the catch site: one runaway upstream body
|
|
2305
|
+
// must not become a runaway directory row (#559).
|
|
2306
|
+
message: entry.message.slice(0, 2000),
|
|
2307
|
+
reference: entry.reference ?? null,
|
|
2308
|
+
at: new Date().toISOString(),
|
|
2309
|
+
});
|
|
2310
|
+
},
|
|
2311
|
+
listOpsFailures: async (actor, filter) => {
|
|
2312
|
+
const rows = await this.cp.listOpsFailures({
|
|
2313
|
+
tenantId: filter?.tenantId,
|
|
2314
|
+
scopeId: filter?.scopeId,
|
|
2315
|
+
vertical: filter?.vertical,
|
|
2316
|
+
operation: filter?.operation,
|
|
2317
|
+
reference: filter?.reference,
|
|
2318
|
+
since: filter?.since,
|
|
2319
|
+
until: filter?.until,
|
|
2320
|
+
limit: filter?.limit,
|
|
2321
|
+
cursor: filter?.cursor,
|
|
2322
|
+
order: filter?.order,
|
|
2323
|
+
});
|
|
2324
|
+
// Rows can name tenants and scopes, so reading them is recorded like the
|
|
2325
|
+
// audit trail's own reads (K-24).
|
|
2326
|
+
await this.recordAccess(actor, 'listOpsFailures', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
|
|
2327
|
+
return rows.map((r) => opsFailureEntry.parse(r));
|
|
2328
|
+
},
|
|
2255
2329
|
};
|
|
2256
2330
|
}
|
|
2257
2331
|
// -- helpers --------------------------------------------------------------
|
|
@@ -2259,6 +2333,28 @@ export class CloudflareScopeHost {
|
|
|
2259
2333
|
* Record a staff read (K-24). `params` is a bounded summary, capped so one query
|
|
2260
2334
|
* cannot write an unbounded row.
|
|
2261
2335
|
*/
|
|
2336
|
+
/**
|
|
2337
|
+
* K-3's cross-check on its own: the (tenant, scope) pair must exist and agree before a
|
|
2338
|
+
* subject-key operation touches anything. Without it a caller could reach another
|
|
2339
|
+
* tenant's keys by naming their scope id.
|
|
2340
|
+
*/
|
|
2341
|
+
async assertScope(tenantId, scopeId) {
|
|
2342
|
+
const rec = await this.cp.getScopeRecord(tenantId, scopeId);
|
|
2343
|
+
if (!rec)
|
|
2344
|
+
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* This scope's per-subject keys (#37). The crypto lives in the kernel
|
|
2348
|
+
* (`createSubjectKeys`); the adapter supplies only the three row operations, which here
|
|
2349
|
+
* are RPCs into the control-plane DO that holds the directory.
|
|
2350
|
+
*/
|
|
2351
|
+
subjectKeysFor(tenantId, scopeId) {
|
|
2352
|
+
return createSubjectKeys(this.secretBox, {
|
|
2353
|
+
read: (subjectId) => this.cp.readSubjectKey(scopeId, subjectId),
|
|
2354
|
+
insert: (subjectId, row) => this.cp.insertSubjectKey({ scopeId, subjectId, tenantId, ...row }),
|
|
2355
|
+
tombstone: (subjectId, at) => this.cp.tombstoneSubjectKey({ scopeId, subjectId, tenantId, at }),
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2262
2358
|
async recordAccess(actor, method, target, params, resultCount) {
|
|
2263
2359
|
await this.cp.recordAccess({
|
|
2264
2360
|
id: ulid(),
|