@substrat-run/adapter-cloudflare 0.133.0 → 0.135.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.
@@ -1 +1 @@
1
- {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAcL,KAAK,WAAW,EAsCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAoCL,KAAK,SAAS,EAmBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAuEtB,MAAM,sBAAsB,CAAC;AAsF9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AA0bD;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAgDxG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAslKjE"}
1
+ {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAcL,KAAK,WAAW,EAwCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAuCL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EA+EtB,MAAM,sBAAsB,CAAC;AAyF9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AA6bD;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAgDxG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CA+qKjE"}
package/dist/scope-do.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { REWIND_REFUSED } from './rewind-refusal.js';
2
2
  import { DurableObject } from 'cloudflare:workers';
3
3
  import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, toWireFailure, grantRefFromProof, principalId, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, requestFingerprint, substratError, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
4
- import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, IDEMPOTENCY_DDL, REFUSALS_DDL, refusalInsert, refusedTransitionOf, assertIdempotencyKey, assertPermissionKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, exportedSinceQuery, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, jobRunListLimit, } from '@substrat-run/kernel';
4
+ import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, redactSubjectScopeText, assertRowLimit, assertRowOffset, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, recordAttachmentText, searchAttachments, IDEMPOTENCY_DDL, REFUSALS_DDL, refusalInsert, refusedTransitionOf, assertIdempotencyKey, assertPermissionKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, exportedSinceQuery, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, jobRunListLimit, } from '@substrat-run/kernel';
5
5
  import { isUpgradeRequest, readSubscription, LIVE_FANOUT_LIMIT, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, } from './live-reads.js';
6
6
  import { OperationQueue } from './serialization.js';
7
- import { doScopedSql, doBuiltColumnsOf, doSpineSql } from './sql.js';
8
- import { actorOf, admitPeer, collectPeers, switchPeer, assertNoSecret, CAPABILITY_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, resolveCapabilitySession, revokeCapabilityAsPlatform, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
7
+ import { doScopedSql, doBuiltColumnsOf, doRedactionSql, doSpineSql } from './sql.js';
8
+ import { actorOf, admitPeer, collectPeers, switchPeer, assertNoSecret, CAPABILITY_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, resolveCapabilitySession, revokeCapabilityAsPlatform, readCapabilityPage, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
9
9
  import { createDoTupleChecker, createLocalControlPlaneReader } from './checker.js';
10
10
  import { assertNoSpineReference, assertSpineTablesBuilt, dumpRowsInsert, isSpineTable, repointScopeGrants, spineColumnAdditions } from '@substrat-run/kernel';
11
11
  /**
@@ -312,6 +312,9 @@ const KERNEL_DDL = `
312
312
  );
313
313
  CREATE INDEX IF NOT EXISTS _substrat_attachments_entity
314
314
  ON _substrat_attachments (entity_type, entity_id);
315
+ -- #1575: attachment text and its FTS5 index, shared from @substrat-run/kernel. After
316
+ -- _substrat_attachments, whose delete trigger it adds.
317
+ ${ATTACHMENT_TEXT_DDL}
315
318
  -- #901: an entity's version is the ULID of the last event about it, so
316
319
  -- MAX(id) per (entity_type, entity_id) is the read. The id column sits last so
317
320
  -- SQLite walks to the end of the matched range instead of aggregating over it.
@@ -1383,6 +1386,9 @@ export function defineScopeDO(modules, bareOps) {
1383
1386
  * where the control plane is unreachable by the sandbox contract and the projected
1384
1387
  * entitlements are the source of truth. A console-managed scope is gated on the
1385
1388
  * coordinator against the shared CP, so this is left undefined / a no-op there.
1389
+ * Also undefined for a module's own declared schedule through the system door
1390
+ * (#1654): the coordinator resolves the key with the kernel's `requiredEntitlementFor`,
1391
+ * which exempts exactly that invoke, so the DO enforces whatever it is handed.
1386
1392
  */
1387
1393
  requiredEntitlement,
1388
1394
  /**
@@ -2089,19 +2095,7 @@ export function defineScopeDO(modules, bareOps) {
2089
2095
  const row = this.sql
2090
2096
  .exec('SELECT * FROM _substrat_attachments WHERE id = ?', attachmentId)
2091
2097
  .toArray()[0];
2092
- if (!row)
2093
- return null;
2094
- return attachmentRecord.parse({
2095
- id: row.id,
2096
- entity: { entityType: row.entity_type, entityId: row.entity_id },
2097
- filename: row.filename,
2098
- contentType: row.content_type,
2099
- size: Number(row.size),
2100
- sha256: row.sha256,
2101
- visibility: row.visibility,
2102
- createdBy: row.created_by,
2103
- createdAt: row.created_at,
2104
- });
2098
+ return row ? attachmentRecordOfRow(row) : null;
2105
2099
  }
2106
2100
  /** Record an uploaded attachment: write gate + row + `attachment.added`, one txn. */
2107
2101
  async attachmentAdd(record, principal, tenantId, scopeId,
@@ -2131,6 +2125,9 @@ export function defineScopeDO(modules, bareOps) {
2131
2125
  piiClass: 'none',
2132
2126
  payload: { attachment: parsed },
2133
2127
  });
2128
+ // #1575: queue its text extraction in the same transaction — a `pending` row
2129
+ // and a job run. Nothing is extracted here, so nothing here can fail the upload.
2130
+ enqueueAttachmentText(doSpineSql(this.sql), parsed.id, ulid(), new Date().toISOString());
2134
2131
  });
