@substrat-run/adapter-cloudflare 0.98.0 → 0.102.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 +73 -1
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +231 -3
- package/dist/control-plane-do.js.map +1 -1
- package/dist/d1.d.ts +1 -1
- package/dist/d1.d.ts.map +1 -1
- package/dist/d1.js +2 -1
- package/dist/d1.js.map +1 -1
- package/dist/host.d.ts +23 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +145 -4
- package/dist/host.js.map +1 -1
- package/dist/platform-sweeper-do.d.ts +2 -1
- package/dist/platform-sweeper-do.d.ts.map +1 -1
- package/dist/platform-sweeper-do.js +2 -1
- package/dist/platform-sweeper-do.js.map +1 -1
- package/dist/r2.d.ts +1 -1
- package/dist/r2.d.ts.map +1 -1
- package/dist/r2.js +2 -1
- package/dist/r2.js.map +1 -1
- package/dist/scope-do.d.ts +8 -0
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +143 -44
- package/dist/scope-do.js.map +1 -1
- package/dist/scope-sweeper-do.d.ts +20 -3
- package/dist/scope-sweeper-do.d.ts.map +1 -1
- package/dist/scope-sweeper-do.js +53 -0
- package/dist/scope-sweeper-do.js.map +1 -1
- package/package.json +4 -4
package/dist/host.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fromWireFailure, accessLogEntry, adminLogEntry, opsFailureEntry, modelUsageEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, entitlementGrant, entitlementGrantInput, instant, meterReading, subjectRef, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, platformRequest, connectorDispatchKind, outboundOfManifestJson, substratError, } from '@substrat-run/contracts';
|
|
1
|
+
import { fromWireFailure, accessLogEntry, adminLogEntry, opsFailureEntry, opsFailureFingerprint, issueEntry, sweepRunEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunsPayload, modelUsageEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, entitlementGrant, entitlementGrantInput, instant, meterReading, subjectRef, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, platformRequest, connectorDispatchKind, outboundOfManifestJson, substratError, } from '@substrat-run/contracts';
|
|
2
2
|
import { normalizeHostname, toRouteTarget } from './route-resolver.js';
|
|
3
|
-
import { attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, foldModelUsage, backoffAt, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, openSealed, ImpersonationRefused, assertSessionUsable, impersonationRowValues, mapImpersonationRow, newImpersonationSession, } from '@substrat-run/kernel';
|
|
3
|
+
import { attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, foldModelUsage, backoffAt, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, openSealed, ImpersonationRefused, assertSessionUsable, impersonationRowValues, mapImpersonationRow, newImpersonationSession, globalFetch, } 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. */
|
|
@@ -81,6 +81,10 @@ export class CloudflareScopeHost {
|
|
|
81
81
|
moduleIds = new Set();
|
|
82
82
|
/** Module id → its declared recurring schedules (#383), for `registeredSchedules`/`runDueSchedules`. */
|
|
83
83
|
moduleSchedules = new Map();
|
|
84
|
+
/** #1232: declared freshness per module. Registered UNCONDITIONALLY — the schedule
|
|
85
|
+
* map is only populated for modules WITH schedules, and hanging freshness off it
|
|
86
|
+
* would drop a freshness-only module at registration. */
|
|
87
|
+
moduleFreshness = new Map();
|
|
84
88
|
/** Registered (module, version) pairs — the frontier `schemaVersion` counts toward (§5.3, #49). */
|
|
85
89
|
migrationTotal = 0;
|
|
86
90
|
operations = new Set();
|
|
@@ -122,7 +126,7 @@ export class CloudflareScopeHost {
|
|
|
122
126
|
this.tenantStores = options.tenantStores;
|
|
123
127
|
this.blobStores = options.blobStores;
|
|
124
128
|
this.attachmentBuckets = options.attachmentBuckets;
|
|
125
|
-
this.fetchImpl = options.fetch ??
|
|
129
|
+
this.fetchImpl = options.fetch ?? globalFetch;
|
|
126
130
|
this.scopeLocalPermissions = options.scopeLocalPermissions ?? false;
|
|
127
131
|
this.scopeNs = options.scope;
|
|
128
132
|
this.cpLess = !options.controlPlane;
|
|
@@ -326,6 +330,14 @@ export class CloudflareScopeHost {
|
|
|
326
330
|
await this.migrateAndRecord(scopeId);
|
|
327
331
|
return this.scopeStub(scopeId).executorDeadLetters();
|
|
328
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* #1232: hand one pass's schedule outcomes to the scope's own intent journal —
|
|
335
|
+
* the CP-less deployment's only road to `_substrat_sweep_runs`. Null = the
|
|
336
|
+
* journal is full and the report was deliberately dropped, never queued late.
|
|
337
|
+
*/
|
|
338
|
+
async enqueueSweepRuns(scopeId, payload) {
|
|
339
|
+
return this.scopeStub(scopeId).enqueueSweepRuns(JSON.stringify(sweepRunsPayload.parse(payload)), JSON.stringify({ system: 'scope-sweeper' }));
|
|
340
|
+
}
|
|
329
341
|
async listPlatformRequests(tenantId, scopeId) {
|
|
330
342
|
await this.cp.validateScopeAccess(tenantId, scopeId);
|
|
331
343
|
await this.migrateAndRecord(scopeId);
|
|
@@ -531,6 +543,9 @@ export class CloudflareScopeHost {
|
|
|
531
543
|
if (manifest.schedules && manifest.schedules.length > 0) {
|
|
532
544
|
this.moduleSchedules.set(manifest.id, manifest.schedules);
|
|
533
545
|
}
|
|
546
|
+
if (manifest.freshness && manifest.freshness.length > 0) {
|
|
547
|
+
this.moduleFreshness.set(manifest.id, manifest.freshness);
|
|
548
|
+
}
|
|
534
549
|
this.migrationTotal += migrations.length;
|
|
535
550
|
const ownOperations = new Set(Object.keys(registration.operations ?? {}));
|
|
536
551
|
for (const name of manifest.withdraws ?? []) {
|
|
@@ -1075,8 +1090,64 @@ export class CloudflareScopeHost {
|
|
|
1075
1090
|
}
|
|
1076
1091
|
return out;
|
|
1077
1092
|
}
|
|
1093
|
+
registeredFreshness() {
|
|
1094
|
+
const out = [];
|
|
1095
|
+
for (const [moduleId, freshness] of this.moduleFreshness) {
|
|
1096
|
+
if (freshness.length > 0)
|
|
1097
|
+
out.push({ moduleId: moduleId, freshness });
|
|
1098
|
+
}
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* The freshness evaluator (#1232), CF half — same verdicts and gating as the pure
|
|
1103
|
+
* adapter's, over one `freshnessProbe` round trip to the scope DO. Aggregates
|
|
1104
|
+
* across EVERY registered module (two modules declaring one type share one
|
|
1105
|
+
* gating-state key and one dedupe unit — per-module evaluation would corrupt
|
|
1106
|
+
* both), collapsing duplicates to the tightest window; called once per scope,
|
|
1107
|
+
* any registered module's id as the entry ticket. NOT gated on the system
|
|
1108
|
+
* grant — see the kernel interface doc.
|
|
1109
|
+
*/
|
|
1110
|
+
async checkFreshness(moduleId, tenantId, scopeId) {
|
|
1111
|
+
const report = { checks: [] };
|
|
1112
|
+
if (!this.moduleIds.has(moduleId))
|
|
1113
|
+
return report;
|
|
1114
|
+
if (!this.cpLess) {
|
|
1115
|
+
const rec = await this.cp.getScopeRecord(tenantId, scopeId);
|
|
1116
|
+
if (!rec || rec.status !== 'active')
|
|
1117
|
+
return report;
|
|
1118
|
+
}
|
|
1119
|
+
const stub = this.scopeStub(scopeId);
|
|
1120
|
+
const windows = new Map();
|
|
1121
|
+
for (const specs of this.moduleFreshness.values()) {
|
|
1122
|
+
for (const f of specs) {
|
|
1123
|
+
const prev = windows.get(f.eventType);
|
|
1124
|
+
if (prev === undefined || f.within.hours < prev)
|
|
1125
|
+
windows.set(f.eventType, f.within.hours);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (windows.size === 0)
|
|
1129
|
+
return report;
|
|
1130
|
+
const probe = await stub.freshnessProbe([...windows.keys()]);
|
|
1131
|
+
const now = Date.now();
|
|
1132
|
+
const nowIso = new Date(now).toISOString();
|
|
1133
|
+
for (const [eventType, withinHours] of windows) {
|
|
1134
|
+
const p = probe[eventType] ?? { observedAt: null, stateAt: null, stateOutcome: null };
|
|
1135
|
+
const outcome = p.observedAt === null
|
|
1136
|
+
? 'skipped' // never observed — the never-run analogue, not a failure
|
|
1137
|
+
: now - Date.parse(p.observedAt) <= withinHours * 3_600_000
|
|
1138
|
+
? 'ok'
|
|
1139
|
+
: 'failed';
|
|
1140
|
+
const changed = p.stateOutcome !== outcome;
|
|
1141
|
+
const heartbeatDue = p.stateAt === null || now - Date.parse(p.stateAt) > FRESHNESS_HEARTBEAT_MINUTES * 60_000;
|
|
1142
|
+
if (!changed && !heartbeatDue)
|
|
1143
|
+
continue;
|
|
1144
|
+
await stub.recordScheduleRun(`freshness:${eventType}`, nowIso, outcome);
|
|
1145
|
+
report.checks.push({ eventType, outcome, observedAt: p.observedAt, withinHours });
|
|
1146
|
+
}
|
|
1147
|
+
return report;
|
|
1148
|
+
}
|
|
1078
1149
|
async runDueSchedules(moduleId, tenantId, scopeId) {
|
|
1079
|
-
const report = { fired: 0, skipped: 0, failed: 0, errors: [] };
|
|
1150
|
+
const report = { fired: 0, skipped: 0, failed: 0, errors: [], runs: [] };
|
|
1080
1151
|
const schedules = this.moduleSchedules.get(moduleId);
|
|
1081
1152
|
if (!schedules || schedules.length === 0)
|
|
1082
1153
|
return report;
|
|
@@ -1103,6 +1174,7 @@ export class CloudflareScopeHost {
|
|
|
1103
1174
|
const dueAt = lastRun === null ? -Infinity : lastRun + schedule.cadence.everyMinutes * 60_000;
|
|
1104
1175
|
if (now < dueAt) {
|
|
1105
1176
|
report.skipped += 1;
|
|
1177
|
+
report.runs.push({ operation: schedule.operation, outcome: 'skipped' });
|
|
1106
1178
|
continue;
|
|
1107
1179
|
}
|
|
1108
1180
|
let status = 'ok';
|
|
@@ -1120,6 +1192,7 @@ export class CloudflareScopeHost {
|
|
|
1120
1192
|
});
|
|
1121
1193
|
}
|
|
1122
1194
|
await stub.recordScheduleRun(schedule.operation, new Date(now).toISOString(), status);
|
|
1195
|
+
report.runs.push({ operation: schedule.operation, outcome: status === 'ok' ? 'ok' : 'failed' });
|
|
1123
1196
|
}
|
|
1124
1197
|
return report;
|
|
1125
1198
|
}
|
|
@@ -2702,11 +2775,15 @@ export class CloudflareScopeHost {
|
|
|
2702
2775
|
tenant_id: entry.tenantId ?? null,
|
|
2703
2776
|
scope_id: entry.scopeId ?? null,
|
|
2704
2777
|
vertical: entry.vertical ?? null,
|
|
2778
|
+
version: entry.version ?? null,
|
|
2705
2779
|
status: entry.status ?? null,
|
|
2706
2780
|
// Bounded here, not trusted from the catch site: one runaway upstream body
|
|
2707
2781
|
// must not become a runaway directory row (#559).
|
|
2708
2782
|
message: entry.message.slice(0, 2000),
|
|
2709
2783
|
reference: entry.reference ?? null,
|
|
2784
|
+
origin: entry.origin ?? null,
|
|
2785
|
+
code: entry.code ?? null,
|
|
2786
|
+
fingerprint: opsFailureFingerprint(entry),
|
|
2710
2787
|
at: new Date().toISOString(),
|
|
2711
2788
|
});
|
|
2712
2789
|
},
|
|
@@ -2715,7 +2792,10 @@ export class CloudflareScopeHost {
|
|
|
2715
2792
|
tenantId: filter?.tenantId,
|
|
2716
2793
|
scopeId: filter?.scopeId,
|
|
2717
2794
|
vertical: filter?.vertical,
|
|
2795
|
+
version: filter?.version,
|
|
2718
2796
|
operation: filter?.operation,
|
|
2797
|
+
code: filter?.code,
|
|
2798
|
+
fingerprint: filter?.fingerprint,
|
|
2719
2799
|
reference: filter?.reference,
|
|
2720
2800
|
since: filter?.since,
|
|
2721
2801
|
until: filter?.until,
|
|
@@ -2728,6 +2808,67 @@ export class CloudflareScopeHost {
|
|
|
2728
2808
|
await this.recordAccess(actor, 'listOpsFailures', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
|
|
2729
2809
|
return rows.map((r) => opsFailureEntry.parse(r));
|
|
2730
2810
|
},
|
|
2811
|
+
recordSweepRun: async (entry) => {
|
|
2812
|
+
await this.cp.recordSweepRun({
|
|
2813
|
+
id: ulid(),
|
|
2814
|
+
kind: entry.kind,
|
|
2815
|
+
unit: entry.unit,
|
|
2816
|
+
outcome: entry.outcome,
|
|
2817
|
+
tenant_id: entry.tenantId ?? null,
|
|
2818
|
+
scope_id: entry.scopeId ?? null,
|
|
2819
|
+
vertical: entry.vertical ?? null,
|
|
2820
|
+
version: entry.version ?? null,
|
|
2821
|
+
operation: entry.operation ?? null,
|
|
2822
|
+
connection_id: entry.connectionId ?? null,
|
|
2823
|
+
// Bounded here, not trusted from the sweep (the #559 rule).
|
|
2824
|
+
error: entry.error == null ? null : entry.error.slice(0, 2000),
|
|
2825
|
+
elapsed_ms: entry.elapsedMs ?? null,
|
|
2826
|
+
request_id: entry.requestId ?? null,
|
|
2827
|
+
event_type: entry.eventType ?? null,
|
|
2828
|
+
observed_at: entry.observedAt ?? null,
|
|
2829
|
+
at: entry.at ?? new Date().toISOString(),
|
|
2830
|
+
});
|
|
2831
|
+
},
|
|
2832
|
+
listSweepRuns: async (actor, filter) => {
|
|
2833
|
+
const rows = await this.cp.listSweepRuns({
|
|
2834
|
+
kind: filter?.kind,
|
|
2835
|
+
unit: filter?.unit,
|
|
2836
|
+
outcome: filter?.outcome,
|
|
2837
|
+
tenantId: filter?.tenantId,
|
|
2838
|
+
scopeId: filter?.scopeId,
|
|
2839
|
+
vertical: filter?.vertical,
|
|
2840
|
+
connectionId: filter?.connectionId,
|
|
2841
|
+
since: filter?.since,
|
|
2842
|
+
until: filter?.until,
|
|
2843
|
+
limit: filter?.limit,
|
|
2844
|
+
cursor: filter?.cursor,
|
|
2845
|
+
order: filter?.order,
|
|
2846
|
+
});
|
|
2847
|
+
// Rows can name tenants and scopes, so reading them is recorded (K-24).
|
|
2848
|
+
await this.recordAccess(actor, 'listSweepRuns', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
|
|
2849
|
+
return rows.map((r) => sweepRunEntry.parse(r));
|
|
2850
|
+
},
|
|
2851
|
+
listIssues: async (actor, filter) => {
|
|
2852
|
+
const rows = await this.cp.listIssues({
|
|
2853
|
+
status: filter?.status,
|
|
2854
|
+
operation: filter?.operation,
|
|
2855
|
+
code: filter?.code,
|
|
2856
|
+
limit: filter?.limit,
|
|
2857
|
+
});
|
|
2858
|
+
// Issues aggregate fleet-wide failures; the read is recorded like the rows' own (K-24).
|
|
2859
|
+
await this.recordAccess(actor, 'listIssues', {}, filter, rows.length);
|
|
2860
|
+
return rows.map((r) => issueEntry.parse(r));
|
|
2861
|
+
},
|
|
2862
|
+
setIssueStatus: async (actor, fingerprint, status) => {
|
|
2863
|
+
const change = await this.cp.setIssueStatus(fingerprint, status, new Date().toISOString());
|
|
2864
|
+
if (!change)
|
|
2865
|
+
return undefined;
|
|
2866
|
+
const before = issueEntry.parse(change.before);
|
|
2867
|
+
const after = issueEntry.parse(change.after);
|
|
2868
|
+
// A lifecycle flip is a staff mutation — audited with the diff (K-33).
|
|
2869
|
+
await this.recordAdmin(actor, 'setIssueStatus', { tenantId: null, vertical: after.lastVertical }, { fingerprint, status: before.status }, { fingerprint, status: after.status });
|
|
2870
|
+
return after;
|
|
2871
|
+
},
|
|
2731
2872
|
recordModelUsage: async (input) => {
|
|
2732
2873
|
const l = input.line;
|
|
2733
2874
|
return this.cp.recordModelUsage({
|