@substrat-run/adapter-cloudflare 0.138.0 → 0.139.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 +40 -22
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +148 -44
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +25 -7
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +103 -32
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +121 -64
- package/dist/scope-do.js.map +1 -1
- package/dist/sql.d.ts +5 -1
- package/dist/sql.d.ts.map +1 -1
- package/dist/sql.js +6 -2
- package/dist/sql.js.map +1 -1
- package/package.json +12 -10
package/dist/host.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { env as ambientEnv } from 'cloudflare:workers';
|
|
2
2
|
import { isRewindRefusal, REWIND_REFUSED } from './rewind-refusal.js';
|
|
3
3
|
import { isSystemDoorMoved, SYSTEM_DOOR_MOVED, SYSTEM_DOOR_REGATES } from './system-door.js';
|
|
4
|
-
import { delegatedReadParams, operationSeriesCount, fromWireFailure, exportReadInput, exportedBatch, importBatch, importResult, importState, importsOfManifestJson, exportsOfManifestJson, importCursorMove, importCursorMoveAt, importCursorMoved, accessLogEntry, adminLogEntry, opsFailureEntry, opsFailureFingerprint, issueEntry, sweepRunEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunsPayload, modelUsageEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, capabilityExchange, mintedCapability, capabilityGrant, principalId, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchOutcome, systemScheduleEntry, systemSwitchRecord, entitlementGrant, entitlementGrantInput, capabilityFilter, instant, meterReading, subjectRef, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, delegatedReadRecord, ownerTransferAudit, copyMarkAudit, subjectShredReceipt, connectorDispatchKind, callsOfManifestJson, outboundOfManifestJson, SCOPE_GATE_REASONS, substratError, redrainEventsInput, coverage, peerCoverage, peerGrantsEntry, peerSwitch, peerSwitchOutcome, verticalCaller, verticalResolution, verticalSlug, entityObjectRef, ATTACHMENT_BLOB_BINDING, blobStoreBindingName, lifecycleDelivery, scopeLifecycle, } from '@substrat-run/contracts';
|
|
4
|
+
import { delegatedReadParams, operationSeriesCount, fromWireFailure, exportReadInput, exportedBatch, importBatch, importResult, importState, importsOfManifestJson, exportsOfManifestJson, importCursorMove, importCursorMoveAt, importCursorMoved, accessLogEntry, adminLogEntry, opsFailureEntry, opsFailureFingerprint, issueEntry, findingEntry, findingRuleEntry, sweepRunEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunsPayload, modelUsageEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, capabilityExchange, mintedCapability, capabilityGrant, principalId, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchOutcome, systemScheduleEntry, systemSwitchRecord, entitlementGrant, entitlementGrantInput, capabilityFilter, instant, meterReading, subjectRef, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, copyMarkAudit, subjectShredReceipt, connectorDispatchKind, callsOfManifestJson, outboundOfManifestJson, SCOPE_GATE_REASONS, substratError, redrainEventsInput, coverage, peerCoverage, peerGrantsEntry, peerSwitch, peerSwitchOutcome, verticalCaller, verticalResolution, verticalSlug, entityObjectRef, ATTACHMENT_BLOB_BINDING, blobStoreBindingName, lifecycleDelivery, scopeLifecycle, } from '@substrat-run/contracts';
|
|
5
5
|
import { normalizeHostname, toRouteTarget } from './route-resolver.js';
|
|
6
|
-
import { attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, KEPT_COPY_REFUSAL, capabilityTokenHash, checkBecomeInput, plausibleSessionToken, foldModelUsage, backoffAt, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, openSealed, ImpersonationRefused, assertSessionUsable, impersonationRowValues, mapImpersonationRow, newImpersonationSession, jobRunOf, assertLeaseMs, runDueJobRuns, SYSTEM_DOOR_WAIT, startJobRun, attachmentTextJob, assertAttachmentExtractors, assertJobRegistrable,
|
|
7
|
-
import {
|
|
6
|
+
import { attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, KEPT_COPY_REFUSAL, capabilityTokenHash, checkBecomeInput, plausibleSessionToken, foldModelUsage, backoffAt, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, openSealed, ImpersonationRefused, assertSessionUsable, impersonationRowValues, mapImpersonationRow, newImpersonationSession, jobRunOf, assertLeaseMs, runDueJobRuns, SYSTEM_DOOR_WAIT, startJobRun, attachmentTextJob, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, attachmentTextBackfillJob, searchLimit, searchMatchExpression, globalFetch, assertRedrainWindow, platformRequestOf, undrainedEventsOf, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, recordedOffFromWire, peerSubjectRef, SWITCH_KINDS, reassertActionOf, reassertEntry, reassertOffRow, reassertOnRow, switchActionOf, switchAuditSubject, switchNotFoundMessage, recordWriteSuperseded, switchSubjectOf, switchSupersededMessage, withRecorded, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, CrossVerticalRegistry, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, collectPeers, peerSeats, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, splitManifestMigrations, unknownRoleError, assertRowLimit, assertRowOffset, emptyImportResult, INERT_SCOPE_REASON, lifecycleReceipt, lifecycleRefusal, isPrimaryScope, isPrimaryScopeRow, asyncInvocationId, asyncLinePass, memberAddedAudit, } from '@substrat-run/kernel';
|
|
7
|
+
import { attributedView } from '@substrat-run/kernel';
|
|
8
8
|
import { isOrangeToOrange, isUpgradeRequest, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, LIVE_WITHIN_HEADER, encodeLiveWithin, liveWithinOf, } from './live-reads.js';
|
|
9
9
|
import { tenantStoreDatabaseName } from './d1.js';
|
|
10
10
|
import { blobStoreBucketName, r2TenantBlobStore } from './r2.js';
|
|
@@ -275,6 +275,7 @@ export class CloudflareScopeHost {
|
|
|
275
275
|
attachmentBuckets;
|
|
276
276
|
/** The parsers attachment text is extracted with (K-43); the host's own, never imported here. */
|
|
277
277
|
attachmentExtractors;
|
|
278
|
+
attachmentTextBounds;
|
|
278
279
|
executors = new Map();
|
|
279
280
|
/**
|
|
280
281
|
* `<moduleId>/<job>` → the pass body and its default step policy (#1577). Host
|
|
@@ -285,10 +286,10 @@ export class CloudflareScopeHost {
|
|
|
285
286
|
*/
|
|
286
287
|
jobs = new Map();
|
|
287
288
|
/**
|
|
288
|
-
* The event
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
289
|
+
* The event an executor is effecting, stamped onto the admin rows it writes (K-22). Like
|
|
290
|
+
* `onBehalfOf`, only ever set on a VIEW (`causedByView`, or `attributed` with a
|
|
291
|
+
* `causedBy`), never on the host: a field set around the handler's `await` stamped every
|
|
292
|
+
* admin call the host served meanwhile — a staff call included — with that event (#2055).
|
|
292
293
|
*/
|
|
293
294
|
causedBy = null;
|
|
294
295
|
/**
|
|
@@ -342,6 +343,7 @@ export class CloudflareScopeHost {
|
|
|
342
343
|
this.attachmentBuckets = options.attachmentBuckets ?? ambientAttachmentBucket;
|
|
343
344
|
assertAttachmentExtractors(options.attachmentExtractors ?? []);
|
|
344
345
|
this.attachmentExtractors = options.attachmentExtractors ?? [];
|
|
346
|
+
this.attachmentTextBounds = resolveAttachmentTextBounds(options.attachmentTextBounds);
|
|
345
347
|
this.fetchImpl = options.fetch ?? globalFetch;
|
|
346
348
|
this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
|
|
347
349
|
this.scopeLocalPermissions = options.scopeLocalPermissions ?? false;
|
|
@@ -571,7 +573,8 @@ export class CloudflareScopeHost {
|
|
|
571
573
|
lines.write({ ...unitOf(event.id, attempt), startedAt, outcome: 'inert' });
|
|
572
574
|
continue;
|
|
573
575
|
}
|
|
574
|
-
|
|
576
|
+
// #2055: the handler writes through a view bound to its event, never through the host.
|
|
577
|
+
const caused = this.causedByView(event.id);
|
|
575
578
|
try {
|
|
576
579
|
if (executor.kind === 'connector' && this.cpLess) {
|
|
577
580
|
// #574 phase 3: this host cannot run a connector — no connection
|
|
@@ -589,14 +592,14 @@ export class CloudflareScopeHost {
|
|
|
589
592
|
lines.write({ ...unitOf(event.id, 1), startedAt, outcome: 'routed' });
|
|
590
593
|
}
|
|
591
594
|
else if (executor.kind === 'connector') {
|
|
592
|
-
await executor.handler(await
|
|
595
|
+
await executor.handler(await caused.connectorContext(tenantId, scopeId, executor.timeoutMs, event.id), event);
|
|
593
596
|
const attempt = await stub.recordExecutorAttempt(event.id, deliveryId, null, null, invocationId);
|
|
594
597
|
report.delivered += 1;
|
|
595
598
|
outcomeOf(event, 'delivered');
|
|
596
599
|
lines.write({ ...unitOf(event.id, attempt), startedAt, outcome: 'delivered' });
|
|
597
600
|
}
|
|
598
601
|
else {
|
|
599
|
-
const result = await executor.handler(
|
|
602
|
+
const result = await executor.handler(caused.admin, event, scope);
|
|
600
603
|
// #1184: a refusal is the handler's own terminal decision — journaled with its
|
|
601
604
|
// reason, no next attempt, listed by `executorDeadLetters` beside an exhausted one.
|
|
602
605
|
const refused = isDeliveryRefusal(result) ? result : null;
|
|
@@ -634,9 +637,6 @@ export class CloudflareScopeHost {
|
|
|
634
637
|
error: err,
|
|
635
638
|
});
|
|
636
639
|
}
|
|
637
|
-
finally {
|
|
638
|
-
this.causedBy = null;
|
|
639
|
-
}
|
|
640
640
|
}
|
|
641
641
|
}
|
|
642
642
|
}
|
|
@@ -701,7 +701,7 @@ export class CloudflareScopeHost {
|
|
|
701
701
|
write: (attachmentId, outcome) => stub.attachmentTextRecord(attachmentId, outcome),
|
|
702
702
|
},
|
|
703
703
|
// K-43: the host's parsers, handed in — this adapter imports none.
|
|
704
|
-
this.attachmentExtractors);
|
|
704
|
+
this.attachmentExtractors, this.attachmentTextBounds);
|
|
705
705
|
}
|
|
706
706
|
async startJobRun(tenantId, scopeId, input) {
|
|
707
707
|
await this.assertLive(tenantId, scopeId);
|
|
@@ -713,12 +713,18 @@ export class CloudflareScopeHost {
|
|
|
713
713
|
// advance, and an archived one's never move again.
|
|
714
714
|
await this.assertLive(tenantId, scopeId);
|
|
715
715
|
await this.migrateAndRecord(scopeId);
|
|
716
|
-
// #1575: the kernel's extraction
|
|
717
|
-
// deployment registers
|
|
718
|
-
|
|
716
|
+
// #1575: the kernel's extraction jobs are this host's own, bound to this scope — no
|
|
717
|
+
// deployment registers them, and none can shadow them. The backfill starts here, on
|
|
718
|
+
// the drive, so a scope's first request never pays for attachments that predate it.
|
|
719
|
+
const stub = this.scopeStub(scopeId);
|
|
720
|
+
await stub.attachmentTextBackfillStart();
|
|
721
|
+
const kernelJobs = {
|
|
722
|
+
[ATTACHMENT_TEXT_JOB]: this.attachmentTextHandler(tenantId, scopeId),
|
|
723
|
+
[ATTACHMENT_TEXT_BACKFILL_JOB]: attachmentTextBackfillJob({ queueBatch: (after) => stub.attachmentTextBackfillBatch(after) }),
|
|
724
|
+
};
|
|
719
725
|
return runDueJobRuns({
|
|
720
726
|
store: this.jobStore(scopeId),
|
|
721
|
-
handlerFor: (run) =>
|
|
727
|
+
handlerFor: (run) => kernelJobFor(run, kernelJobs) ?? this.jobs.get(`${run.module_id}/${run.job}`),
|
|
722
728
|
now: () => new Date().toISOString(),
|
|
723
729
|
// #1834: the door is opened FOR this pass, so its "not now" is tied to this pass alone.
|
|
724
730
|
openScope: async (run, pass) => this.buildStub(tenantId, scopeId, undefined, undefined, await this.openSystemDoor(run.module_id, tenantId, scopeId, undefined, pass)),
|
|
@@ -740,13 +746,9 @@ export class CloudflareScopeHost {
|
|
|
740
746
|
// ran it. On a CP-less host `connectorContext` throws from the null control plane:
|
|
741
747
|
// fail closed, exactly the hole routing exists to avoid.
|
|
742
748
|
await this.assertLive(tenantId, scopeId);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
finally {
|
|
748
|
-
this.causedBy = null;
|
|
749
|
-
}
|
|
749
|
+
// Built on a view bound to the event, so its admin rows carry it and nothing else the
|
|
750
|
+
// host serves does (#2055).
|
|
751
|
+
await handler(await this.causedByView(event.id).connectorContext(tenantId, scopeId, options?.timeoutMs ?? 30_000, event.id), event);
|
|
750
752
|
}
|
|
751
753
|
async executorDeadLetters(tenantId, scopeId) {
|
|
752
754
|
await this.validateScopeAccess(tenantId, scopeId);
|
|
@@ -2167,6 +2169,32 @@ export class CloudflareScopeHost {
|
|
|
2167
2169
|
throw unknownRoleError(roleKey);
|
|
2168
2170
|
return coverage.parse(bound);
|
|
2169
2171
|
}
|
|
2172
|
+
async listScopeRoleHolders(tenantId, scopeId, principal) {
|
|
2173
|
+
await this.scopeRoleGate(tenantId, scopeId, 'listScopeRoleHolders');
|
|
2174
|
+
return this.scopeStub(scopeId).scopeRoleHoldersFor(scopeId, principal === undefined ? undefined : principalId.parse(principal));
|
|
2175
|
+
}
|
|
2176
|
+
async changeScopeRoleBounded(tenantId, scopeId, caller, principal, from, to) {
|
|
2177
|
+
await this.scopeRoleGate(tenantId, scopeId, 'changeScopeRoleBounded');
|
|
2178
|
+
const target = principalId.parse(principal);
|
|
2179
|
+
const bound = await this.scopeStub(scopeId).changeScopeRoleBoundedFor(tenantId, scopeId, principalId.parse(caller), target, from, to);
|
|
2180
|
+
if (bound === 'not-held')
|
|
2181
|
+
throw substratError('conflict', `${target} does not hold '${from}' at this scope`);
|
|
2182
|
+
if (bound === 'unknown-to')
|
|
2183
|
+
throw unknownRoleError(to);
|
|
2184
|
+
return coverage.parse(bound);
|
|
2185
|
+
}
|
|
2186
|
+
async revokeScopeRolesBounded(tenantId, scopeId, caller, principal) {
|
|
2187
|
+
await this.scopeRoleGate(tenantId, scopeId, 'revokeScopeRolesBounded');
|
|
2188
|
+
const result = await this.scopeStub(scopeId).revokeScopeRolesBoundedFor(tenantId, scopeId, principalId.parse(caller), principalId.parse(principal));
|
|
2189
|
+
return { coverage: coverage.parse(result.coverage), revoked: result.revoked };
|
|
2190
|
+
}
|
|
2191
|
+
/** The (tenant, scope) gate the scope-role verbs share — `assignScopeRoleBounded`'s two checks. */
|
|
2192
|
+
async scopeRoleGate(tenantId, scopeId, verb) {
|
|
2193
|
+
if (this.cpLess && !(await this.scopeStub(scopeId).servesTenant(tenantId))) {
|
|
2194
|
+
throw unknownScopeForTenant(tenantId, scopeId);
|
|
2195
|
+
}
|
|
2196
|
+
await this.peerScopeGate(tenantId, scopeId, verb);
|
|
2197
|
+
}
|
|
2170
2198
|
/**
|
|
2171
2199
|
* Where a peer verb (#1706) may reach a scope: its own ScopeDO, after the ordinary pair and
|
|
2172
2200
|
* lifecycle gate. Refused, loudly, on the SHARED control plane for a scope bound to a
|
|
@@ -2651,8 +2679,12 @@ export class CloudflareScopeHost {
|
|
|
2651
2679
|
}
|
|
2652
2680
|
// -- admin surface --------------------------------------------------------
|
|
2653
2681
|
/** #977: this host, with every admin row it writes naming who the actor acted for. */
|
|
2654
|
-
attributed(onBehalfOf) {
|
|
2655
|
-
return
|
|
2682
|
+
attributed(onBehalfOf, options) {
|
|
2683
|
+
return attributedView(this, { onBehalfOf, causedBy: options?.causedBy }, this.buildAdmin);
|
|
2684
|
+
}
|
|
2685
|
+
/** #2055: this host, with every admin row it writes naming `eventId` as its cause. */
|
|
2686
|
+
causedByView(eventId) {
|
|
2687
|
+
return attributedView(this, { causedBy: eventId }, this.buildAdmin);
|
|
2656
2688
|
}
|
|
2657
2689
|
buildAdmin() {
|
|
2658
2690
|
// The directory row → the `scope` contract. Parsed, not cast: the columns are
|
|
@@ -4156,21 +4188,25 @@ export class CloudflareScopeHost {
|
|
|
4156
4188
|
await this.recordAccess(actor, 'getOrg', { tenantId }, { orgId }, r ? 1 : 0);
|
|
4157
4189
|
return r ? mapOrg(r) : undefined;
|
|
4158
4190
|
},
|
|
4159
|
-
addMember: async (actor, tenantId, principal, orgId) => {
|
|
4191
|
+
addMember: async (actor, tenantId, principal, orgId, opts) => {
|
|
4160
4192
|
await requireOrg(tenantId, orgId);
|
|
4161
|
-
|
|
4162
|
-
await this.
|
|
4193
|
+
const expiresAt = opts?.expiresAt ?? null;
|
|
4194
|
+
await this.cp.writeTenantTuple(tenantId, `principal:${principal}`, 'member', `org:${orgId}`, expiresAt);
|
|
4195
|
+
await this.recordAdmin(actor, 'addMember', { tenantId }, null, memberAddedAudit(principal, orgId, expiresAt));
|
|
4163
4196
|
await this.fanOut(tenantId); // membership is a tenant-level tuple
|
|
4164
4197
|
},
|
|
4165
4198
|
applyMembership: async (actor, change) => {
|
|
4166
4199
|
// The row is minted here, where attribution and `causedBy` live, and written by the
|
|
4167
4200
|
// ControlPlaneDO in the same synchronous method as the fence, the bound and the tuple
|
|
4168
4201
|
// (#1184): one DO unit, so nothing lands between the check and the write.
|
|
4169
|
-
const { tenantId, principal, roleKey, op } = change;
|
|
4202
|
+
const { tenantId, principal, roleKey, op, orgId } = change;
|
|
4170
4203
|
const assignment = { principalId: principal, roleKey, node: { tenantId, scopeId: null } };
|
|
4171
4204
|
const result = await this.cp.applyMembership(change, op === 'add'
|
|
4172
4205
|
? this.adminEntry(actor, 'assignRole', { tenantId, scopeId: null }, null, assignment)
|
|
4173
|
-
: this.adminEntry(actor, 'unassignRole', { tenantId, scopeId: null }, assignment, null)
|
|
4206
|
+
: this.adminEntry(actor, 'unassignRole', { tenantId, scopeId: null }, assignment, null),
|
|
4207
|
+
// #2047: the org's own row, when the change joins or leaves one. The unit fills in its
|
|
4208
|
+
// `before`/`after` and writes it only for what it does — a join's carries the expiry.
|
|
4209
|
+
orgId ? this.adminEntry(actor, op === 'add' ? 'addMember' : 'removeMember', { tenantId }, null, null) : undefined);
|
|
4174
4210
|
// The tenant-level tuple, or its tombstone, reaches the projections.
|
|
4175
4211
|
if (result.applied && (op === 'add' || result.changed))
|
|
4176
4212
|
await this.fanOut(tenantId);
|
|
@@ -4192,6 +4228,7 @@ export class CloudflareScopeHost {
|
|
|
4192
4228
|
principal: r.subject.slice('principal:'.length),
|
|
4193
4229
|
orgId,
|
|
4194
4230
|
revokedAt: r.revoked_at,
|
|
4231
|
+
expiresAt: r.expires_at,
|
|
4195
4232
|
}));
|
|
4196
4233
|
},
|
|
4197
4234
|
createTenant: async (actor, input) => {
|
|
@@ -5085,6 +5122,11 @@ export class CloudflareScopeHost {
|
|
|
5085
5122
|
const { tenantId, scopeId, ...after } = ownerTransferAudit.parse(entry);
|
|
5086
5123
|
await this.recordAdmin(actor, 'transferOwner', { tenantId, scopeId }, null, after);
|
|
5087
5124
|
},
|
|
5125
|
+
/** #1150: one phase of a dashboard member change, written around the vertical's own. */
|
|
5126
|
+
recordMemberChange: async (actor, entry) => {
|
|
5127
|
+
const { tenantId, scopeId, ...after } = memberChangeAudit.parse(entry);
|
|
5128
|
+
await this.recordAdmin(actor, 'manageScopeMember', { tenantId, scopeId }, null, after);
|
|
5129
|
+
},
|
|
5088
5130
|
/** #2005: one change to a scope's copy marker, written around the vertical's own change. */
|
|
5089
5131
|
recordCopyMark: async (actor, entry) => {
|
|
5090
5132
|
const { tenantId, scopeId, action, ...after } = copyMarkAudit.parse(entry);
|
|
@@ -5241,6 +5283,8 @@ export class CloudflareScopeHost {
|
|
|
5241
5283
|
},
|
|
5242
5284
|
// Checked here as well as in the directory, so the refusal keeps its code across the hop.
|
|
5243
5285
|
pruneTelemetry: async (_actor, limit) => this.cp.pruneTelemetry(assertRowLimit('limit', limit)),
|
|
5286
|
+
// #1748: the DO writes each stale resolution's audit row in the same unit, from this one.
|
|
5287
|
+
pruneFindings: async (actor, limit) => this.cp.pruneFindings(assertRowLimit('limit', limit), this.adminEntry(actor, 'resolveStaleFinding', { tenantId: null }, null, null)),
|
|
5244
5288
|
listIssues: async (actor, filter) => {
|
|
5245
5289
|
const rows = await this.cp.listIssues({
|
|
5246
5290
|
status: filter?.status,
|
|
@@ -5262,6 +5306,33 @@ export class CloudflareScopeHost {
|
|
|
5262
5306
|
await this.recordAdmin(actor, 'setIssueStatus', { tenantId: null, vertical: after.lastVertical }, { fingerprint, status: before.status }, { fingerprint, status: after.status });
|
|
5263
5307
|
return after;
|
|
5264
5308
|
},
|
|
5309
|
+
listFindings: async (actor, filter) => {
|
|
5310
|
+
const rows = await this.cp.listFindings({ ...filter });
|
|
5311
|
+
await this.recordAccess(actor, 'listFindings', { tenantId: filter?.tenantId ?? null }, filter, rows.length);
|
|
5312
|
+
return rows.map((r) => findingEntry.parse(r));
|
|
5313
|
+
},
|
|
5314
|
+
// #1748: each findings mutation writes its audit row in the DO's own unit, from the row
|
|
5315
|
+
// minted here (actor, attribution) — so a failed audit write rolls the mutation back.
|
|
5316
|
+
setFindingStatus: async (actor, tenantId, id, status) => {
|
|
5317
|
+
const audit = this.adminEntry(actor, 'setFindingStatus', { tenantId }, null, null);
|
|
5318
|
+
const change = await this.cp.setFindingStatus(tenantId, id, status, new Date().toISOString(), audit);
|
|
5319
|
+
return change ? findingEntry.parse(change.after) : undefined;
|
|
5320
|
+
},
|
|
5321
|
+
createFindingRule: async (actor, tenantId, input) => {
|
|
5322
|
+
const audit = this.adminEntry(actor, 'createFindingRule', { tenantId }, null, null);
|
|
5323
|
+
const created = await this.directory('createFindingRule', tenantId, input, actor, new Date().toISOString(), audit);
|
|
5324
|
+
return { rule: findingRuleEntry.parse(created.rule), suppressed: created.suppressed.length };
|
|
5325
|
+
},
|
|
5326
|
+
revokeFindingRule: async (actor, tenantId, ruleId) => {
|
|
5327
|
+
const audit = this.adminEntry(actor, 'revokeFindingRule', { tenantId }, null, null);
|
|
5328
|
+
const change = await this.cp.revokeFindingRule(tenantId, ruleId, new Date().toISOString(), audit);
|
|
5329
|
+
return change ? findingRuleEntry.parse(change.after) : undefined;
|
|
5330
|
+
},
|
|
5331
|
+
listFindingRules: async (actor, tenantId, filter) => {
|
|
5332
|
+
const rows = await this.cp.listFindingRules(tenantId, filter?.active ? new Date().toISOString() : undefined, filter?.limit);
|
|
5333
|
+
await this.recordAccess(actor, 'listFindingRules', { tenantId }, filter, rows.length);
|
|
5334
|
+
return rows.map((r) => findingRuleEntry.parse(r));
|
|
5335
|
+
},
|
|
5265
5336
|
recordModelUsage: async (input) => {
|
|
5266
5337
|
const l = input.line;
|
|
5267
5338
|
return this.cp.recordModelUsage({
|