2135
2132
  }
2136
2133
  catch (err) {
@@ -2263,6 +2260,49 @@ export function defineScopeDO(modules, bareOps) {
2263
2260
  return record;
2264
2261
  });
2265
2262
  }
2263
+ // -- attachment text (#1575) ---------------------------------------------------
2264
+ /**
2265
+ * Extracted-text search as `ctx`'s subject: authorized first — the check `open` makes,
2266
+ * the target's read key on the owning entity — then matched over readable owners only
2267
+ * (`searchAttachments`). The term and the limit are judged here again, never trusted
2268
+ * from the coordinator.
2269
+ */
2270
+ searchAttachmentsAs(ctx, term, limit) {
2271
+ return searchAttachments(doSpineSql(this.sql), { targets: this.attachmentTargets, check: (permission, entity) => ctx.check(permission, entity) }, term, limit);
2272
+ }
2273
+ /**
2274
+ * #1575: `ScopeAttachments.search` for a principal or a connection. Its failure travels as
2275
+ * DATA, the capability verbs' envelope, because a throw across this RPC keeps only its
2276
+ * message — and the too-many-owners refusal's `reason` is what a UI explains it by.
2277
+ */
2278
+ async attachmentSearch(term, limit, principal, tenantId, scopeId, connectionId) {
2279
+ await this.ensureMigrations();
2280
+ try {
2281
+ const ctx = this.operationContext(principal, tenantId, scopeId, undefined, connectionId, undefined, undefined, undefined, 'attachments.search');
2282
+ return { value: await this.searchAttachmentsAs(ctx, term, limit) };
2283
+ }
2284
+ catch (err) {
2285
+ return { failure: toWireFailure(err) };
2286
+ }
2287
+ }
2288
+ /**
2289
+ * #1575: the extraction job's read of one record. No gate, deliberately — extraction
2290
+ * is the kernel's own derivation and the coordinator is its only caller; what a person
2291
+ * learns is gated at search.
2292
+ */
2293
+ async attachmentTextSource(attachmentId) {
2294
+ await this.ensureMigrations();
2295
+ return this.attachmentRow(attachmentId);
2296
+ }
2297
+ /**
2298
+ * #1575: write one extraction outcome, guarded on the attachment still existing.
2299
+ * Through the queue, so it can never land inside an invoke's or an upload's open
2300
+ * transaction and roll back with it.
2301
+ */
2302
+ async attachmentTextRecord(attachmentId, outcome) {
2303
+ await this.ensureMigrations();
2304
+ return this.queue.enqueue(async () => this.ctx.storage.transactionSync(() => recordAttachmentText(doSpineSql(this.sql), attachmentId, outcome, new Date().toISOString())));
2305
+ }
2266
2306
  // -- attachments through a capability (#1686) -------------------------------
2267
2307
  // The coordinator hashed the session token; only the hash arrives. Each verb resolves it
2268
2308
  // INSIDE the queue, as `invoke` does, so nothing can revoke between the resolution and
@@ -2332,6 +2372,11 @@ export function defineScopeDO(modules, bareOps) {
2332
2372
  return record;
2333
2373
  });
2334
2374
  }
