sporades 0.8.6 โ†’ 0.8.7

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/CHANGES.md CHANGED
@@ -1,103 +1,32 @@
1
1
  # Changes
2
2
 
3
- ## Unreleased - 2026-08-17
3
+ ## Unreleased - 2026-08-18
4
4
 
5
- Changes since v0.8.5.
5
+ Changes since v0.8.6.
6
6
 
7
7
  ### ๐Ÿš€ Features
8
8
 
9
- - Migrate additive unique constraints atomically (2cf8381).
10
- - Add conflict-targeted idempotent table inserts (bd2aeaa).
11
- - Expose privileged Team inspections (16d784a).
12
- - Declare unique Capsule table constraints (f638b36).
13
- - Expose exact Team membership counts (6893db7).
9
+ - Add transaction-bound trusted reads (9d23493).
14
10
 
15
11
  ### ๐Ÿ› Bug Fixes
16
12
 
17
- - Shut down rejected runtime candidates (5ec8198).
18
- - Preserve candidate on activation degradation (302268c).
19
- - Activate replacement jobs after teardown (ee4db9f).
20
- - Serialize job lease recovery (09dca55).
21
- - Refresh job leases after dev handoff (5961f77).
22
- - Recover jobs across runtime handoff (6d2b344).
23
- - Gate jobs on successful runtime initialization (3082aad).
24
- - Validate all schedule cursors (bf3dea0).
25
- - Arm schedule recovery after commit (da43e54).
26
- - Persist schedule upgrade lineage (54d664e).
27
- - Close schedule handoff and root reentry races (281029a).
28
- - Serialize legacy schedule upgrades (9e87fb1).
29
- - Publish schedule incarnations atomically (78da563).
30
- - Make schedule generations authoritative (9005b1e).
31
- - Bind schedules to declaration generations (b478ae7).
32
- - Make schedule claims transactional (960f2c6).
33
- - Seal retained job and libsql lifecycle (c5dc235).
34
- - Recover future leases and schedules safely (fa08688).
35
- - Bound job lifecycle time domains (89149c7).
36
- - Close pre-handler job shutdown races (ffdca81).
37
- - Make runtime shutdown failure atomic (1958f3b).
38
- - Harden transactional lifecycle cleanup (e408f2d).
39
- - Defer transactional job cancellation (2d3cf3e).
40
- - Linearize team reads and job claims (1aa5391).
41
- - Wake retained jobs on startup (0a66d61).
42
- - Settle transactional log writes (43e1b2b).
43
- - Revoke settled transaction adapters (bef9821).
44
- - Abort jobs through the root runtime (5a43040).
45
- - Settle jobs before lifecycle shutdown (51d1d9d).
46
- - Settle job shutdown safely (9d38300).
47
- - Await runtime closure during shutdown (ff688f1).
48
- - Settle scheduled Jobs before runtime shutdown (7e7204e).
49
- - Scope unique migration error translation (9af3b24).
50
- - Preserve non-unique migration errors (2cfb207).
51
- - Fix chained transaction release and async insert type (386ea84).
52
- - Fix async insertOrIgnore documentation (a41f2ac).
53
- - Guard privileged join-link reads (dea4ebc).
54
- - Close privileged Team inspection races (d53136c).
55
- - Redact privileged Team join metadata (ad77482).
56
- - Defer existing table unique changes (e49477d).
57
-
58
- ### ๐Ÿ”ง Improvements
59
-
60
- - Ignore Macos fluff files (d4ae934).
13
+ - Keep admission logs in transaction (5a9d012).
14
+ - Revoke trusted reads at settlement (dca0fea).
15
+ - Reject trusted reads from snapshots (9abe441).
16
+ - Bind Join cancellation to commit (e37058d).
17
+ - Close Team admission boundary races (b12dbde).
18
+ - Trust Team Join admission reads (32b65ee).
19
+ - Close trusted-read review gaps (da0c453).
61
20
 
62
21
  ### ๐Ÿ“ Documentation
63
22
 
64
- - Pin final platform contract evidence (65250af).
65
- - Align platform contracts after swarm amendments (8f64c4f).
66
- - Record swarm cleanup (7233572).
67
- - Close combined platform contract swarm (a1862c8).
68
- - Record final combined contract package proof (4c44bf8).
69
- - Record combined contract proof evidence (ac1c48c).
70
- - Record accepted unique migration contract (6c5e484).
71
- - Record accepted idempotent insert contract (19e7b63).
72
- - Align shipped Job lifecycle reference (9b7820f).
73
- - Document atomic Job enqueue lifecycle (9dbeb8a).
74
- - Reject nested database transaction scopes (3cc98b6).
75
- - Record accepted platform contract tickets (6ef6d1c).
76
- - Close unique constraint contract gaps (20baabb).
77
- - Prove unique constraint boundaries across adapters (f6d3517).
78
- - Cover immutable unique schema boundary (4fb81d9).
79
- - Initialize platform contracts swarm ledger (61816a1).
80
- - Add Team subscription platform tickets (494a086).
23
+ - Complete trusted Join admission ticket (0347c0e).
24
+ - Complete trusted-read foundation ticket (2a24b7b).
81
25
 
82
26
  ### ๐Ÿงช Tests
83
27
 
