@k2b/cloud 0.48.0 → 0.50.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.
Files changed (206) hide show
  1. package/package.json +3 -3
  2. package/src/_internal/app-access-resources.ts +2 -1
  3. package/src/_internal/app-capabilities.ts +73 -0
  4. package/src/_internal/app-event-catalog.ts +1 -1
  5. package/src/_internal/app-queries.ts +1 -1
  6. package/src/_internal/application-package.ts +2 -1
  7. package/src/_internal/capabilities.ts +17 -2
  8. package/src/_internal/define-app.ts +2 -0
  9. package/src/ai/app-skill-store.ts +2 -1
  10. package/src/ai/app-skills.ts +2 -1
  11. package/src/ai/approvals.ts +2 -1
  12. package/src/ai/chat-tasks.ts +2 -1
  13. package/src/ai/credentials.ts +2 -1
  14. package/src/ai/dictation-runtime.ts +1 -1
  15. package/src/ai/dictations.ts +1 -1
  16. package/src/ai/files-store.ts +2 -1
  17. package/src/ai/inference-calls.ts +2 -1
  18. package/src/ai/map-store.ts +1 -1
  19. package/src/ai/map-tool.ts +1 -1
  20. package/src/ai/memories.ts +1 -1
  21. package/src/ai/memory-learning-runs.ts +1 -1
  22. package/src/ai/memory-learning.ts +1 -1
  23. package/src/ai/memory-workflow-evidence.ts +1 -1
  24. package/src/ai/message-queue.ts +2 -1
  25. package/src/ai/migrate.ts +1 -1
  26. package/src/ai/model-access-migrate.ts +1 -1
  27. package/src/ai/model-access.ts +1 -1
  28. package/src/ai/model-catalog.ts +1 -1
  29. package/src/ai/model-delete.ts +1 -1
  30. package/src/ai/model-usage.ts +2 -1
  31. package/src/ai/open-questions.ts +1 -1
  32. package/src/ai/openai-api-slots.ts +2 -1
  33. package/src/ai/prefs.ts +2 -1
  34. package/src/ai/projects.ts +2 -1
  35. package/src/ai/provider-connections.ts +1 -1
  36. package/src/ai/quota-limits.ts +2 -1
  37. package/src/ai/quota-provider.ts +2 -1
  38. package/src/ai/quota-report.ts +2 -1
  39. package/src/ai/quotas-migrate.ts +2 -1
  40. package/src/ai/quotas.ts +2 -1
  41. package/src/ai/request-headers.ts +2 -1
  42. package/src/ai/review-content.ts +2 -1
  43. package/src/ai/skill-search.ts +2 -1
  44. package/src/ai/skills.ts +2 -1
  45. package/src/ai/store.ts +2 -1
  46. package/src/ai/structured-runs.ts +2 -1
  47. package/src/ai/tool-audit.ts +2 -1
  48. package/src/ai/turn-timing.ts +1 -1
  49. package/src/ai/usage.ts +2 -1
  50. package/src/ai/web-provenance.ts +2 -1
  51. package/src/api/admin-ai-quotas.ts +1 -1
  52. package/src/api/admin-ai.ts +1 -1
  53. package/src/api/admin-app-keys.ts +47 -3
  54. package/src/api/admin-core-settings.ts +1 -1
  55. package/src/api/admin-outgoing-mail.ts +1 -1
  56. package/src/api/app-bootstrap.ts +23 -1
  57. package/src/api/app-events.ts +1 -1
  58. package/src/api/auth.ts +1 -1
  59. package/src/api/authenticate-app-request.ts +129 -0
  60. package/src/api/capabilities.ts +79 -5
  61. package/src/api/index.ts +2 -0
  62. package/src/api/me.ts +1 -1
  63. package/src/api/search.bench.ts +1 -1
  64. package/src/app-pwa/service.ts +2 -1
  65. package/src/app-pwa/transaction.ts +4 -2
  66. package/src/capabilities/claims.ts +2 -1
  67. package/src/capabilities/executions.ts +8 -4
  68. package/src/capabilities/geo-client.ts +1 -1
  69. package/src/capabilities/migrate.ts +3 -1
  70. package/src/capabilities/resource-readers.ts +2 -2
  71. package/src/capabilities/server-invoke.ts +45 -18
  72. package/src/capabilities/server.ts +2 -1
  73. package/src/cli/admin/app-keys.ts +25 -3
  74. package/src/config/env.ts +4 -4
  75. package/src/contracts/capabilities.ts +15 -2
  76. package/src/contracts/outgoing-mail.ts +1 -1
  77. package/src/events/app-event-delivery.ts +1 -1
  78. package/src/events/live-delivery.ts +2 -1
  79. package/src/runtime/bootstrap-protocol.ts +11 -4
  80. package/src/runtime/clients.ts +6 -2
  81. package/src/server/index.ts +1 -0
  82. package/src/server/ratelimit.ts +1 -1
  83. package/src/server/services/access.ts +2 -1
  84. package/src/services/account-category-policy.ts +1 -1
  85. package/src/services/account-lifecycle/audit.ts +1 -1
  86. package/src/services/account-lifecycle/index.ts +1 -1
  87. package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
  88. package/src/services/accounts/app.ts +1 -1
  89. package/src/services/accounts/avatar.ts +1 -1
  90. package/src/services/accounts/base-group.ts +1 -1
  91. package/src/services/accounts/directory-shares.ts +1 -1
  92. package/src/services/accounts/duplicate-emails.ts +1 -1
  93. package/src/services/accounts/entities.ts +2 -1
  94. package/src/services/accounts/group-sql.ts +2 -1
  95. package/src/services/accounts/groups.ts +1 -1
  96. package/src/services/accounts/identities.ts +13 -11
  97. package/src/services/accounts/ipa-data.ts +1 -1
  98. package/src/services/accounts/local-groups.ts +2 -1
  99. package/src/services/accounts/machine-group-management.ts +2 -1
  100. package/src/services/accounts/posix.ts +13 -12
  101. package/src/services/accounts/request-policy.ts +2 -1
  102. package/src/services/accounts/switching.ts +1 -1
  103. package/src/services/accounts/users.ts +2 -1
  104. package/src/services/announcements/index.ts +1 -1
  105. package/src/services/app-access-management.ts +2 -1
  106. package/src/services/app-access.ts +2 -1
  107. package/src/services/app-admin-facts.ts +11 -3
  108. package/src/services/app-approval.ts +34 -30
  109. package/src/services/app-bootstrap.ts +104 -21
  110. package/src/services/app-details.ts +3 -2
  111. package/src/services/app-events/accept.ts +5 -5
  112. package/src/services/app-events/admin.ts +15 -15
  113. package/src/services/app-events/declarations.ts +6 -3
  114. package/src/services/app-events/deliver.ts +6 -6
  115. package/src/services/app-events.ts +3 -2
  116. package/src/services/app-keys.ts +42 -11
  117. package/src/services/approvals/content.ts +1 -1
  118. package/src/services/approvals/index.ts +2 -1
  119. package/src/services/approvals/list.ts +1 -1
  120. package/src/services/approvals/migrate.ts +1 -1
  121. package/src/services/audit/index.ts +2 -1
  122. package/src/services/auth-flows/ipa.ts +1 -1
  123. package/src/services/auth-flows/magic-link.ts +1 -1
  124. package/src/services/auth-flows/password-reset.ts +1 -1
  125. package/src/services/auth-flows/proxy-return.ts +1 -1
  126. package/src/services/geo/admin.ts +13 -12
  127. package/src/services/geo/identity.ts +46 -0
  128. package/src/services/geo/ledger.ts +118 -106
  129. package/src/services/geo/migrate.ts +2 -1
  130. package/src/services/geo/route-chain.ts +3 -3
  131. package/src/services/geo/service.ts +4 -24
  132. package/src/services/geoip/index.ts +5 -5
  133. package/src/services/geoip/store.ts +24 -21
  134. package/src/services/help/index.ts +2 -1
  135. package/src/services/help/store.ts +2 -1
  136. package/src/services/identity/app-request.ts +44 -0
  137. package/src/services/identity/constants.ts +1 -0
  138. package/src/services/identity/index.ts +1 -0
  139. package/src/services/identity/invocation-actor.ts +2 -1
  140. package/src/services/identity/invocation-token.ts +112 -0
  141. package/src/services/identity/key-ring.ts +2 -2
  142. package/src/services/identity/resource-readers.ts +1 -1
  143. package/src/services/identity/routes.ts +11 -3
  144. package/src/services/identity/runtime-config.ts +14 -3
  145. package/src/services/identity/workload-auth.ts +2 -0
  146. package/src/services/identity/workload-runtime.ts +1 -1
  147. package/src/services/ipa/groups.ts +1 -1
  148. package/src/services/ipa/profile.ts +1 -1
  149. package/src/services/ipa/search.ts +1 -1
  150. package/src/services/ipa/sync.ts +1 -1
  151. package/src/services/ipa/users.ts +1 -1
  152. package/src/services/known-browsers.ts +2 -1
  153. package/src/services/legal-consent.ts +1 -1
  154. package/src/services/logging/index.ts +6 -3
  155. package/src/services/logging/trace.ts +1 -1
  156. package/src/services/logging/web-vitals.ts +1 -1
  157. package/src/services/mandates/index.ts +2 -1
  158. package/src/services/notifications/batches.ts +1 -1
  159. package/src/services/notifications/browser.ts +1 -1
  160. package/src/services/notifications/catalog.ts +1 -1
  161. package/src/services/notifications/dispatcher.ts +1 -1
  162. package/src/services/notifications/index.ts +1 -1
  163. package/src/services/notifications/observability.ts +2 -1
  164. package/src/services/notifications/platform.ts +1 -1
  165. package/src/services/notifications/quiet.ts +1 -1
  166. package/src/services/notifications/user.ts +1 -1
  167. package/src/services/oauth-tokens.ts +1 -1
  168. package/src/services/outbox.ts +15 -14
  169. package/src/services/outgoing-mail/admin.ts +1 -1
  170. package/src/services/outgoing-mail/dispatcher.ts +1 -1
  171. package/src/services/outgoing-mail/drain.ts +2 -1
  172. package/src/services/outgoing-mail/enqueue.ts +2 -1
  173. package/src/services/outgoing-mail/messages.ts +2 -1
  174. package/src/services/outgoing-mail/retention.ts +1 -1
  175. package/src/services/outgoing-mail/send.ts +1 -1
  176. package/src/services/outgoing-mail/store.ts +2 -1
  177. package/src/services/panel-context-consent.ts +32 -28
  178. package/src/services/providers/local/auth.ts +1 -1
  179. package/src/services/providers/local/users.ts +1 -1
  180. package/src/services/pwa-devices.ts +2 -1
  181. package/src/services/rail-preferences.ts +16 -12
  182. package/src/services/rail-shortcuts.ts +12 -10
  183. package/src/services/request-cache-redis.ts +3 -3
  184. package/src/services/service-account-credentials.ts +2 -1
  185. package/src/services/service-accounts.ts +2 -1
  186. package/src/services/session/index.ts +2 -1
  187. package/src/services/session/user.ts +2 -1
  188. package/src/services/settings/index.ts +1 -1
  189. package/src/services/settings/store.ts +1 -1
  190. package/src/services/settings/versions.ts +2 -1
  191. package/src/services/weather/forecast.ts +2 -1
  192. package/src/services/weather/locations.ts +1 -1
  193. package/src/services/weather/migrate.ts +2 -1
  194. package/src/services/weather/short-id.ts +2 -1
  195. package/src/services/webauthn.ts +1 -1
  196. package/src/shared-text/documents.ts +2 -2
  197. package/src/ssr/compact-shell.browser-harness.ts +2 -0
  198. package/src/workflows/ai/store.ts +2 -1
  199. package/src/workflows/store/budget.ts +2 -1
  200. package/src/workflows/store/definitions.ts +2 -1
  201. package/src/workflows/store/events.ts +2 -1
  202. package/src/workflows/store/observability.ts +2 -1
  203. package/src/workflows/store/retention.ts +2 -1
  204. package/src/workflows/store/runs.ts +2 -1
  205. package/src/workflows/store/transaction.ts +2 -1
  206. package/src/workflows/store/worker.ts +2 -1
