@substrat-run/adapter-cloudflare 0.130.1 → 0.132.1

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,EAqCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAmCL,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;AAwbD;;;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,CAi/JjE"}
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,CAklKjE"}
package/dist/scope-do.js CHANGED
@@ -1,7 +1,7 @@
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, 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, 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';
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
7
  import { doScopedSql, doBuiltColumnsOf, doSpineSql } from './sql.js';
@@ -160,6 +160,8 @@ const KERNEL_DDL = `
160
160
  module_id TEXT NOT NULL,
161
161
  version TEXT NOT NULL,
162
162
  applied_at TEXT NOT NULL,
163
+ duration_ms INTEGER,
164
+ rows_changed INTEGER,
163
165
  PRIMARY KEY (module_id, version)
164
166
  );
165
167
  -- #286: the PITR bookmark taken immediately BEFORE a migration pass runs on a
@@ -833,13 +835,15 @@ export function defineScopeDO(modules, bareOps) {
833
835
  */
834
836
  appliedMigrations(limit = 100) {
835
837
  return this.sql
836
- .exec(`SELECT module_id, version, applied_at FROM _substrat_migrations
838
+ .exec(`SELECT module_id, version, applied_at, duration_ms, rows_changed FROM _substrat_migrations
837
839
  ORDER BY applied_at DESC, module_id, version LIMIT ?`, limit)
838
840
  .toArray()
839
841
  .map((r) => ({
840
842
  moduleId: r.module_id,
841
843
  version: r.version,
842
844
  appliedAt: r.applied_at ?? null,
845
+ durationMs: r.duration_ms ?? null,
846
+ rowsChanged: r.rows_changed ?? null,
843
847
  }));
844
848
  }
845
849
  /**
@@ -2201,6 +2205,28 @@ export function defineScopeDO(modules, bareOps) {
2201
2205
  }
2202
2206
  return record;
2203
2207
  }
2208
+ /** Authorize a job's read as its registered module, never as a person. */
2209
+ async systemAttachmentAuthorize(attachmentId, moduleId, tenantId, scopeId) {
2210
+ await this.ensureMigrations();
2211
+ if (!this.modules.has(moduleId)) {
2212
+ throw toRpcError(substratError('not_found', `module not registered in this scope: ${moduleId}`));
2213
+ }
2214
+ const record = this.attachmentRow(attachmentId);
2215
+ if (!record)
2216
+ return null;
2217
+ const gate = this.attachmentGate(record.entity.entityType);
2218
+ try {
2219
+ const ctx = this.operationContext(this.systemPrincipal, tenantId, scopeId, undefined, undefined, moduleId, undefined, undefined, 'attachments.open');
2220
+ assertAllowed(await ctx.check(gate.read, record.entity));
2221
+ }
2222
+ catch (err) {
2223
+ if (err instanceof PermissionDenied) {
2224
+ this.recordDenial({ kind: 'system', id: moduleId }, tenantId, 'attachments.open', err, null);
2225
+ }
2226
+ throw toRpcError(err);
2227
+ }
2228
+ return record;
2229
+ }
2204
2230
  /** Remove an attachment's metadata fact: write gate + delete + `attachment.removed`. */
2205
2231
  async attachmentRemove(attachmentId, principal, tenantId, scopeId, connectionId) {
2206
2232
  await this.ensureMigrations();
@@ -2402,6 +2428,40 @@ export function defineScopeDO(modules, bareOps) {
2402
2428
  return permissions.map((permission) => ({ permission, held: !missing.has(permission) }));
2403
2429
  });
2404
2430
  }