84
- - Reset native schedule clock before init (1153643).
85
- - Prove atomic Team Job enqueue failures (09265ab).
86
- - Preserve PostgreSQL quoting recorder transactions (85d2266).
87
- - Cover unrelated rebuild constraint failures (4605ec5).
88
- - Isolate Node 24 SQLite server tests (8542eb9).
89
- - Harden failed mutation cleanup (d43a5ab).
90
- - Bound transaction handler context lifetime (67dccb4).
91
- - Defer runtime Job dispatch until commit (0a04afb).
92
- - Persist Jobs inside handler transactions (b59004f).
93
- - Reject captured transaction reentry and drop rolled-back jobs (952309e).
94
- - Replace async-local transaction ownership (95cbb85).
95
- - Gate transaction connections and track mutation writes (20cda07).
96
- - Serialize shared database connection transactions (425e4f0).
97
- - Isolate composite unique conformance (75ace57).
98
- - Cover Team count membership lifecycle (80d6cd7).
99
- - Accept cross-realm query arrays (55b87a6).
100
- - Reject query argument array subclasses (2171b61).
28
+ - Restore PostgreSQL SQL quoting audit (c1fcf7c).
29
+ - Tidy up test (f643360).
101
30
 
102
31
  ## 0.8.5 - 2026-08-15
103
32
 
package/README.md CHANGED
@@ -54,6 +54,8 @@ sporades host push --restart
54
54
  - **Built-in collaboration, explicitly authorized**: runtime-owned Teams,
55
55
  email-bound Join links, membership application roles, and explicit Team ACL
56
56
  decisionsโ€”without a hidden current Team or automatic data partitioning.
57
+ Trusted `teams.admitJoin` policies may make atomic seat decisions from
58
+ read-only app state hidden by the joining user's ordinary row ACLs.
57
59
  - **Realtime everything**: configuration free - automatic notifications, even for Postgress and AWS-S3
58
60
 
59
61
  ## Documentation
@@ -253,6 +253,7 @@ var nodeCryptoModule2 = process.getBuiltinModule("node:crypto");
253
253
  var TEAM_JOIN_LINK_DEFAULT_TTL_SECONDS = 60 * 60 * 24;
254
254
  var TEAM_JOIN_LINK_MIN_TTL_SECONDS = 5 * 60;
255
255
  var TEAM_JOIN_LINK_MAX_TTL_SECONDS = 60 * 60 * 24 * 7;
256
+ var transactionBeforeCommitChecks = Symbol.for("sporades.database.transactionBeforeCommitChecks");
256
257
 
257
258
  // src/auth-runtime.ts
258
259
  var nodeCryptoModule3 = process.getBuiltinModule("node:crypto");
@@ -265,8 +266,10 @@ var PASSWORD_RESET_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
265
266
  var nodeCryptoModule4 = process.getBuiltinModule("node:crypto");
266
267
  var nodeFsModule = process.getBuiltinModule("node:fs");
267
268
  var transactionOperations = Symbol.for("sporades.database.transactionOperations");
269
+ var transactionBeforeCommitChecks2 = Symbol.for("sporades.database.transactionBeforeCommitChecks");
268
270
 
269
271
  // src/server-runtime-source.ts