@@ -1,7 +1,8 @@
1
- import { type SQLQuery, sql } from "bun";
1
+ import type { SQLQuery } from "bun";
2
2
  import { z } from "zod";
3
3
  import { DEFAULT_LINUX_IDENTITY_CONFIGURATION, LinuxIdentityConfigurationSchema } from "../../contracts/posix";
4
4
  import type { RequestActor, UserProfile, UserProvider } from "../../contracts/shared";
5
+ import { sql } from "../../runtime/clients";
5
6
  import { userFromActor } from "../../server/actor";
6
7
  import { isAccountCategoryAllowed } from "../account-category-policy";
7
8
  import { toPgTextArray } from "../postgres";
@@ -66,9 +67,10 @@ const cursor = (after?: string): string | null => {
66
67
  };
67
68
 
68
69
  /** Server-side identity reads. Authentication and application resource authorization remain caller-owned. */
69
- export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdentity = readUpstreamIdentity) => {
70
+ export const createAccountIdentityService = (injectedDb?: typeof sql, upstreamIdentity = readUpstreamIdentity) => {
71
+ const db = () => injectedDb ?? sql;
70
72
  const setting = async (key: string, fallback: unknown): Promise<unknown> => {
71
- const [row] = await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
73
+ const [row] = await db()<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
72
74
  return row ? decryptValue(row.value) : fallback;
73
75
  };
74
76
  const availability = async (): Promise<AccountIdentityAvailability> => {
@@ -84,7 +86,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
84
86
  throw new AccountIdentityError("invalid_identity_configuration");
85
87
  }
86
88
  };
87
- const userRows = (filter: SQLQuery) => db<UserRow[]>`
89
+ const userRows = (filter: SQLQuery) => db()<UserRow[]>`
88
90
  SELECT u.id, u.provider, u.uid, u.profile, u.admin,
89
91
  (u.account_expires IS NOT NULL AND u.account_expires <= now()) AS expired,
90
92
  p.managed_by, p.uid_number, p.primary_gid_number
@@ -98,7 +100,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
98
100
  const [row] = await userRows(sql`u.id = ${supplied.id}::uuid`);
99
101
  if (!row || row.expired || row.provider !== supplied.provider || row.uid !== supplied.uid || row.profile !== supplied.profile)
100
102
  throw new AccountIdentityError("identity_unavailable");
101
- if (!(await isAccountCategoryAllowed(row, db))) throw new AccountIdentityError("identity_unavailable");
103
+ if (!(await isAccountCategoryAllowed(row, db()))) throw new AccountIdentityError("identity_unavailable");
102
104
  const state = await availability();
103
105
  if (row.provider === "ipa" && !state.freeipaEnabled) throw new AccountIdentityError("identity_unavailable");
104
106
  return { row, state, supplied };
@@ -113,14 +115,14 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
113
115
  if (!Array.isArray(configured) || !configured.every((value): value is string => typeof value === "string"))
114
116
  throw new AccountIdentityError("invalid_identity_configuration");
115
117
  const names = configured.length > 0 ? configured : ["admins"];
116
- const [membership] = await db`SELECT 1 FROM auth.ipa_user_effective_groups
118
+ const [membership] = await db()`SELECT 1 FROM auth.ipa_user_effective_groups
117
119
  WHERE user_id = ${current.row.id}::uuid AND group_name = ANY(${toPgTextArray(names)}::text[]) LIMIT 1`;
118
120
  if (!membership) throw new AccountIdentityError("admin_required");
119
121
  }
120
122
  return current;
121
123
  };
122
124
  const groupRows = async (filter: SQLQuery) => {
123
- const rows = await db<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
125
+ const rows = await db()<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
124
126
  SELECT g.id, g.provider, g.name, g.gid_number,
125
127
  EXISTS(SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id) AS personal
126
128
  FROM auth.groups g
@@ -189,7 +191,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
189
191
  eligible: current.profile === "user",
190
192
  };
191
193
  }
192
- const [ownership] = await db<{ personal: boolean }[]>`SELECT EXISTS (
194
+ const [ownership] = await db()<{ personal: boolean }[]>`SELECT EXISTS (
193
195
  SELECT 1 FROM auth.personal_groups p
194
196
  WHERE (${input.identityId ?? null}::uuid IS NOT NULL AND p.group_id = ${input.identityId ?? null}::uuid)
195
197
  OR (${input.identityId ?? null}::uuid IS NULL AND p.group_name = ${input.name})
@@ -238,10 +240,10 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
238
240
  const after = cursor(input.after);
239
241
  const state = await availability();
240
242
  const [localAllowed, ipaAllowed] = await Promise.all([
241
- isAccountCategoryAllowed({ provider: "local", profile: "user" }, db),
242
- isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db),
243
+ isAccountCategoryAllowed({ provider: "local", profile: "user" }, db()),
244
+ isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db()),
243
245
  ]);
244
- const rows = await db<
246
+ const rows = await db()<
245
247
  {
246
248
  id: string;
247
249
  provider: UserProvider;
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { IpaUserData } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
 
4
4
  type DbRow = Record<string, unknown>;
5
5
 
@@ -1,5 +1,6 @@
1
- import { sql, type TransactionSQL } from "bun";
1
+ import type { TransactionSQL } from "bun";
2
2
  import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
3
+ import { sql } from "../../runtime/clients";
3
4
  import { escapeLikePattern, isUniqueViolation } from "../postgres";
4
5
  import { buildBaseGroup, personalOwnerJoin } from "./base-group";
5
6
  import { recursiveServiceAccountGroupIdsSubquery } from "./group-sql";
@@ -1,4 +1,5 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../../runtime/clients";
2
+
2
3
  import type { ServiceAccountRequestActor } from "../../server/middleware/auth";
3
4
  import { err, fail, ok, type Result } from "../../server/services";
4
5
  import { audit } from "../audit";
@@ -1,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import {
3
2
  DEFAULT_LINUX_IDENTITY_CONFIGURATION,
4
3
  isPosixName,
@@ -8,6 +7,7 @@ import {
8
7
  PosixOverridesSchema,
9
8
  } from "../../contracts/posix";
10
9
  import type { MutationResult } from "../../contracts/shared";
10
+ import { sql } from "../../runtime/clients";
11
11
  import { freeipa } from "../../server/services";
12
12
  import { type AuditActor, audit } from "../audit";
13
13
  import { getFreeIpaConfig } from "../freeipa-config";
@@ -128,8 +128,9 @@ const candidate = (row: IdentityRow): PosixCandidate => {
128
128
  };
129
129
 
130
130
  // Database injection is used by isolated integration tests; production uses the shared pool.
131
- export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdRanges) => {
132
- const readConfig = async (connection = db): Promise<LinuxIdentityConfiguration> => {
131
+ export const createPosixRuntime = (injectedDb?: typeof sql, readRanges = readIpaIdRanges) => {
132
+ const db = () => injectedDb ?? sql;
133
+ const readConfig = async (connection = db()): Promise<LinuxIdentityConfiguration> => {
133
134
  // Provisioning must not use a stale cache after an operator disables or changes the range.
134
135
  const [row] = await connection<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${CONFIG_KEY}`;
135
136
  try {
@@ -256,7 +257,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
256
257
  actor?: AuditActor,
257
258
  ): Promise<MutationResult<{ id: string }>> => {
258
259
  try {
259
- return await db.begin(async (tx) => {
260
+ return await db().begin(async (tx) => {
260
261
  await lock(tx);
261
262
  const result = await write(tx);
262
263
  if (!result.ok) return result;
@@ -316,13 +317,13 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
316
317
  },
317
318
  async overview(actor: Actor, after: string | null = null, filters: { search?: string; scope?: "ready" | "all" } = {}) {
318
319
  requireAdmin(actor);
319
- const result = await rows(db, null, after, filters);
320
+ const result = await rows(db(), null, after, filters);
320
321
  const items = result.slice(0, POSIX_PAGE_SIZE).map(candidate);
321
322
  return { config: await service.configuration(actor), items, nextCursor: result.length > POSIX_PAGE_SIZE ? items.at(-1)!.id : null };
322
323
  },
323
324
  async get(actor: Actor, id: string) {
324
325
  requireAdmin(actor);
325
- const [row] = await rows(db, id, null);
326
+ const [row] = await rows(db(), id, null);
326
327
  if (!row) throw new PosixError("user_not_found", 404);
327
328
  return { config: await readConfig(), user: candidate(row) };
328
329
  },
@@ -331,7 +332,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
331
332
  const parsed = LinuxIdentityConfigurationSchema.safeParse(input);
332
333
  if (!parsed.success) throw new PosixError("invalid_configuration", 400);
333
334
  const ranges = parsed.data.enabled ? await readRanges() : [];
334
- await db.begin(async (tx) => {
335
+ await db().begin(async (tx) => {
335
336
  // Same order as provisioning; settings write upgrades this lock in the same transaction.
336
337
  await lock(tx);
337
338
  if (options?.replaceUnreadable !== true) {
@@ -360,7 +361,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
360
361
  },
361
362
  async provision(actor: Actor, id: string) {
362
363
  requireAdmin(actor);
363
- return db.begin(async (tx) => {
364
+ return db().begin(async (tx) => {
364
365
  await lock(tx);
365
366
  const config = await readConfig(tx);
366
367
  if (!config.enabled) throw new PosixError("setup_disabled");
@@ -371,7 +372,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
371
372
  requireAdmin(actor);
372
373
  const parsed = PosixOverridesSchema.safeParse(input);
373
374
  if (!parsed.success) throw new PosixError("invalid_paths", 400);
374
- return db.begin(async (tx) => {
375
+ return db().begin(async (tx) => {
375
376
  await lock(tx);
376
377
  const [row] = await rows(tx, id, null);
377
378
  if (!row) throw new PosixError("user_not_found", 404);
@@ -394,7 +395,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
394
395
  async createGroup(actor: Actor, input: { name: string; description?: string }) {
395
396
  requireAdmin(actor);
396
397
  const ranges = await readRanges();
397
- return db.begin(async (tx) => {
398
+ return db().begin(async (tx) => {
398
399
  await lock(tx);
399
400
  const created = await createLocalGroup(input, tx);
400
401
  if (!created.ok) throw new PosixError("group_conflict");
@@ -405,7 +406,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
405
406
  async provisionGroup(actor: Actor, id: string) {
406
407
  requireAdmin(actor);
407
408
  const ranges = await readRanges();
408
- return db.begin(async (tx) => {
409
+ return db().begin(async (tx) => {
409
410
  await lock(tx);
410
411
  return assignGroup(tx, actor, id, ranges);
411
412
  });
@@ -413,5 +414,5 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
413
414
  };
414
415
  return { service, writeLocalAccount };
415
416
  };
416
- export const createPosixService = (db: typeof sql = sql, readRanges = readIpaIdRanges) => createPosixRuntime(db, readRanges).service;
417
+ export const createPosixService = (db?: typeof sql, readRanges = readIpaIdRanges) => createPosixRuntime(db, readRanges).service;
417
418
  export const { service: posix, writeLocalAccount } = createPosixRuntime();
@@ -1,4 +1,5 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../../runtime/clients";
2
+
2
3
  import { decryptValue } from "../settings/crypto";
3
4
 
4
5
  /** Read durable policy so disabling new requests is not delayed by the settings cache. */
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { UserProfile, UserProvider } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
  import * as settings from "../settings";
4
4
  import type { IpaAccountTransitionPolicy } from "./model";
5
5
 
@@ -1,4 +1,4 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type {
3
3
  BaseUser,
4
4
  MutationResult,
@@ -9,6 +9,7 @@ import type {
9
9
  UserProfile,
10
10
  UserProvider,
11
11
  } from "../../contracts/shared";
12
+ import { sql } from "../../runtime/clients";
12
13
  import { freeipa } from "../../server/services";
13
14
  import { createAuthLoginUrl } from "../../shared/redirect";
14
15
  import { isAccountCategoryAllowed } from "../account-category-policy";
@@ -1,5 +1,4 @@
1
1
  import { err, fail, ok, type Result } from "@k2b/stdlib";
2
- import { sql } from "bun";
3
2
  import { HTTPException } from "hono/http-exception";
4
3
  import { z } from "zod";
5
4
  import type {
@@ -11,6 +10,7 @@ import type {
11
10
  } from "../../contracts/announcements";
12
11
  import { AnnouncementDisplayEntrySchema } from "../../contracts/announcements";
13
12
  import { hasRole, type User } from "../../contracts/shared";
13
+ import { sql } from "../../runtime/clients";
14
14
  import { markdown } from "../../shared/markdown";
15
15
  import { claimCacheFill, completeCacheFill } from "../cache-fill";
16
16
  import { logger } from "../logging";
@@ -1,8 +1,9 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { managerEligibility } from "../_internal/app-access-manager-sql";
3
3
  import type { AppMeta } from "../contracts/app";
4
4
  import type { AppAccessDeclaration, AppAccessSnapshot } from "../contracts/app-access";
5
5
  import { publishAppAccessChanged, wakeAppAccessChanged } from "../events/app-access";
6
+ import { sql } from "../runtime/clients";
6
7
  import { isAppSessionActor, userFromActor } from "../server/actor";
7
8
  import type { RequestAuthority } from "../server/middleware/auth";
8
9
  import {
@@ -1,8 +1,9 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type { Context } from "hono";
3
3
  import { RESOURCE_ACCESS_TIMEOUT_MS, resourceAppAccess } from "../_internal/app-access-resources";
4
4
  import type { AppMeta } from "../contracts/app";
5
5
  import { AppAccessRuleSchema, type FixedAppAccessRule } from "../contracts/app-access";
6
+ import { sql } from "../runtime/clients";
6
7
  import { userFromActor } from "../server/actor";
7
8
  import { type AuthContext, auth, type RequestAuthority } from "../server/middleware/auth";
8
9
  import { buildAccessPrincipalCondition } from "../server/services/access";
@@ -1,7 +1,8 @@
1
- import { sql } from "bun";
2
1
  import { managerEligibility } from "../_internal/app-access-manager-sql";
2
+ import { sql } from "../runtime/clients";
3
3
  import type { RequestAuthority } from "../server/middleware/auth";
4
4
  import { AppAccessManagementError, canRepairAppAccess } from "./app-access-management";
5
+ import type { LastRejectedStart } from "./app-keys";
5
6
 
6
7
  export type AppAdminFacts = {
7
8
  access: { people: number; groups: number; serviceAccounts: number; managers: number; configured: boolean } | null;
@@ -14,12 +15,14 @@ export type AppAdminFacts = {
14
15
  accountStatus: "active" | "disabled" | null;
15
16
  };
16
17
  tokens: { active: number; total: number };
18
+ /** Core's last refusal of a start with a verified proof; cleared by the next successful start. */
19
+ lastRejectedStart: Omit<LastRejectedStart, "keyId"> | null;
17
20
  events: { hears: number; sends: number | null; failedDeliveries: number; waitingDeliveries: number };
18
21
  notifications: number | null;
19
22
  firstKnownAt: string | null;
20
23
  };
21
24
 
22
- /** Core-owned administration summary: at most 500 input IDs and four batched SQL reads. */
25
+ /** Core-owned administration summary: at most 500 input IDs and five batched SQL reads. */
23
26
  export const appAdminFacts = async (authority: RequestAuthority, appIds: readonly string[]): Promise<Map<string, AppAdminFacts>> => {
24
27
  if (!canRepairAppAccess(authority)) throw new AppAccessManagementError("FORBIDDEN", "Cloud administration authority is required.");
25
28
  if (appIds.length > 500) throw new AppAccessManagementError("TOO_MANY_APPS", "At most 500 application IDs may be read at once.", 400);
@@ -31,6 +34,7 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
31
34
  access: null,
32
35
  keys: { active: 0, expired: 0, revoked: 0, lastUsedAt: null, firstCreatedAt: null, accountStatus: null },
33
36
  tokens: { active: 0, total: 0 },
37
+ lastRejectedStart: null,
34
38
  events: { hears: 0, sends: 0, failedDeliveries: 0, waitingDeliveries: 0 },
35
39
  notifications: 0,
36
40
  firstKnownAt: null,
@@ -39,7 +43,7 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
39
43
  );
40
44
  if (!ids.length) return facts;
41
45
 
42
- const [access, credentials, events, notifications] = await Promise.all([
46
+ const [access, credentials, events, notifications, rejections] = await Promise.all([
43
47
  sql<
44
48
  {
45
49
  app_id: string;
@@ -125,6 +129,9 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
125
129
  SELECT app_id, count(*)::int AS count FROM notifications.definitions
126
130
  WHERE app_id IN ${sql(ids)} AND active = true GROUP BY app_id
127
131
  `,
132
+ sql<{ app_id: string; rejected_at: Date; reason: LastRejectedStart["reason"] }[]>`
133
+ SELECT app_id, rejected_at, reason FROM auth.app_start_rejections WHERE app_id IN ${sql(ids)}
134
+ `,
128
135
  ]);
129
136
 
130
137
  for (const row of access) {
@@ -156,5 +163,6 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
156
163
  facts.get(row.app_id)!.events = { hears: row.hears, sends: row.sends, failedDeliveries: row.failed, waitingDeliveries: row.waiting };
157
164
  }
158
165
  for (const row of notifications) facts.get(row.app_id)!.notifications = row.count;
166
+ for (const row of rejections) facts.get(row.app_id)!.lastRejectedStart = { at: row.rejected_at.toISOString(), reason: row.reason };
159
167
  return facts;
160
168
  };
@@ -1,4 +1,4 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type { z } from "zod";
3
3
  import { lazySync } from "../_internal/process-sync";
4
4
  import { env } from "../config/env";
@@ -25,6 +25,7 @@ import {
25
25
  appPairingProofMessage,
26
26
  APP_APPROVAL_LIMITS as limits,
27
27
  } from "../contracts/app-approval";
28
+ import { sql } from "../runtime/clients";
28
29
  import { addressPrefix } from "../server/request-context/address";
29
30
  import { publicCloudOrigin } from "../shared/app-url";
30
31
  import { isAccountCategoryAllowed } from "./account-category-policy";
@@ -296,12 +297,13 @@ const verify = async (key: AppDevicePublicKey, message: string, signature: strin
296
297
  /** One owner for production API and isolated HTTP consumers. Config is re-read
297
298
  * per operation; SQL transactions serialize device decisions and revocation. */
298
299
  export const createAppApprovalService = (
299
- db: SQL = sql,
300
- configuration = (requireEnabled = true) => readAppApprovalConfig(db, requireEnabled),
300
+ injectedDb?: SQL,
301
+ configuration = (requireEnabled = true) => readAppApprovalConfig(injectedDb ?? sql, requireEnabled),
301
302
  hints: AppLoginDecisionHints = syncLoginDecisionHints,
302
303
  push: AppLoginPushSender = fetchPushSender,
303
304
  addresses: AppLoginAddressStore = valkeyAppLoginAddressStore,
304
305
  ) => {
306
+ const db = () => injectedDb ?? sql;
305
307
  // Error messages may contain their inputs; log only the error class, never addresses or proofs.
306
308
  const addressFailure = (error: unknown) =>
307
309
  log.warn("App login address hint unavailable", { error: error instanceof Error ? error.name : "UnknownError" });
@@ -332,12 +334,12 @@ export const createAppApprovalService = (
332
334
  const cleanup = async () => {
333
335
  // Bounded opportunistic maintenance: each accepted write retires up to 100
334
336
  // expired rows per table, so bursts cannot trigger an unbounded sweep.
335
- await db`DELETE FROM auth.app_device_changes WHERE id IN (SELECT id FROM auth.app_device_changes WHERE expires_at < now() - interval '1 minute' ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
336
- await db`DELETE FROM auth.app_pairings WHERE id IN (SELECT p.id FROM auth.app_pairings p
337
+ await db()`DELETE FROM auth.app_device_changes WHERE id IN (SELECT id FROM auth.app_device_changes WHERE expires_at < now() - interval '1 minute' ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
338
+ await db()`DELETE FROM auth.app_pairings WHERE id IN (SELECT p.id FROM auth.app_pairings p
337
339
  WHERE p.expires_at < now() - interval '1 minute' AND NOT EXISTS (SELECT 1 FROM auth.app_device_changes c WHERE c.pairing_id=p.id)
338
340
  ORDER BY p.expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
339
- await db`DELETE FROM auth.app_logins WHERE id IN (SELECT id FROM auth.app_logins WHERE expires_at < now() AND NOT (kind='terminal' AND state='approved') ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
340
- await db`DELETE FROM auth.app_device_proofs WHERE (device_id,jti) IN (SELECT device_id,jti FROM auth.app_device_proofs WHERE expires_at < now() ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
341
+ await db()`DELETE FROM auth.app_logins WHERE id IN (SELECT id FROM auth.app_logins WHERE expires_at < now() AND NOT (kind='terminal' AND state='approved') ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
342
+ await db()`DELETE FROM auth.app_device_proofs WHERE (device_id,jti) IN (SELECT device_id,jti FROM auth.app_device_proofs WHERE expires_at < now() ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
341
343
  };
342
344
  const pairingOwner = async (tx: SQL, actor: AppApprovalActor, pairing: PairingRow, cfg: AppApprovalConfig) => {
343
345
  if (
@@ -409,7 +411,7 @@ export const createAppApprovalService = (
409
411
  if (!cfg.enabled || !cfg.appOrigin) return;
410
412
  try {
411
413
  // A withdrawn device change keeps its window, so withdrawing and asking again cannot push without bound.
412
- const rows = await db<{ push_token: string }[]>`
414
+ const rows = await db()<{ push_token: string }[]>`
413
415
  UPDATE auth.app_devices d SET login_wake_at=now(), login_wake_ref=${requestId}::uuid
414
416
  WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND revoked_at IS NULL AND push_token IS NOT NULL
415
417
  AND (${options?.accept ?? null}::text IS NULL OR ${options?.accept ?? null}::text=ANY(d.accepts))
@@ -423,7 +425,7 @@ export const createAppApprovalService = (
423
425
  Array.from(new Set(rows.map((row) => row.push_token)), async (token) => {
424
426
  const status = await push(cfg.appOrigin, { token, cloudOrigin: cfg.issuer, requestRef: requestId });
425
427
  if (status === 410)
426
- await db`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
428
+ await db()`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
427
429
  }),
428
430
  );
429
431
  } catch (error) {
@@ -463,7 +465,7 @@ export const createAppApprovalService = (
463
465
  try {
464
466
  const cfg = await config(false);
465
467
  if (!cfg.enabled || !cfg.appOrigin) return;
466
- const rows = await db<{ push_token: string; user_id: string }[]>`
468
+ const rows = await db()<{ push_token: string; user_id: string }[]>`
467
469
  UPDATE auth.app_devices d SET action_wake_at=now(), action_wake_ref=${approvalId}::uuid
468
470
  WHERE d.id IN (
469
471
  SELECT device.id FROM auth.app_devices device JOIN approvals.requests r ON r.approver_user_id=device.user_id
@@ -483,7 +485,7 @@ export const createAppApprovalService = (
483
485
  Array.from(tokens, async ([token, userId]) => {
484
486
  const status = await push(cfg.appOrigin, { token, cloudOrigin: cfg.issuer, requestRef: approvalId, kind: "action" });
485
487
  if (status === 410)
486
- await db`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
488
+ await db()`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
487
489
  }),
488
490
  );
489
491
  } catch (error) {
@@ -492,7 +494,7 @@ export const createAppApprovalService = (
492
494
  },
493
495
  maintain: async (notify: (notice: AppDeviceEnrollmentNotice) => Promise<unknown>, signal?: AbortSignal) => {
494
496
  await cleanup();
495
- const rows = await db<(DeviceRow & { has_email: boolean })[]>`
497
+ const rows = await db()<(DeviceRow & { has_email: boolean })[]>`
496
498
  SELECT d.*, COALESCE(btrim(u.mail), '') <> '' AS has_email
497
499
  FROM auth.app_devices d JOIN auth.users u ON u.id = d.user_id
498
500
  WHERE d.notified_at IS NULL ORDER BY d.created_at LIMIT ${limits.pageSize}`;
@@ -503,7 +505,7 @@ export const createAppApprovalService = (
503
505
  // The notice is email-only, so an account without email relies on the
504
506
  // enrollment audit entry instead of a delivery that can only fail.
505
507
  if (row.has_email) await notify({ deviceId: row.id, userId: row.user_id, name: row.name, assisted: row.assisted });
506
- await db`UPDATE auth.app_devices SET notified_at=now() WHERE id=${row.id}::uuid AND notified_at IS NULL`;
508
+ await db()`UPDATE auth.app_devices SET notified_at=now() WHERE id=${row.id}::uuid AND notified_at IS NULL`;
507
509
  }
508
510
  },
509
511
  info: async () => {
@@ -524,7 +526,7 @@ export const createAppApprovalService = (
524
526
  const id = crypto.randomUUID(),
525
527
  token = secret(),
526
528
  expiresAt = future(limits.pairingSeconds);
527
- return db.begin(async (tx) => {
529
+ return db().begin(async (tx) => {
528
530
  await fresh(tx, actor);
529
531
  if (targetId !== actor.userId && (!actor.admin || !cfg.adminPairing)) return reject("FORBIDDEN", 403);
530
532
  await tx`SELECT id FROM auth.users WHERE id = ${targetId}::uuid FOR UPDATE`;
@@ -549,7 +551,7 @@ export const createAppApprovalService = (
549
551
  claimPairing: async (claim: { pairingId: string; secret: string; publicKey: AppDevicePublicKey; name: string; signature: string }) => {
550
552
  const cfg = await config();
551
553
  if (!(await verify(claim.publicKey, appPairingProofMessage(cfg.issuer, claim), claim.signature))) return reject("FORBIDDEN", 403);
552
- return db.begin(async (tx) => {
554
+ return db().begin(async (tx) => {
553
555
  const [p] = await tx<
554
556
  PairingRow[]
555
557
  >`SELECT * FROM auth.app_pairings WHERE id = ${claim.pairingId}::uuid AND issuer = ${cfg.issuer} FOR UPDATE`;
@@ -564,7 +566,7 @@ export const createAppApprovalService = (
564
566
  },
565
567
  pairingResult: async (id: string, token: string, key: AppDevicePublicKey) => {
566
568
  const cfg = await config();
567
- const [p] = await db<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
569
+ const [p] = await db()<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
568
570
  if (!p || !matches(token, p.secret_hash)) return reject("FORBIDDEN", 403);
569
571
  const age = Date.now() - new Date(p.expires_at).getTime();
570
572
  if (age >= 0 && !(age < 60_000 && (p.state === "confirmed" || p.state === "cancelled"))) return reject("FORBIDDEN", 403);
@@ -576,7 +578,7 @@ export const createAppApprovalService = (
576
578
  },
577
579
  inspectPairing: async (actor: AppApprovalActor, id: string) => {
578
580
  const cfg = await config();
579
- return db.begin(async (tx) => {
581
+ return db().begin(async (tx) => {
580
582
  const [p] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
581
583
  if (!p) return reject("UNAVAILABLE", 404);
582
584
  const [change] = await tx<ChangeRow[]>`SELECT * FROM auth.app_device_changes WHERE pairing_id=${p.id}::uuid`;
@@ -598,7 +600,7 @@ export const createAppApprovalService = (
598
600
  confirmPairing: async (actor: AppApprovalActor, id: string, code: string): Promise<z.infer<typeof AppPairingConfirmResultSchema>> => {
599
601
  const cfg = await config();
600
602
  let wakeId: string | undefined;
601
- const result = await db.begin(async (tx) => {
603
+ const result = await db().begin(async (tx) => {
602
604
  const [snapshot] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id=${id}::uuid AND issuer=${cfg.issuer}`;
603
605
  if (!snapshot) return reject("CONFLICT", 409);
604
606
  await tx`SELECT id FROM auth.users WHERE id=${snapshot.user_id}::uuid FOR NO KEY UPDATE`;
@@ -636,7 +638,7 @@ export const createAppApprovalService = (
636
638
  },
637
639
  cancelPairing: async (actor: AppApprovalActor, id: string) => {
638
640
  const cfg = await config();
639
- return db.begin(async (tx) => {
641
+ return db().begin(async (tx) => {
640
642
  const [snapshot] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id=${id}::uuid AND issuer=${cfg.issuer}`;
641
643
  if (!snapshot) return reject("UNAVAILABLE", 404);
642
644
  await tx`SELECT id FROM auth.users WHERE id=${snapshot.user_id}::uuid FOR NO KEY UPDATE`;
@@ -656,7 +658,7 @@ export const createAppApprovalService = (
656
658
  const cfg = await config(false);
657
659
  const changeId = "changeId" in lookup ? lookup.changeId : null,
658
660
  deviceId = "deviceId" in lookup ? lookup.deviceId : null;
659
- const [row] = await db<ChangeRow[]>`SELECT * FROM auth.app_device_changes
661
+ const [row] = await db()<ChangeRow[]>`SELECT * FROM auth.app_device_changes
660
662
  WHERE issuer=${cfg.issuer} AND user_id=${actor.userId}::uuid AND requested_by_sid=${actor.sid}::uuid AND kind='revoke'
661
663
  AND (${changeId}::uuid IS NULL OR id=${changeId}::uuid) AND (${deviceId}::uuid IS NULL OR device_id=${deviceId}::uuid)
662
664
  ORDER BY created_at DESC, id DESC LIMIT 1`;
@@ -665,7 +667,7 @@ export const createAppApprovalService = (
665
667
  },
666
668
  cancelChange: async (actor: AppApprovalActor, id: string) => {
667
669
  const cfg = await config(false);
668
- return db.begin(async (tx) => {
670
+ return db().begin(async (tx) => {
669
671
  await tx`SELECT id FROM auth.users WHERE id=${actor.userId}::uuid FOR NO KEY UPDATE`;
670
672
  const change = await ownedChange(tx, cfg, actor, id);
671
673
  if (changeStatus(change).state !== "pending") return reject("CONFLICT", 409);
@@ -676,7 +678,7 @@ export const createAppApprovalService = (
676
678
  },
677
679
  listDevices: async (actor: AppApprovalActor, after?: string) => {
678
680
  const cfg = await config(false);
679
- const rows = await db<
681
+ const rows = await db()<
680
682
  DeviceRow[]
681
683
  >`SELECT * FROM auth.app_devices WHERE issuer=${cfg.issuer} AND user_id=${actor.userId}::uuid AND (${after ?? null}::uuid IS NULL OR id > ${after ?? null}::uuid) ORDER BY id LIMIT ${limits.pageSize + 1}`;
682
684
  return {
@@ -692,7 +694,7 @@ export const createAppApprovalService = (
692
694
  ): Promise<z.infer<typeof AppDeviceRevokeResultSchema> | undefined> => {
693
695
  const cfg = await config(false);
694
696
  let wakeId: string | undefined;
695
- const result = await db.begin(async (tx) => {
697
+ const result = await db().begin(async (tx) => {
696
698
  await tx`SELECT id FROM auth.users WHERE id=${actor.userId}::uuid FOR NO KEY UPDATE`;
697
699
  const user = await fresh(tx, actor);
698
700
  const [device] = await tx<DeviceRow[]>`SELECT * FROM auth.app_devices
@@ -723,7 +725,7 @@ export const createAppApprovalService = (
723
725
  listUserDevices: async (actor: AppDeviceAdministrator, userId: string): Promise<AppDeviceView[]> => {
724
726
  if (!actor.admin) return reject("FORBIDDEN", 403);
725
727
  const cfg = await config(false);
726
- const rows = await db<
728
+ const rows = await db()<
727
729
  DeviceRow[]
728
730
  >`SELECT * FROM auth.app_devices WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND revoked_at IS NULL ORDER BY created_at, id LIMIT ${limits.devicesPerAccount}`;
729
731
  return rows.map(view);
@@ -733,7 +735,7 @@ export const createAppApprovalService = (
733
735
  revokeUserDevice: async (actor: AppDeviceAdministrator, userId: string, id: string) => {
734
736
  if (!actor.admin) return reject("FORBIDDEN", 403);
735
737
  const cfg = await config(false);
736
- return db.begin(async (tx) => {
738
+ return db().begin(async (tx) => {
737
739
  const [device] = await tx<
738
740
  DeviceRow[]
739
741
  >`SELECT * FROM auth.app_devices WHERE id=${id}::uuid AND issuer=${cfg.issuer} AND user_id=${userId}::uuid FOR UPDATE`;
@@ -753,7 +755,7 @@ export const createAppApprovalService = (
753
755
  code = comparison(),
754
756
  expiresAt = future(limits.loginSeconds);
755
757
  let owner: string | undefined;
756
- await db.begin(async (tx) => {
758
+ await db().begin(async (tx) => {
757
759
  let candidate = await loginCandidate(tx, identifier, category);
758
760
  if (candidate && !(await loginBudget(tx, cfg.issuer, candidate.id))) candidate = undefined;
759
761
  const now = new Date();
@@ -789,7 +791,7 @@ export const createAppApprovalService = (
789
791
  code = comparison(),
790
792
  expiresAt = future(limits.loginSeconds);
791
793
  let owner: string | undefined;
792
- await db.begin(async (tx) => {
794
+ await db().begin(async (tx) => {
793
795
  let candidate = await loginCandidate(tx, input.identifier);
794
796
  const subject = candidate ?? { id: crypto.randomUUID(), profile: "user" as const };
795
797
  // Even a decoy performs the client-access, device and shared-budget queries.
@@ -830,7 +832,7 @@ export const createAppApprovalService = (
830
832
  return reject("FORBIDDEN", 403);
831
833
  await cleanup();
832
834
  let decidedBrowserId: string | undefined;
833
- const outcome = await db.begin(async (tx) => {
835
+ const outcome = await db().begin(async (tx) => {
834
836
  let changeUserId: string | undefined;
835
837
  if (proof.command.operation === "change") {
836
838
  const [snapshot] = await tx<
@@ -1105,7 +1107,9 @@ export const createAppApprovalService = (
1105
1107
  browserStatus: async (id: string, token: string, hold?: { ms: number; signal?: AbortSignal }) => {
1106
1108
  const cfg = await config();
1107
1109
  const read = async () => {
1108
- const [row] = await db<LoginRow[]>`SELECT * FROM auth.app_logins WHERE kind='browser' AND id=${id}::uuid AND issuer=${cfg.issuer}`;
1110
+ const [row] = await db()<
1111
+ LoginRow[]
1112
+ >`SELECT * FROM auth.app_logins WHERE kind='browser' AND id=${id}::uuid AND issuer=${cfg.issuer}`;
1109
1113
  if (!row || !matches(token, row.browser_hash)) return reject("UNAVAILABLE", 404);
1110
1114
  const expiresIn = new Date(row.expires_at).getTime() - Date.now();
1111
1115
  return { state: expiresIn <= 0 ? "expired" : row.state, expiresIn };
@@ -1134,7 +1138,7 @@ export const createAppApprovalService = (
1134
1138
  },
1135
1139
  consumeLogin: async (id: string, token: string) => {
1136
1140
  const cfg = await config();
1137
- return db.begin(async (tx) => {
1141
+ return db().begin(async (tx) => {
1138
1142
  // Lock order is device -> request for both approval and completion.
1139
1143
  const [snapshot] = await tx<
1140
1144
  LoginRow[]