2375
+ /** #1575: extracted-text search as the capability — its keys, its subtree, its minter now. */
2376
+ async capabilityAttachmentSearch(term, limit, sessionHash, tenantId, scopeId) {
2377
+ await this.ensureMigrations();
2378
+ return this.asCapability(sessionHash, tenantId, scopeId, 'attachments.search', (ctx) => this.searchAttachmentsAs(ctx, term, limit));
2379
+ }
2335
2380
  /**
2336
2381
  * Refuse an upload or a remove through a capability, and record it. Throws for an
2337
2382
  * unknown attachment id too — the same answer either way — but only a known target has
@@ -2708,6 +2753,15 @@ export function defineScopeDO(modules, bareOps) {
2708
2753
  await this.ensureMigrations();
2709
2754
  return await this.queue.enqueue(() => this.ctx.storage.transactionSync(() => revokeCapabilityAsPlatform(doSpineSql(this.sql), id, actor, instant.parse(new Date().toISOString()))) ?? null);
2710
2755
  }
2756
+ /**
2757
+ * The operator's read of this scope's capabilities (#1686). The kernel's one read
2758
+ * (`readCapabilityPage`): no `token_hash` selected, decoded to a record with no field to hold one.
2759
+ * Authorization and the K-3 cross-check happen on the coordinator first.
2760
+ */
2761
+ async listCapabilities(filter) {
2762
+ await this.ensureMigrations();
2763
+ return readCapabilityPage(doSpineSql(this.sql), filter);
2764
+ }
2711
2765
  /**
2712
2766
  * The last time a schedule's operation ran on this scope (#383), or null.
2713
2767
  *
@@ -3284,8 +3338,10 @@ export function defineScopeDO(modules, bareOps) {
3284
3338
  .toArray().map((r) => r.name));
3285
3339
  if (!known.has(table))
3286
3340
  throw new Error(`unknown table '${table}'`);
3287
- const l = Math.min(Math.max(1, limit), SCOPE_TABLE_PAGE_MAX);
3288
- const o = Math.max(0, offset);
3341
+ // The ceiling clamps; a bound SQLite would misread (NaN, non-finite, fractional,
3342
+ // negative) is refused instead of reaching LIMIT / OFFSET (#1632).
3343
+ const l = Math.min(assertRowLimit('limit', limit), SCOPE_TABLE_PAGE_MAX);
3344
+ const o = assertRowOffset('offset', offset);
3289
3345
  const cursor = this.sql.exec(`SELECT * FROM "${table}" LIMIT ? OFFSET ?`, l, o);
3290
3346
  const columns = cursor.columnNames;
3291
3347
  const rows = Array.from(cursor.raw(), (row) => row.map(cellToJson));
@@ -3338,6 +3394,14 @@ export function defineScopeDO(modules, bareOps) {
3338
3394
  const q = denialListQuery(filter);
3339
3395
  return this.sql.exec(q.sql, ...q.params).toArray().map(mapDenialRow);
3340
3396
  }
3397
+ /**
3398
+ * #1745: the refusal log — every lifecycle move refused and failed with, recorded after
3399
+ * the rollback. Authorization and the K-3 cross-check happen on the coordinator first.
3400
+ */
3401
+ listRefusals(filter) {
3402
+ const q = refusalListQuery(filter);
3403
+ return this.sql.exec(q.sql, ...q.params).toArray().map(mapRefusalRow);
3404
+ }
3341
3405
  /**
3342
3406
  * The same log bucketed per (actor, permission) — or per operation when the filter
3343
3407
  * says `groupBy: 'operation'` (#1456) — with the window's own facts.
@@ -3601,6 +3665,10 @@ export function defineScopeDO(modules, bareOps) {
3601
3665
  // not this one. Dropped, so a restore never carries a receipt over; the repair
3602
3666
  // projection that follows writes this scope's own.
3603
3667
  this.sql.exec(`DELETE FROM _substrat_meta WHERE key = 'provisioned_for'`);
3668
+ // #1575: attachment text is not in a dump, so the load left it as it was. Drop the
3669
+ // text of attachments the dump did not bring back, and queue extraction for those
3670
+ // it brought back without text — the bytes decide what that run finds.
3671
+ reconcileAttachmentText(doSpineSql(this.sql), ulid, new Date().toISOString());
3604
3672
  // Re-point the restored grants at THIS scope (after the spine exists, so a dump
3605
3673
  // that carried no tuples table still finds one here).
3606
3674
  if (destScopeId) {
@@ -3747,12 +3815,16 @@ export function defineScopeDO(modules, bareOps) {
3747
3815
  // A failed delivery's error text about one of those events (#1632) — keyed by event
3748
3816
  // id, so the outbox predicate names it. Not a copy of the event; not counted.
3749
3817
  this.sql.exec(DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, REDACTED_DELIVERY_NOTE, subjectId);
3818
+ const sql = doRedactionSql(this.sql);
3750
3819
  return {
3751
3820
  events: doomed.length,
3752
3821
  intents: this.redactSubjectIntents(subjectId, at),
3753
3822
  // The job-run tables (#1632) — the kernel's walk, so the pure host runs the
3754
3823
  // identical SQL.
3755
- jobRuns: redactSubjectJobRuns((sql, params) => this.sql.exec(sql, ...params).toArray(), subjectId, at),
3824
+ jobRuns: redactSubjectJobRuns(sql, subjectId, at),
3825
+ // The free-text copies (#1632), and the tombstoned intents the coordinator hands to
3826
+ // the directory half. Last, so those ids include every intent tombstoned above.
3827
+ ...redactSubjectScopeText(sql, subjectId, at),
3756
3828
  };
3757
3829
  }
3758
3830
  /**