272
+ var trustedReadTransactionAdapter = Symbol("sporades.trustedReadTransactionAdapter");
270
273
  function logPayloadMaxBytes(config = {}) {
271
274
  const configured = Number(config.logs?.payloadMaxBytes ?? config.logging?.payloadMaxBytes);
272
275
  return Number.isInteger(configured) && configured > 0 ? configured : 4096;
@@ -422,7 +425,7 @@ function writeEnvelope(result, failed = false) {
422
425
  }
423
426
 
424
427
  // src/cli/cli-version.ts
425
- var CLI_VERSION = "0.8.6";
428
+ var CLI_VERSION = "0.8.7";
426
429
 
427
430
  // src/cli/schedule-inspection-envelope.ts
428
431
  function sanitizeScheduleInspectionEnvelope(envelope, invalid) {
package/bin/sporades.js CHANGED
@@ -8842,6 +8842,7 @@ var TEAM_JOIN_LINK_MAX_OUTSTANDING = 20;
8842
8842
  var TEAM_JOIN_LINK_CREATION_MAX_PER_HOUR = 10;
8843
8843
  var TEAM_JOIN_LINK_PRUNE_LIMIT = 100;
8844
8844
  var TEAM_JOIN_LINK_SECRET_ID = "v1";
8845
+ var transactionBeforeCommitChecks = Symbol.for("sporades.database.transactionBeforeCommitChecks");
8845
8846
  var TEAM_APPLICATION_ROLE_MAX = 32;
8846
8847
  var TEAM_APPLICATION_ROLE_PATCH_MAX = 16;
8847
8848
  var TEAM_APPLICATION_ROLE_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
@@ -9195,8 +9196,11 @@ async function validateTeamJoinLink(database, auth, code) {
9195
9196
  async function joinCurrentUserTeam(database, auth, code, eventContext) {
9196
9197
  let joined;
9197
9198
  let deniedTeamId = null;
9199
+ let auditUserId = null;
9198
9200
  try {
9199
9201
  requireAuth({ auth }, { linked: true });
9202
+ const joiningUserId = String(auth.userId);
9203
+ auditUserId = joiningUserId;
9200
9204
  const parsed = parseTeamJoinCode(code);
9201
9205
  if (!parsed) throw invalidTeamJoinLink();
9202
9206
  joined = await withTeamTransaction(database, async (tx) => {
@@ -9226,7 +9230,7 @@ async function joinCurrentUserTeam(database, auth, code, eventContext) {
9226
9230
  if (!team) throw invalidTeamJoinLink();
9227
9231
  const attachedEmails = await tx.prepare(sql(
9228
9232
  "SELECT [email] FROM [sporades_auth_email_credentials] WHERE [userId] = ? UNION ALL SELECT [email] FROM [sporades_auth_identities] WHERE [userId] = ? AND [email] IS NOT NULL"
9229
- )).all(auth.userId, auth.userId);
9233
+ )).all(joiningUserId, joiningUserId);
9230
9234
  const targetEmail = normalizeTeamJoinIdentityEmail(row.email);
9231
9235
  if (!attachedEmails.some((identity) => normalizeTeamJoinIdentityEmail(identity.email) === targetEmail)) {
9232
9236
  deniedTeamId = String(team.id);
@@ -9236,12 +9240,12 @@ async function joinCurrentUserTeam(database, auth, code, eventContext) {
9236
9240
  "SELECT [userId] FROM [sporades_team_join_link_redemptions] WHERE [joinLinkId] = ?"
9237
9241
  )).get(row.id);
9238
9242
  if (row.consumedAt) {
9239
- if (redemption?.userId !== auth.userId) throw invalidTeamJoinLink();
9243
+ if (redemption?.userId !== joiningUserId) throw invalidTeamJoinLink();
9240
9244
  const membership2 = await tx.prepare(sql(
9241
9245
  "SELECT [role] FROM [sporades_team_memberships] WHERE [teamId] = ? AND [userId] = ?"
9242
- )).get(row.teamId, auth.userId);
9246
+ )).get(row.teamId, joiningUserId);
9243
9247
  if (!membership2) throw invalidTeamJoinLink();
9244
- await ensureInitialTeamOnAdapter(tx, auth.userId);
9248
+ await ensureInitialTeamOnAdapter(tx, joiningUserId);
9245
9249
  const count2 = await tx.prepare(sql("SELECT COUNT(*) AS [count] FROM [sporades_team_memberships] WHERE [teamId] = ?")).get(row.teamId);
9246
9250
  return teamSummary({ id: team.id, name: team.name, role: membership2.role, memberCount: Number(count2?.count ?? 0) });
9247
9251
  }
@@ -9252,23 +9256,24 @@ async function joinCurrentUserTeam(database, auth, code, eventContext) {
9252
9256
  if (Number(consumed?.changes ?? 0) !== 1) throw invalidTeamJoinLink();
9253
9257
  await tx.prepare(sql(
9254
9258
  "INSERT INTO [sporades_team_join_link_redemptions] ([joinLinkId], [teamId], [userId], [createdAt]) VALUES (?, ?, ?, ?)"
9255
- )).run(row.id, row.teamId, auth.userId, now);
9256
- await ensureInitialTeamOnAdapter(tx, auth.userId);
9259
+ )).run(row.id, row.teamId, joiningUserId, now);
9260
+ await ensureInitialTeamOnAdapter(tx, joiningUserId);
9257
9261
  let membership = await tx.prepare(sql(
9258
9262
  "SELECT [role] FROM [sporades_team_memberships] WHERE [teamId] = ? AND [userId] = ?"
9259
- )).get(row.teamId, auth.userId);
9263
+ )).get(row.teamId, joiningUserId);
9260
9264
  if (!membership) {
9261
- await enforceTeamJoinAdmission(database, tx, auth, String(row.teamId));
9262
- await ensureMembershipCounterOnAdapter(tx, auth.userId);
9265
+ await enforceTeamJoinAdmission(database, tx, auth, joiningUserId, String(row.teamId), eventContext?.signal);
9266
+ registerTeamJoinCancellationBeforeCommit(tx, eventContext?.signal);
9267
+ await ensureMembershipCounterOnAdapter(tx, joiningUserId);
9263
9268
  const claim = await tx.prepare(sql(
9264
9269
  "UPDATE [sporades_team_membership_counters] SET [membershipCount] = [membershipCount] + 1 WHERE [userId] = ? AND [membershipCount] < ?"
9265
- )).run(auth.userId, TEAM_MEMBERSHIP_MAX);
9270
+ )).run(joiningUserId, TEAM_MEMBERSHIP_MAX);
9266
9271
  if (Number(claim?.changes ?? 0) !== 1) {
9267
9272
  throw commandError2("Team limit reached.", `A user can belong to at most ${TEAM_MEMBERSHIP_MAX} Teams.`, "TEAM_LIMIT_REACHED");
9268
9273
  }
9269
9274
  await tx.prepare(sql(
9270
9275
  "INSERT INTO [sporades_team_memberships] ([teamId], [userId], [role], [createdAt]) VALUES (?, ?, 'member', ?)"
9271
- )).run(row.teamId, auth.userId, now);
9276
+ )).run(row.teamId, joiningUserId, now);
9272
9277
  membership = { role: "member" };
9273
9278
  }