2431
+ /**
2432
+ * `ctx.canAssign`'s bound for a principal the host names (#1931), queued like `peerCovers`
2433
+ * so no invoke's open transaction is read half-done. `null` when the tenant defines no such
2434
+ * role: a typed error thrown here would reach the host flattened, so the host types it.
2435
+ */
2436
+ async canAssignFor(tenantId, scopeId, principal, roleKey) {
2437
+ await this.ensureMigrations();
2438
+ return this.queue.enqueue(() => this.assignmentBound({ kind: 'principal', id: principal }, tenantId, scopeId, roleKey));
2439
+ }
2440
+ /** Check and grant in one serialized scope task; a refusal never writes a tuple. */
2441
+ async assignScopeRoleBoundedFor(tenantId, scopeId, caller, assignee, roleKey) {
2442
+ await this.ensureMigrations();
2443
+ return this.queue.enqueue(async () => {
2444
+ const bound = await this.assignmentBound({ kind: 'principal', id: caller }, tenantId, scopeId, roleKey);
2445
+ if (bound?.covered) {
2446
+ this.sql.exec(`INSERT OR REPLACE INTO _substrat_tuples (subject, relation, object, expires_at)
2447
+ VALUES (?, ?, ?, NULL)`, `principal:${assignee}`, `role:${roleKey}`, `scope:${scopeId}`);
2448
+ }
2449
+ return bound;
2450
+ });
2451
+ }
2452
+ /**
2453
+ * §5.1's assignment bound, resolved against the SAME role table the checker
2454
+ * expands — the tenant's projected role, not a vertical's compile-time `ROLES`
2455
+ * array, because the projected role is what assignment would actually confer.
2456
+ * The one resolution both `ctx.canAssign` and `canAssignFor` answer from; `null`
2457
+ * when the tenant defines no such role.
2458
+ */
2459
+ async assignmentBound(subject, tenantId, scopeId, roleKey) {
2460
+ const role = await this.controlPlaneReader().getRole(tenantId, roleKey);
2461
+ if (!role)
2462
+ return null;
2463
+ return this.checker.covers(subject, role.permissions, { tenantId, scopeId });
2464
+ }
2405
2465
  /**
2406
2466
  * Every module this scope holds or has held system authority for, and where each
2407
2467
  * stands (#1674) — the kernel's `systemGrantsStatus`, over this DO's own storage. A
@@ -2933,13 +2993,16 @@ export function defineScopeDO(modules, bareOps) {
2933
2993
  .exec('SELECT 1 FROM _substrat_migrations WHERE module_id = ? AND version = ?', moduleId, migration.version)
2934
2994
  .toArray()[0];
2935
2995
  if (!already) {
2996
+ const started = performance.now();
2997
+ const before = this.sql.exec('SELECT total_changes() AS n').toArray()[0].n;
2936
2998
  // #1898: a migration runs on this DO's own handle, not `ctx.sql`, so the
2937
2999
  // spine guard's REFERENCES rule is applied here.
2938
3000
  assertNoSpineReference(migration.sql, `migration ${key}`);
2939
3001
  for (const stmt of splitSqlStatements(migration.sql)) {
2940
3002
  this.sql.exec(stmt);
2941
3003
  }
2942
- this.sql.exec('INSERT INTO _substrat_migrations (module_id, version, applied_at) VALUES (?, ?, ?)', moduleId, migration.version, new Date().toISOString());
3004
+ const after = this.sql.exec('SELECT total_changes() AS n').toArray()[0].n;
3005
+ this.sql.exec('INSERT INTO _substrat_migrations (module_id, version, applied_at, duration_ms, rows_changed) VALUES (?, ?, ?, ?, ?)', moduleId, migration.version, new Date().toISOString(), Math.max(0, Math.round(performance.now() - started)), after - before);
2943
3006
  }
2944
3007
  });
2945
3008
  }
@@ -3349,6 +3412,9 @@ export function defineScopeDO(modules, bareOps) {
3349
3412
  // have added it bare already, and then this ALTER is skipped as a duplicate (#1883).
3350
3413
  // `lint:spine-ddl` refuses one that is not.
3351
3414
  for (const alter of [
3415
+ // #1763: rows written before these fields keep NULL, meaning unrecorded.
3416
+ 'ALTER TABLE _substrat_migrations ADD COLUMN duration_ms INTEGER',
3417
+ 'ALTER TABLE _substrat_migrations ADD COLUMN rows_changed INTEGER',
3352
3418
  'ALTER TABLE _substrat_tuples ADD COLUMN revoked_at TEXT',
3353
3419
  // Executor retry state (#100). The defaults read as "terminal", which is
3354
3420
  // right for every row already there: each is a completed delivery or a
@@ -3991,17 +4057,12 @@ export function defineScopeDO(modules, bareOps) {
3991
4057
  }
3992
4058
  return decision;
3993
4059
  };
3994
- /**
3995
- * §5.1's assignment bound, resolved against the SAME role table the checker
3996
- * expands — the tenant's projected role, not a vertical's compile-time `ROLES`
3997
- * array, because the projected role is what assignment would actually confer.
3998
- */
3999
4060
  const runCanAssign = async (roleKey) => {
4000
- const role = await this.controlPlaneReader().getRole(tenantId, roleKey);
4001
- if (!role) {
4002
- throw substratError('not_found', `no such role in this tenant: ${roleKey}`);
4061
+ const bound = await this.assignmentBound(subject, tenantId, scopeId, roleKey);
4062
+ if (!bound) {
4063
+ throw unknownRoleError(roleKey);
4003
4064
  }
4004
- return checker.covers(subject, role.permissions, { tenantId, scopeId });
4065
+ return bound;
4005
4066
  };
4006
4067
  // ctx.emit's writer, and the kernel's own events' (#1864): one path to the outbox, with the
4007
4068
  // reserved-type refusal applied to module code only. Never handed to module code as-is.