9274
9279
  await releaseTeamJoinLinkCapacity(tx, String(row.teamId));
@@ -9276,29 +9281,40 @@ async function joinCurrentUserTeam(database, auth, code, eventContext) {
9276
9281
  return teamSummary({ id: team.id, name: team.name, role: membership.role, memberCount: Number(count?.count ?? 0) });
9277
9282
  });
9278
9283
  } catch (error) {
9279
- emitTeamSecurityEvent(database, eventContext, "teams.joinLink.join", auth?.userId, deniedTeamId, "denied", String(error?.code ?? "INVALID_JOIN_LINK"));
9284
+ emitTeamSecurityEvent(database, eventContext, "teams.joinLink.join", auditUserId ?? auth?.userId, deniedTeamId, "denied", String(error?.code ?? "INVALID_JOIN_LINK"));
9280
9285
  throw error;
9281
9286
  }
9282
- emitTeamSecurityEvent(database, eventContext, "teams.joined", auth.userId, joined.id, "succeeded", "TEAM_JOINED");
9287
+ emitTeamSecurityEvent(database, eventContext, "teams.joined", auditUserId, joined.id, "succeeded", "TEAM_JOINED");
9283
9288
  return { team: joined };
9284
9289
  }
9285
- async function enforceTeamJoinAdmission(database, tx, auth, teamId) {
9286
- if (typeof database.teamJoinAdmission !== "function") return;
9290
+ async function enforceTeamJoinAdmission(database, tx, auth, joiningUserId, teamId, signal) {
9291
+ if (typeof database.runTeamJoinAdmission !== "function") return;
9287
9292
  const count = await tx.prepare(tx.dialect.sql(
9288
9293
  "SELECT COUNT(*) AS [count] FROM [sporades_team_memberships] WHERE [teamId] = ?"
9289
9294
  )).get(teamId);
9290
9295
  try {
9291
- const context = database.createTeamJoinAdmissionContext(tx, auth);
9292
- const decision = await database.teamJoinAdmission(context, {
9296
+ const decision = await database.runTeamJoinAdmission(tx, auth, {
9293
9297
  teamId,
9294
- userId: auth.userId,
9298
+ userId: joiningUserId,
9295
9299
  currentMemberCount: Number(count?.count ?? 0)
9296
- });
9300
+ }, signal);
9297
9301
  if (decision?.allow !== true) throw teamJoinDenied();
9298
9302
  } catch {
9299
9303
  throw teamJoinDenied();
9300
9304
  }
9301
9305
  }
9306
+ function throwIfTeamJoinCancelled(signal) {
9307
+ if (signal?.aborted) throw teamJoinDenied();
9308
+ }
9309
+ function registerTeamJoinCancellationBeforeCommit(transactionAdapter, signal) {
9310
+ if (!signal) return;
9311
+ let checks = transactionAdapter[transactionBeforeCommitChecks];
9312
+ if (!Object.prototype.hasOwnProperty.call(transactionAdapter, transactionBeforeCommitChecks)) {
9313
+ checks = [];
9314
+ Object.defineProperty(transactionAdapter, transactionBeforeCommitChecks, { value: checks });
9315
+ }
9316
+ checks.push(() => throwIfTeamJoinCancelled(signal));
9317
+ }
9302
9318
  function normalizeTeamJoinEmail(email) {
9303
9319
  const normalized = String(email ?? "").trim().toLowerCase();
9304
9320
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
@@ -13282,8 +13298,14 @@ async function rejectNestedTransactionScope() {
13282
13298
  "Keep mutation work inside a single Sporades mutation transaction."
13283
13299
  );
13284
13300
  }
13285
- var transactionScopeRevokers = /* @__PURE__ */ new WeakMap();
13286
- function createTransactionScopedAdapter(adapter, operations = {}) {
13301
+ var transactionScopes = /* @__PURE__ */ new WeakMap();
13302
+ function isActiveTransactionScopedAdapter(value, owner) {
13303
+ const scope = value && typeof value === "object" ? transactionScopes.get(value) : void 0;
13304
+ return Boolean(
13305
+ scope && scope.kind === "transaction" && (owner === void 0 || scope.owner === owner)
13306
+ );
13307
+ }
13308
+ function createTransactionScopedAdapter(adapter, operations, owner, kind) {
13287
13309
  let active = true;
13288
13310
  const assertActive = () => {
13289
13311
  if (!active) throw commandError2(
@@ -13317,16 +13339,24 @@ function createTransactionScopedAdapter(adapter, operations = {}) {
13317
13339
  withTransaction: rejectNestedTransactionScope,
13318
13340
  withReadOnlySnapshot: rejectNestedTransactionScope
13319
13341
  });
13320
- transactionScopeRevokers.set(scopedAdapter, () => {
13321
- active = false;
13342
+ transactionScopes.set(scopedAdapter, {
13343
+ revoke: () => {
13344
+ active = false;
13345
+ },
13346
+ owner,
13347
+ kind
13322
13348
  });
13323
13349
  return scopedAdapter;
13324
13350
  }
13325
13351
  function revokeTransactionScopedAdapter(adapter) {
13326
- transactionScopeRevokers.get(adapter)?.();
13327
- transactionScopeRevokers.delete(adapter);
13352
+ transactionScopes.get(adapter)?.revoke();
13353
+ transactionScopes.delete(adapter);
13328
13354
  }
13329
13355
  var transactionOperations = Symbol.for("sporades.database.transactionOperations");
13356
+ var transactionBeforeCommitChecks2 = Symbol.for("sporades.database.transactionBeforeCommitChecks");
13357
+ async function runTransactionBeforeCommitChecks(transactionAdapter) {
13358
+ for (const check of transactionAdapter[transactionBeforeCommitChecks2] ?? []) await check();
13359
+ }
13330
13360
  async function createRuntimeDatabaseAdapter(databasePath, serverEnv = {}, config = {}) {
13331
13361
  if (config.services?.database?.engine === "libsql" && serverEnv.SPORADES_SERVICE_DATABASE_ENGINE === "libsql" && serverEnv.SPORADES_SERVICE_DATABASE_URL) {
13332
13362
  return await createLibsqlDatabaseAdapter({
@@ -14106,13 +14136,14 @@ async function createSqliteDatabaseAdapter(databasePath, options = {}) {
14106
14136
  async withTransaction(fn) {
14107
14137
  return await connectionGate.runTransaction(async () => {
14108
14138
  const ownerOperations = typeof this[transactionOperations] === "function" ? this[transactionOperations]() : { exec: this.exec.bind(this), prepare: this.prepare.bind(this) };
14109
- const transactionAdapter = createTransactionScopedAdapter(this, ownerOperations);
14139
+ const transactionAdapter = createTransactionScopedAdapter(this, ownerOperations, this, "transaction");
14110
14140
  const transactionExec = ownerOperations.exec;
14111
14141
  await transactionExec("BEGIN");
14112
14142
  try {
14113
14143
  let result;
14114
14144
  try {
14115
14145
  result = await fn(transactionAdapter);
14146
+ await runTransactionBeforeCommitChecks(transactionAdapter);
14116
14147
  } finally {
14117
14148
  revokeTransactionScopedAdapter(transactionAdapter);
14118
14149
  }
@@ -14127,7 +14158,7 @@ async function createSqliteDatabaseAdapter(databasePath, options = {}) {
14127
14158
  async withReadOnlySnapshot(fn) {
14128
14159
  return await connectionGate.runTransaction(async () => {
14129
14160
  const ownerOperations = typeof this[transactionOperations] === "function" ? this[transactionOperations]() : { exec: this.exec.bind(this), prepare: this.prepare.bind(this) };
14130
- const ownerAdapter = createTransactionScopedAdapter(this, ownerOperations);
14161
+ const ownerAdapter = createTransactionScopedAdapter(this, ownerOperations, this, "snapshot");
14131
14162
  const transactionExec = ownerOperations.exec;
14132
14163
  await transactionExec("BEGIN");
14133
14164
  await transactionExec("PRAGMA query_only = ON");
@@ -14250,10 +14281,11 @@ async function createPostgresDatabaseAdapter(options) {
14250
14281
  return await connectionGate.runTransaction(async () => {
14251
14282
  await rawQuery("BEGIN");
14252
14283
  try {
14253
- const transactionAdapter = createTransactionScopedAdapter(adapter, createOperations(runDirectly));
14284
+ const transactionAdapter = createTransactionScopedAdapter(adapter, createOperations(runDirectly), adapter, "transaction");
14254
14285
  let result;
14255
14286
  try {
14256
14287
  result = await fn(transactionAdapter);
14288
+ await runTransactionBeforeCommitChecks(transactionAdapter);
14257
14289
  } finally {
14258
14290
  revokeTransactionScopedAdapter(transactionAdapter);
14259
14291
  }
@@ -14270,7 +14302,7 @@ async function createPostgresDatabaseAdapter(options) {
14270
14302
  },
14271
14303
  async withReadOnlySnapshot(fn) {
14272
14304
  return await connectionGate.runTransaction(async () => {
14273
- const transactionAdapter = createTransactionScopedAdapter(adapter, createOperations(runDirectly));
14305
+ const transactionAdapter = createTransactionScopedAdapter(adapter, createOperations(runDirectly), adapter, "snapshot");
14274
14306
  await rawQuery("BEGIN TRANSACTION READ ONLY");
14275
14307
  try {
14276
14308
  let result;
@@ -14758,13 +14790,14 @@ async function createLibsqlDatabaseAdapter(options) {
14758
14790
  const transactionAdapter = createTransactionScopedAdapter({
14759
14791
  ...adapter,
14760
14792
  ...createOperations(transaction, runDirectly)
14761
- });
14793
+ }, {}, adapter, "transaction");
14762
14794
  activeTransactions.add(transaction);
14763
14795
  try {
14764
14796
  await libsqlExecute({ endpoint, authToken, transaction, sql: "BEGIN", params: [], close: false });
14765
14797
  let result;
14766
14798
  try {
14767
14799
  result = await fn(transactionAdapter);
14800
+ await runTransactionBeforeCommitChecks(transactionAdapter);
14768
14801
  } finally {
14769
14802
  revokeTransactionScopedAdapter(transactionAdapter);
14770
14803
  }
@@ -14786,7 +14819,7 @@ async function createLibsqlDatabaseAdapter(options) {
14786
14819
  return await connectionGate.runTransaction(async () => {
14787
14820
  assertLibsqlOpen(closed);
14788
14821
  const transaction = { baton: null, baseUrl: endpoint };
14789
- const snapshotAdapter = createTransactionScopedAdapter({ ...adapter, ...createOperations(transaction, runDirectly) });
14822
+ const snapshotAdapter = createTransactionScopedAdapter({ ...adapter, ...createOperations(transaction, runDirectly) }, {}, adapter, "snapshot");
14790
14823
  activeTransactions.add(transaction);
14791
14824
  try {
14792
14825
  await libsqlExecute({ endpoint, authToken, transaction, sql: "BEGIN", params: [], close: false });
@@ -15257,6 +15290,8 @@ function quoteIdentifier(identifier) {
15257
15290
 
15258
15291
  // src/server-runtime-source.ts
15259
15292
  var mutationResultsWithWrites = /* @__PURE__ */ new WeakSet();
15293
+ var trustedReadPurposes = /* @__PURE__ */ new Set(["teams.join-admission"]);
15294
+ var trustedReadTransactionAdapter = Symbol("sporades.trustedReadTransactionAdapter");
15260
15295
  async function shutdownAndCloseDatabase(database) {
15261
15296
  let shutdownError;
15262
15297
  let closeError;
@@ -15487,10 +15522,24 @@ async function openDevDatabase(databasePath, serverSource, serverEnv = {}, confi
15487
15522
  passwordResetConfig: resolvePasswordResetConfig(config),
15488
15523
  teamJoinLinkConfig: resolveTeamJoinLinkConfig(config),
15489
15524
  teamApplicationRoles,
15490
- teamJoinAdmission: capsuleDefinition?.teams?.admitJoin,
15491
- createTeamJoinAdmissionContext(transactionAdapter, auth) {
15492
- return createTeamJoinAdmissionContext(createTransactionDatabase(database, transactionAdapter), auth);
15493
- },
15525
+ runTeamJoinAdmission: typeof capsuleDefinition?.teams?.admitJoin === "function" ? async function(transactionAdapter, auth, input, signal) {
15526
+ const rootDatabase = this.__rootDatabase ?? this;
15527
+ const trustedTransaction = this[trustedReadTransactionAdapter] ?? transactionAdapter;
15528
+ const admissionDatabase = createTransactionDatabase(rootDatabase, trustedTransaction);
15529
+ try {
15530
+ return await withTrustedRead(rootDatabase, {
15531
+ transaction: trustedTransaction,
15532
+ purpose: "teams.join-admission",
15533
+ subject: { teamId: input.teamId, userId: input.userId },
15534
+ signal
15535
+ }, (trustedDb) => capsuleDefinition.teams.admitJoin(
15536
+ createTeamJoinAdmissionContext(admissionDatabase, auth, trustedDb),
15537
+ input
15538
+ ));
15539
+ } finally {
15540
+ await drainPendingLogWrites(admissionDatabase);
15541
+ }
15542
+ } : void 0,
15494
15543
  fileAcl,
15495
15544
  securityPolicy: resolveRuntimeSecurityPolicy(config),
15496
15545
  fileStorage,
@@ -17381,6 +17430,7 @@ function createTransactionDatabase(database, transactionAdapter, writeState) {
17381
17430
  adapter,
17382
17431
  sqlite: adapter,
17383
17432
  __transactionActive: true,
17433
+ [trustedReadTransactionAdapter]: transactionAdapter,
17384
17434
  __rootDatabase: database.__rootDatabase ?? database,
17385
17435
  __pendingLogWrites: pendingLogWrites
17386
17436
  };
@@ -17569,32 +17619,104 @@ function createEndpointDatabaseApi(database, contextGetter = null) {
17569
17619
  database.schema.tables.map((table) => [table.name, createEndpointTableApi(database, table, {}, contextGetter)])
17570
17620
  );
17571
17621
  }
17572
- function createEndpointReadOnlyDatabaseApi(database, contextGetter = null) {
17622
+ function createEndpointReadOnlyDatabaseApi(database, contextGetter = null, assertActive = () => {
17623
+ }) {
17573
17624
  return Object.fromEntries(
17574
17625
  database.schema.tables.map((table) => [
17575
17626
  table.name,
17576
- readOnlyEndpointTableApi(createEndpointTableApi(database, table, {}, contextGetter))
17627
+ readOnlyEndpointTableApi(createEndpointTableApi(database, table, {}, contextGetter), assertActive)
17577
17628
  ])
17578
17629
  );
17579
17630
  }
17580
- function readOnlyEndpointTableApi(tableApi) {
17631
+ function readOnlyEndpointTableApi(tableApi, assertActive) {
17581
17632
  return {
17582
17633
  where(fieldName, value) {
17583
- return readOnlyEndpointTableApi(tableApi.where(fieldName, value));
17634
+ assertActive();
17635
+ return readOnlyEndpointTableApi(tableApi.where(fieldName, value), assertActive);
17584
17636
  },
17585
17637
  orderBy(fieldName, direction = "asc") {
17586
- return readOnlyEndpointTableApi(tableApi.orderBy(fieldName, direction));
17638
+ assertActive();
17639
+ return readOnlyEndpointTableApi(tableApi.orderBy(fieldName, direction), assertActive);
17587
17640
  },
17588
17641
  limit(count) {
17589
- return readOnlyEndpointTableApi(tableApi.limit(count));
17642
+ assertActive();
17643
+ return readOnlyEndpointTableApi(tableApi.limit(count), assertActive);
17590
17644
  },
17591
17645
  get() {
17592
- return tableApi.get();
17646
+ assertActive();
17647
+ return trustedReadResult(tableApi.get(), assertActive);
17593
17648
  },
17594
17649
  all() {
17595
- return tableApi.all();
17650
+ assertActive();
17651
+ return trustedReadResult(tableApi.all(), assertActive);
17652
+ }
17653
+ };
17654
+ }
17655
+ function trustedReadResult(value, assertActive) {
17656
+ return Promise.resolve(value).then((result) => {
17657
+ assertActive();
17658
+ return result;
17659
+ });
17660
+ }
17661
+ async function withTrustedRead(database, options, callback) {
17662
+ if (!isActiveTransactionScopedAdapter(options?.transaction, database?.adapter)) {
17663
+ throw commandError2(
17664
+ "Trusted app-database reads require an active transaction.",
17665
+ "Start the trusted policy from the runtime-owned transition transaction.",
17666
+ "TRUSTED_READ_TRANSACTION_REQUIRED"
17667
+ );
17668
+ }
17669
+ if (!trustedReadPurposes.has(options?.purpose)) {
17670
+ throw commandError2(
17671
+ "Trusted app-database read purpose is invalid.",
17672
+ "Use a runtime-owned trusted policy purpose.",
17673
+ "INVALID_TRUSTED_READ_PURPOSE"
17674
+ );
17675
+ }
17676
+ const signal = options?.signal;
17677
+ const abortError = () => commandError2(
17678
+ "Trusted app-database read was aborted.",
17679
+ "Retry the runtime-owned trusted policy if cancellation was not intended.",
17680
+ "TRUSTED_READ_ABORTED"
17681
+ );
17682
+ if (signal?.aborted) throw abortError();
17683
+ const transactionDatabase = createTransactionDatabase(database, options.transaction);
17684
+ let active = true;
17685
+ const assertActive = () => {
17686
+ if (!active) {
17687
+ throw commandError2(
17688
+ "Trusted app-database read access is no longer active.",
17689
+ "Start a new runtime-owned trusted policy callback before reading app data.",
17690
+ "TRUSTED_READ_ACCESS_INACTIVE"
17691
+ );
17596
17692
  }
17693
+ if (signal?.aborted) throw abortError();
17597
17694
  };
17695
+ const context = {
17696
+ subject: options.subject,
17697
+ purpose: options.purpose,
17698
+ signal
17699
+ };
17700
+ grantPrivilegedDbAccess(context);
17701
+ const holder = createContextHolder(context);
17702
+ const db = createEndpointReadOnlyDatabaseApi(transactionDatabase, () => holder.current, assertActive);
17703
+ try {
17704
+ try {
17705
+ const result = await callback(db);
17706
+ assertActive();
17707
+ return result;
17708
+ } catch {
17709
+ if (signal?.aborted) throw abortError();
17710
+ throw commandError2(
17711
+ "Trusted app-database read failed.",
17712
+ "The runtime-owned trusted policy could not be evaluated.",
17713
+ "TRUSTED_READ_FAILED"
17714
+ );
17715
+ }
17716
+ } finally {
17717
+ active = false;
17718
+ revokePrivilegedDbAccess(context);
17719
+ }
17598
17720
  }
17599
17721
  function createEndpointTableApi(database, table, query = {}, contextGetter = null) {
17600
17722
  return {
@@ -19376,14 +19498,13 @@ function createMutationContext(database, auth) {
19376
19498
  };
19377
19499
  return context;
19378
19500
  }
19379
- function createTeamJoinAdmissionContext(database, auth) {
19501
+ function createTeamJoinAdmissionContext(database, auth, trustedDb) {
19380
19502
  const context = {
19381
- auth,
19503
+ auth: Object.freeze({ ...auth }),
19382
19504
  env: database.serverEnv,
19383
- log: createEndpointLogger(database)
19505
+ log: createEndpointLogger(database),
19506
+ db: trustedDb
19384
19507
  };
19385
- const holder = createContextHolder(context);
19386
- context.db = createEndpointReadOnlyDatabaseApi(database, () => holder.current);
19387
19508
  return context;
19388
19509
  }
19389
19510
  function deferOrScheduleJobDispatch(database, queueDatabase, context = void 0) {
@@ -25655,7 +25776,7 @@ jobs:
25655
25776
  }
25656
25777
 
25657
25778
  // src/cli/cli-version.ts
25658
- var CLI_VERSION = "0.8.6";
25779
+ var CLI_VERSION = "0.8.7";
25659
25780
 
25660
25781
  // src/cli/sporades.ts
25661
25782
  var SUPPORTED_TEMPLATES = new Set(CLIENT_TEMPLATES);
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.8.6";
1
+ export declare const CLI_VERSION = "0.8.7";
2
2
  //# sourceMappingURL=cli-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Generated by `npm run build` from package.json. Do not edit by hand.
2
- export const CLI_VERSION = "0.8.6";
2
+ export const CLI_VERSION = "0.8.7";
3
3
  //# sourceMappingURL=cli-version.js.map
@@ -2,6 +2,7 @@ import type { PathLike } from "node:fs";
2
2
  type LooseRecord = Record<string, any>;
3
3
  type RuntimeConfig = LooseRecord;
4
4
  type RuntimeEnv = Record<string, string | undefined>;
5
+ export declare function isActiveTransactionScopedAdapter(value: any, owner?: any): boolean;
5
6
  export declare function createRuntimeDatabaseAdapter(databasePath: any, serverEnv?: RuntimeEnv, config?: RuntimeConfig): Promise<LooseRecord>;
6
7
  export declare function createRuntimeInspectionAdapter(databasePath: any, serverEnv?: RuntimeEnv, config?: RuntimeConfig): Promise<LooseRecord | null>;
7
8
  export declare function createDatabaseDialect(spec: LooseRecord): LooseRecord;
@@ -1 +1 @@
1
- {"version":3,"file":"database-runtime.d.ts","sourceRoot":"","sources":["../src/database-runtime.ts"],"names":[],"mappings":"AAiKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAuBxC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACvC,KAAK,aAAa,GAAG,WAAW,CAAC;AACjC,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AA0GrD,wBAAsB,4BAA4B,CAAC,YAAY,EAAE,GAAG,EAAE,SAAS,GAAE,UAAe,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAqBlJ;AAED,wBAAsB,8BAA8B,CAAC,YAAY,EAAE,GAAG,EAAE,SAAS,GAAE,UAAe,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAS3J;AAkBD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAyBpE;AA2BD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAa1E;AAMD,wBAAgB,sBAAsB,gBAMrC;AAED,wBAAgB,wBAAwB,gBAgBvC;AAED,wBAAgB,sBAAsB,gBASrC;AAGD,wBAAgB,qBAAqB,gBAoCpC;AAED,wBAAgB,uBAAuB,gBAmDtC;AAKD,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,CA0qBpF;AAED,wBAAsB,2BAA2B,CAAC,YAAY,EAAE,QAAQ,EAAE,OAAO,GAAE,WAAgB,wBAqFlG;AAED,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAC;CAAE,wBA4HzE;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,GAAG;;eA+FxC,MAAM;;;;;;GAyEpB;AAyMD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,GAAE,GAAG,EAAO,UA2E/D;AAMD,wBAAsB,2BAA2B,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,GAAG,CAAC;CAAE;;;;wBAiF1D,CAAC,kBAAkB,EAAE,WAAW,KAAK,GAAG;6BA2BnC,CAAC,OAAO,EAAE,WAAW,KAAK,GAAG;;cAnFlD,MAAM;iBAUH,MAAM;uBAGA,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE;uBAQtB,SAAS,EAAE;uBAGX,MAAM,EAAE;;;GAoG9B;AA4VD,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,MAAa,OAM7F;AAyED,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;CAAE,gBAEhF;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;CAAE,gBAE1E;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;CAAE,EAAE,GAAG,EAAE,GAAG,gBAExF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,YAuE1C;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,GAAG,UAE9C"}
1
+ {"version":3,"file":"database-runtime.d.ts","sourceRoot":"","sources":["../src/database-runtime.ts"],"names":[],"mappings":"AAiKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAuBxC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACvC,KAAK,aAAa,GAAG,WAAW,CAAC;AACjC,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAkErD,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,WAOvE;AAqDD,wBAAsB,4BAA4B,CAAC,YAAY,EAAE,GAAG,EAAE,SAAS,GAAE,UAAe,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAqBlJ;AAED,wBAAsB,8BAA8B,CAAC,YAAY,EAAE,GAAG,EAAE,SAAS,GAAE,UAAe,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAS3J;AAkBD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAyBpE;AA2BD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAa1E;AAMD,wBAAgB,sBAAsB,gBAMrC;AAED,wBAAgB,wBAAwB,gBAgBvC;AAED,wBAAgB,sBAAsB,gBASrC;AAGD,wBAAgB,qBAAqB,gBAoCpC;AAED,wBAAgB,uBAAuB,gBAmDtC;AAKD,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,CA0qBpF;AAED,wBAAsB,2BAA2B,CAAC,YAAY,EAAE,QAAQ,EAAE,OAAO,GAAE,WAAgB,wBAwFlG;AAED,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAC;CAAE,wBA+HzE;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,GAAG;;eA+FxC,MAAM;;;;;;GAyEpB;AAyMD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,GAAE,GAAG,EAAO,UA2E/D;AAMD,wBAAsB,2BAA2B,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,GAAG,CAAC;CAAE;;;;wBAiF1D,CAAC,kBAAkB,EAAE,WAAW,KAAK,GAAG;6BA8BnC,CAAC,OAAO,EAAE,WAAW,KAAK,GAAG;;cAtFlD,MAAM;iBAUH,MAAM;uBAGA,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE;uBAQtB,SAAS,EAAE;uBAGX,MAAM,EAAE;;;GAuG9B;AA4VD,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,MAAa,OAM7F;AAyED,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;CAAE,gBAEhF;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;CAAE,gBAE1E;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;CAAE,EAAE,GAAG,EAAE,GAAG,gBAExF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,YAuE1C;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,GAAG,UAE9C"}