@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,6 +1,6 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { decodeJwt, decodeProtectedHeader, type JWTPayload, jwtVerify } from "jose";
3
- import { importAppPublicKey } from "../app-key";
3
+ import { importAppPublicKey, validateAppKeyAppId } from "../app-key";
4
4
  import {
5
5
  BOOTSTRAP_PATH,
6
6
  BOOTSTRAP_PROOF_BYTES,
@@ -8,9 +8,11 @@ import {
8
8
  BOOTSTRAP_REPLAY_RETENTION_SECONDS,
9
9
  BOOTSTRAP_TYPE,
10
10
  type BootstrapIdentity,
11
+ BootstrapIdentitySchema,
11
12
  bootstrapBodyDigest,
12
13
  } from "../runtime/bootstrap-protocol";
13
- import { getActiveAppKey, getInstallationId } from "./app-keys";
14
+ import { sql } from "../runtime/clients";
15
+ import { getActiveAppKey, getInstallationId, getStoredAppKey, type LastRejectedStart } from "./app-keys";
14
16
  import { get } from "./settings";
15
17
 
16
18
  export class BootstrapProofError extends Error {
@@ -19,8 +21,34 @@ export class BootstrapProofError extends Error {
19
21
  this.name = "BootstrapProofError";
20
22
  }
21
23
  }
24
+ /** Evidence attached only after signature and every request claim have been verified. */
25
+ export class RejectedBootstrapStart extends BootstrapProofError {
26
+ constructor(
27
+ readonly identity: BootstrapIdentity,
28
+ readonly jti: string,
29
+ readonly reason: LastRejectedStart["reason"],
30
+ ) {
31
+ super(reason === "connections_not_permitted" ? reason : "invalid_proof");
32
+ }
33
+ }
22
34
  const invalid = () => new BootstrapProofError("invalid_proof");
23
35
 
36
+ /** A refused proof is never consumed. Already-consumed proofs cannot change administrator state. */
37
+ export const recordRejectedBootstrapStart = async (rejection: RejectedBootstrapStart, db: SQL = sql): Promise<void> => {
38
+ const { identity, jti, reason } = rejection;
39
+ await db`
40
+ INSERT INTO auth.app_start_rejections (app_id, rejected_at, reason, key_id)
41
+ SELECT ${identity.appId}, clock_timestamp(), ${reason}, ${identity.keyId}::uuid
42
+ WHERE NOT EXISTS (SELECT 1 FROM auth.app_proof_replays WHERE key_id = ${identity.keyId}::uuid AND jti = ${jti})
43
+ ON CONFLICT (app_id) DO UPDATE
44
+ SET rejected_at = EXCLUDED.rejected_at, reason = EXCLUDED.reason, key_id = EXCLUDED.key_id
45
+ `;
46
+ };
47
+
48
+ export const clearRejectedBootstrapStart = async (appId: string, db: SQL = sql): Promise<void> => {
49
+ await db`DELETE FROM auth.app_start_rejections WHERE app_id = ${appId}`;
50
+ };
51
+
24
52
  /** Shared Postgres uniqueness rejects a proof used concurrently on different Core replicas. */
25
53
  export const consumeBootstrapProof = async (keyId: string, jti: string, exp: number, db: SQL = sql): Promise<boolean> => {
26
54
  const rows = await db`
@@ -72,14 +100,10 @@ export const cleanupBootstrapProofs = async (
72
100
  return { deleted, remaining: backlog?.count ?? 0 };
73
101
  };
74
102
 
75
- /** No caches: every HTTP transmission rechecks revocation, expiry and account state in Postgres. */
76
- export const authenticateBootstrap = async (
77
- proof: string,
78
- request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
79
- db: SQL = sql,
80
- now = Date.now(),
81
- ): Promise<{ identity: BootstrapIdentity; jti: string; exp: number }> => {
82
- if (Buffer.byteLength(proof) > BOOTSTRAP_PROOF_BYTES) throw invalid();
103
+ /** Reject malformed or expired envelopes before allocating or reading a signed request body. Not authentication. */
104
+ export const inspectAppProof = (proof: string, now = Date.now()): { appId: string; keyId: string } => {
105
+ if (Buffer.byteLength(proof) > BOOTSTRAP_PROOF_BYTES || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{86}$/.test(proof))
106
+ throw invalid();
83
107
  let appId: string;
84
108
  let keyId: string;
85
109
  try {
@@ -87,14 +111,47 @@ export const authenticateBootstrap = async (
87
111
  const claims = decodeJwt(proof);
88
112
  if (header.alg !== "EdDSA" || header.typ !== BOOTSTRAP_TYPE || typeof header.kid !== "string" || typeof claims.iss !== "string")
89
113
  throw invalid();
114
+ validateAppKeyAppId(claims.iss);
115
+ if (!BootstrapIdentitySchema.shape.keyId.safeParse(header.kid).success) throw invalid();
116
+ const seconds = Math.floor(now / 1000);
117
+ if (
118
+ !Number.isSafeInteger(claims.iat) ||
119
+ !Number.isSafeInteger(claims.exp) ||
120
+ claims.iat! > seconds + 2 ||
121
+ claims.exp! <= seconds ||
122
+ claims.exp! <= claims.iat! ||
123
+ claims.exp! - claims.iat! > BOOTSTRAP_PROOF_SECONDS
124
+ )
125
+ throw invalid();
90
126
  appId = claims.iss;
91
127
  keyId = header.kid;
92
128
  } catch {
93
129
  throw invalid();
94
130
  }
131
+ return { appId, keyId };
132
+ };
133
+
134
+ /** No caches: every HTTP transmission rechecks revocation, expiry and account state in Postgres. */
135
+ export const authenticateAppProof = async (
136
+ proof: string,
137
+ request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
138
+ db: SQL = sql,
139
+ now = Date.now(),
140
+ ): Promise<{ identity: BootstrapIdentity; key: NonNullable<Awaited<ReturnType<typeof getActiveAppKey>>>; jti: string; exp: number }> => {
141
+ const { appId, keyId } = inspectAppProof(proof, now);
95
142
  // Only validated IDs reach SQL, and the app identity must own this active credential.
96
143
  const key = await getActiveAppKey({ appId, keyId }, db);
97
144
  if (!key) throw invalid();
145
+ return verifyAppProof(proof, request, key, db, now);
146
+ };
147
+
148
+ const verifyAppProof = async (
149
+ proof: string,
150
+ request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
151
+ key: NonNullable<Awaited<ReturnType<typeof getStoredAppKey>>>,
152
+ db: SQL,
153
+ now: number,
154
+ ) => {
98
155
  let claims: JWTPayload;
99
156
  try {
100
157
  const verified = await jwtVerify(proof, await importAppPublicKey(key.publicKey), {
@@ -107,19 +164,11 @@ export const authenticateBootstrap = async (
107
164
  } catch {
108
165
  throw invalid();
109
166
  }
110
- const seconds = Math.floor(now / 1000);
111
167
  if (
112
- !Number.isSafeInteger(claims.iat) ||
113
- !Number.isSafeInteger(claims.exp) ||
114
- claims.iat! > seconds + 2 ||
115
- claims.exp! <= claims.iat! ||
116
- claims.exp! - claims.iat! > BOOTSTRAP_PROOF_SECONDS ||
117
168
  typeof claims.jti !== "string" ||
118
169
  claims.jti.length === 0 ||
119
170
  claims.jti.length > 64 ||
120
171
  claims.jti.includes("\0") ||
121
- request.method !== "POST" ||
122
- request.path !== BOOTSTRAP_PATH ||
123
172
  claims.htm !== request.method ||
124
173
  claims.htu !== request.path ||
125
174
  claims.bdh !== (await bootstrapBodyDigest(request.body))
@@ -128,7 +177,41 @@ export const authenticateBootstrap = async (
128
177
  if (claims.iss !== key.appId || claims.sub !== key.appId) throw new BootstrapProofError("wrong_app");
129
178
  const installationId = await getInstallationId(db);
130
179
  if (claims.aud !== `cloud-core:${installationId}`) throw new BootstrapProofError("wrong_installation");
131
- if (!key.connectionsAllowed) throw new BootstrapProofError("connections_not_permitted");
132
180
  // Admission must succeed before the route atomically consumes this verified nonce.
133
- return { identity: { installationId, appId, keyId }, jti: claims.jti, exp: claims.exp! };
181
+ return { identity: { installationId, appId: key.appId, keyId: key.keyId }, key, jti: claims.jti, exp: claims.exp! };
182
+ };
183
+
184
+ /** Only connection delivery requires connections_allowed; runners can still sign requests. */
185
+ export const authenticateBootstrap = async (
186
+ proof: string,
187
+ request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
188
+ db: SQL = sql,
189
+ now = Date.now(),
190
+ ): Promise<{ identity: BootstrapIdentity; jti: string; exp: number }> => {
191
+ const ids = inspectAppProof(proof, now);
192
+ const key = await getStoredAppKey(ids, db);
193
+ if (!key) throw invalid();
194
+ const inactive = key.status !== "active" || key.accountStatus !== "active";
195
+ let verified: Awaited<ReturnType<typeof verifyAppProof>>;
196
+ try {
197
+ verified = await verifyAppProof(proof, request, key, db, now);
198
+ } catch (error) {
199
+ // Inactive keys have always returned invalid_proof, regardless of their claims.
200
+ if (inactive) throw invalid();
201
+ throw error;
202
+ }
203
+ if (request.method !== "POST" || request.path !== BOOTSTRAP_PATH) throw invalid();
204
+ const reason =
205
+ key.status === "revoked"
206
+ ? "revoked"
207
+ : key.status === "expired"
208
+ ? "expired"
209
+ : key.accountStatus === "disabled"
210
+ ? "disabled"
211
+ : !key.connectionsAllowed
212
+ ? "connections_not_permitted"
213
+ : null;
214
+ if (reason) throw new RejectedBootstrapStart(verified.identity, verified.jti, reason);
215
+ const { identity, jti, exp } = verified;
216
+ return { identity, jti, exp };
134
217
  };
@@ -1,4 +1,4 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
3
3
  import { getApp, listCapabilities } from "../_internal/registry";
4
4
  import {
@@ -9,6 +9,7 @@ import {
9
9
  } from "../contracts/app-details";
10
10
  import { type NotificationDefinitionPresentationCatalog, resolveNotificationDefinitionPresentation } from "../contracts/notification-types";
11
11
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
12
+ import { sql } from "../runtime/clients";
12
13
  import type { RequestAuthority } from "../server/middleware/auth";
13
14
  import { resolveAppIdentityPresentation } from "../shared/app-presentation";
14
15
  import { AppAccessManagementError, appAccessGrantCounts, canRepairAppAccess } from "./app-access-management";
@@ -48,8 +49,8 @@ type Dependencies = {
48
49
 
49
50
  /** Core-owned administration read shared by SSR and HTTP; app access is not administrator authority. */
50
51
  export function createAppDetailsService(dependencies: Dependencies = {}) {
51
- const db = dependencies.db ?? sql;
52
52
  return async (appId: string, options: { authority: RequestAuthority; locale?: string }): Promise<AppDetails | null> => {
53
+ const db = dependencies.db ?? sql;
53
54
  if (!canRepairAppAccess(options.authority))
54
55
  throw new AppAccessManagementError("FORBIDDEN", "Cloud administration authority is required.");
55
56
  const locale = options.locale ?? "en";
@@ -15,8 +15,8 @@ const Metadata = z.object({
15
15
  version: Envelope.shape.version.catch(1),
16
16
  });
17
17
 
18
- export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
19
- const markSeen = async (sequence: number, tx: SQL = db) => {
18
+ export function createEventAcceptance(db: () => SQL, reconcile: () => Promise<void>) {
19
+ const markSeen = async (sequence: number, tx: SQL = db()) => {
20
20
  // Keep holes until replay or the periodic DLQ/retention check accounts for them.
21
21
  // Advancing the high watermark alone would hide eviction before the next tick.
22
22
  await tx`
@@ -36,7 +36,7 @@ export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
36
36
  return;
37
37
  }
38
38
  const { id, type, source, version } = metadata.data;
39
- await db.begin(async (tx) => {
39
+ await db().begin(async (tx) => {
40
40
  // Invalid or oversized input is represented by metadata only, never a second failing payload insert.
41
41
  await tx`
42
42
  INSERT INTO events.app_events(id, type, version, source, subject_key, envelope, state, code)
@@ -61,7 +61,7 @@ export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
61
61
  } catch {
62
62
  log.warn("Declaration refresh failed; accepting against persisted declarations", { sequence });
63
63
  }
64
- const [declaration] = await db<StoredDeclaration[]>`
64
+ const [declaration] = await db()<StoredDeclaration[]>`
65
65
  SELECT * FROM events.app_declarations
66
66
  WHERE app_id = ${envelope.source} AND type = ${envelope.type} AND version = ${envelope.version}
67
67
  `;
@@ -85,7 +85,7 @@ export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
85
85
  }
86
86
  const subjectKey = `${envelope.subject.type}:${envelope.subject.id}`;
87
87
  try {
88
- await db.begin(async (tx) => {
88
+ await db().begin(async (tx) => {
89
89
  const inserted = await tx`
90
90
  INSERT INTO events.app_events(id, type, version, source, subject_key, envelope, state, code)
91
91
  VALUES (${envelope.id}::uuid, ${envelope.type}, ${envelope.version}, ${envelope.source}, ${subjectKey},
@@ -13,12 +13,12 @@ type Dependencies = {
13
13
  deadLetters: (after?: string) => Promise<TopicDeadLetter<AppEventEnvelope>[]>;
14
14
  streams: () => AsyncIterable<Pick<TopicInventoryEntry, "id" | "firstSequence">>;
15
15
  };
16
- export function createEventAdministration(db: SQL, dependencies: Dependencies) {
16
+ export function createEventAdministration(db: () => SQL, dependencies: Dependencies) {
17
17
  const catalog = async (locale = "en"): Promise<{ events: CapabilityEventCatalogEntry[] }> => {
18
18
  const [entries, declarations, storedSubscriptions] = await Promise.all([
19
19
  dependencies.registry(),
20
- db<StoredDeclaration[]>`SELECT * FROM events.app_declarations WHERE retired_at IS NULL ORDER BY type, version`,
21
- db<
20
+ db()<StoredDeclaration[]>`SELECT * FROM events.app_declarations WHERE retired_at IS NULL ORDER BY type, version`,
21
+ db()<
22
22
  {
23
23
  appId: string;
24
24
  subscriptionId: string;
@@ -62,7 +62,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
62
62
  };
63
63
  const subscriptions = async (app?: string) => {
64
64
  return {
65
- items: await db`
65
+ items: await db()`
66
66
  SELECT subscription.id, subscription.app_id AS "appId", subscription.event_type AS "eventType", subscription.version,
67
67
  subscription.state, subscription.reason, subscription.first_seen_at AS "firstSeenAt",
68
68
  (SELECT jsonb_object_agg(state, count) FROM (
@@ -77,7 +77,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
77
77
  const deliveries = async (filter: { app?: string; state?: string; eventType?: string; offset?: number; limit?: number } = {}) => {
78
78
  const limit = Math.max(1, Math.min(100, filter.limit ?? 50));
79
79
  const offset = Math.max(0, filter.offset ?? 0);
80
- const rows = await db`
80
+ const rows = await db()`
81
81
  SELECT delivery.id, delivery.event_id AS "eventId", delivery.subscription_id AS "subscriptionId",
82
82
  subscription.app_id AS "appId", event.type AS "eventType", delivery.state, delivery.code, delivery.attempts,
83
83
  delivery.last_error AS "lastError", delivery.last_attempt_at AS "lastAttemptAt",
@@ -104,7 +104,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
104
104
  };
105
105
  };
106
106
  const redeliver = async (id: string, actor: AuditActor) =>
107
- db.begin(async (tx) => {
107
+ db().begin(async (tx) => {
108
108
  const changed = await tx`
109
109
  UPDATE events.app_deliveries delivery SET state = 'pending', code = NULL, last_error = NULL,
110
110
  attempts = 0, unreachable_count = 0, retained_from = now(), next_attempt_at = now(), claimed_until = NULL, claim_token = NULL, updated_at = now()
@@ -129,12 +129,12 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
129
129
  });
130
130
 
131
131
  const status = async () => {
132
- const [stored] = await db`
132
+ const [stored] = await db()`
133
133
  SELECT state, code, last_sequence AS "lastSequence", gap_from AS "gapFrom", gap_to AS "gapTo", updated_at AS "updatedAt"
134
134
  FROM events.app_broker_status WHERE id = true
135
135
  `;
136
- const [rejected] = await db`SELECT count(*)::int AS count FROM events.app_events WHERE state = 'rejected'`;
137
- const rejections = await db`
136
+ const [rejected] = await db()`SELECT count(*)::int AS count FROM events.app_events WHERE state = 'rejected'`;
137
+ const rejections = await db()`
138
138
  SELECT type, source, code, received_at AS time FROM events.app_events
139
139
  WHERE state = 'rejected' ORDER BY seq DESC LIMIT 10
140
140
  `;
@@ -156,7 +156,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
156
156
  };
157
157
  };
158
158
  const checkGap = async () => {
159
- const [stored] = await db<{ last_sequence: string }[]>`
159
+ const [stored] = await db()<{ last_sequence: string }[]>`
160
160
  SELECT last_sequence FROM events.app_broker_status WHERE id = true
161
161
  `;
162
162
  let position = Number(stored?.last_sequence ?? 0);
@@ -175,7 +175,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
175
175
  after = next;
176
176
  }
177
177
  if (seen.size) {
178
- await db`
178
+ await db()`
179
179
  UPDATE events.app_broker_status SET unseen_sequences = unseen_sequences - (
180
180
  SELECT range_agg(int8range(sequence, sequence + 1, '[)'))
181
181
  FROM unnest(${toPgTextArray([...seen].map(String))}::bigint[]) AS sequence
@@ -183,13 +183,13 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
183
183
  `;
184
184
  }
185
185
  while (seen.has(position + 1)) position++;
186
- await db`
186
+ await db()`
187
187
  UPDATE events.app_broker_status SET last_sequence = ${position}, updated_at = now()
188
188
  WHERE id = true AND last_sequence < ${position}
189
189
  `;
190
190
  for await (const stream of dependencies.streams()) {
191
191
  if (stream.id !== "cloud:app-events") continue;
192
- await db`
192
+ await db()`
193
193
  UPDATE events.app_broker_status SET state = 'gap_detected', code = 'reconcile_required',
194
194
  gap_from = COALESCE(gap_from, LEAST(lower(unseen_sequences), last_sequence + 1)), gap_to = ${stream.firstSequence - 1}, updated_at = now()
195
195
  WHERE id = true AND LEAST(lower(unseen_sequences), last_sequence + 1) < ${stream.firstSequence}
@@ -199,7 +199,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
199
199
  }
200
200
  };
201
201
  const acknowledgeGap = async (actor: AuditActor) =>
202
- db.begin(async (tx) => {
202
+ db().begin(async (tx) => {
203
203
  const [previous] = await tx`
204
204
  SELECT state, gap_from, gap_to FROM events.app_broker_status WHERE id = true FOR UPDATE
205
205
  `;
@@ -223,7 +223,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
223
223
  return result;
224
224
  });
225
225
  const retain = async () =>
226
- db.begin(async (tx) => {
226
+ db().begin(async (tx) => {
227
227
  await tx`
228
228
  UPDATE events.app_deliveries SET state = 'cancelled', code = 'retention_expired',
229
229
  claimed_until = NULL, claim_token = NULL, updated_at = now()
@@ -34,13 +34,14 @@ async function auditTransitions(tx: SQL, transitions: Transition[]) {
34
34
  }
35
35
  }
36
36
 
37
- export function createDeclarationSync(db: SQL, registry: () => Promise<CapabilityRegistryEntry[]>) {
37
+ export function createDeclarationSync(db: () => SQL, registry: () => Promise<CapabilityRegistryEntry[]>) {
38
38
  let previousSnapshot: string | undefined;
39
+ let previousDatabase: SQL | undefined;
39
40
  let lastCheckedAt = -Infinity;
40
41
  let running: Promise<void> | undefined;
41
42
 
42
43
  async function sync(entries: CapabilityRegistryEntry[]) {
43
- await db.begin(async (tx) => {
44
+ await db().begin(async (tx) => {
44
45
  for (const entry of entries.toSorted((left, right) => left.appId.localeCompare(right.appId))) {
45
46
  const hash = capabilityHash({ events: entry.manifest.events ?? [], subscribes: entry.manifest.subscribes ?? [] });
46
47
  const [known] = await tx`SELECT hash FROM events.app_manifests WHERE app_id = ${entry.appId}`;
@@ -173,11 +174,13 @@ export function createDeclarationSync(db: SQL, registry: () => Promise<Capabilit
173
174
  }))
174
175
  .sort((left, right) => left.appId.localeCompare(right.appId)),
175
176
  );
176
- if (snapshotHash === previousSnapshot && performance.now() - lastCheckedAt < 30_000) return;
177
+ const database = db();
178
+ if (database === previousDatabase && snapshotHash === previousSnapshot && performance.now() - lastCheckedAt < 30_000) return;
177
179
  running = sync(entries);
178
180
  try {
179
181
  await running;
180
182
  previousSnapshot = snapshotHash;
183
+ previousDatabase = database;
181
184
  lastCheckedAt = performance.now();
182
185
  } finally {
183
186
  running = undefined;
@@ -40,14 +40,14 @@ async function auditFailure(tx: SQL, row: Delivery, code: string | null) {
40
40
  }
41
41
 
42
42
  export function createEventDelivery(
43
- db: SQL,
43
+ db: () => SQL,
44
44
  findApp: typeof getCapability,
45
45
  reconcileSubscriptions: () => Promise<void>,
46
46
  dependencies: Dependencies,
47
47
  ) {
48
48
  async function postpone(row: Delivery) {
49
49
  const delay = Math.min(30_000 * 2 ** Math.min(row.unreachable_count, 7), 3_600_000);
50
- await db`
50
+ await db()`
51
51
  UPDATE events.app_deliveries SET state = 'retrying', code = 'subscriber_unreachable', attempts = attempts - 1,
52
52
  unreachable_count = unreachable_count + 1, claimed_until = NULL, claim_token = NULL,
53
53
  next_attempt_at = now() + ${delay} * interval '1 millisecond', updated_at = now()
@@ -55,7 +55,7 @@ export function createEventDelivery(
55
55
  `;
56
56
  }
57
57
  const claim = async () =>
58
- db.begin(async (tx) => {
58
+ db().begin(async (tx) => {
59
59
  const exhausted = await tx<Delivery[]>`
60
60
  UPDATE events.app_deliveries SET state = 'failed', code = 'attempts_exhausted',
61
61
  claimed_until = NULL, claim_token = NULL, updated_at = now()
@@ -99,7 +99,7 @@ export function createEventDelivery(
99
99
  code: string | null,
100
100
  error: string | null = null,
101
101
  ) =>
102
- db.begin(async (tx) => {
102
+ db().begin(async (tx) => {
103
103
  const delay = APP_EVENT_DELIVERY_BACKOFF_MS[Math.min(row.attempts - 1, APP_EVENT_DELIVERY_BACKOFF_MS.length - 1)] ?? 0;
104
104
  const changed = await tx`
105
105
  UPDATE events.app_deliveries SET state = ${state}, code = ${code}, last_error = ${error},
@@ -122,7 +122,7 @@ export function createEventDelivery(
122
122
 
123
123
  const deliver = async (row: Delivery) => {
124
124
  try {
125
- const [subscription] = await db`
125
+ const [subscription] = await db()`
126
126
  SELECT state, reason FROM events.app_subscriptions WHERE id = ${row.subscription_id}
127
127
  `;
128
128
  if (subscription?.state === "cancelled") {
@@ -131,7 +131,7 @@ export function createEventDelivery(
131
131
  }
132
132
  if (subscription?.state !== "active") {
133
133
  // A declaration transition can block a subscription after this delivery was claimed.
134
- await db`
134
+ await db()`
135
135
  UPDATE events.app_deliveries SET state = 'retrying', code = ${subscription?.reason ?? "subscription_blocked"},
136
136
  attempts = attempts - 1, claimed_until = NULL, claim_token = NULL, next_attempt_at = now(), updated_at = now()
137
137
  WHERE id = ${row.id}::uuid AND claim_token = ${row.claim_token}::uuid AND state IN ('pending', 'retrying')
@@ -1,10 +1,11 @@
1
1
  import type { TopicDeadLetter, TopicInventoryEntry } from "@k2b/sync";
2
- import { type SQL, sql } from "bun";
2
+ import type { SQL } from "bun";
3
3
  import { lazySync } from "../_internal/process-sync";
4
4
  import { getCapability, listCapabilities } from "../_internal/registry";
5
5
  import type { AppEventEnvelope } from "../contracts/app-events";
6
6
  import type { CapabilityRegistryEntry } from "../contracts/registry";
7
7
  import { appEventTopic } from "../events/app-event-delivery";
8
+ import { sql } from "../runtime/clients";
8
9
  import { createEventAcceptance } from "./app-events/accept";
9
10
  import { createEventAdministration } from "./app-events/admin";
10
11
  import { createDeclarationSync } from "./app-events/declarations";
@@ -28,7 +29,7 @@ type BrokerDependencies = {
28
29
  const message = (error: unknown): string => (error instanceof Error ? error.message : String(error)).slice(0, 1000);
29
30
 
30
31
  export const createAppEventBroker = (dependencies: BrokerDependencies = {}) => {
31
- const db = dependencies.db ?? sql;
32
+ const db = () => dependencies.db ?? sql;
32
33
  const registry = dependencies.registry ?? listCapabilities;
33
34
  const declarations = createDeclarationSync(db, registry);
34
35
  const admin = createEventAdministration(db, {
@@ -1,7 +1,8 @@
1
1
  import { err, fail, ok, type Result } from "@k2b/stdlib";
2
- import { type SQL, sql } from "bun";
2
+ import type { SQL } from "bun";
3
3
  import { appKeyFingerprint, validateAppKeyAppId } from "../app-key";
4
4
  import type { User } from "../contracts/shared";
5
+ import { sql } from "../runtime/clients";
5
6
  import { audit } from "./audit";
6
7
  import { serviceAccounts } from "./service-accounts";
7
8
 
@@ -13,6 +14,7 @@ export type AppKeyMetadata = {
13
14
  kind: "app_key";
14
15
  name: string;
15
16
  publicKey: string;
17
+ grants: "oauth:issue"[];
16
18
  fingerprint: string;
17
19
  status: "active" | "expired" | "revoked";
18
20
  accountStatus: "active" | "disabled";
@@ -23,12 +25,18 @@ export type AppKeyMetadata = {
23
25
  revokedBy: string | null;
24
26
  lastUsedAt: string | null;
25
27
  };
28
+ export type LastRejectedStart = {
29
+ at: string;
30
+ reason: "revoked" | "expired" | "disabled" | "connections_not_permitted";
31
+ keyId: string | null;
32
+ };
26
33
  type Row = {
27
34
  id: string;
28
35
  app_id: string;
29
36
  service_account_id: string;
30
37
  name: string;
31
38
  public_key: string;
39
+ oauth_issue_allowed: boolean;
32
40
  fingerprint: string;
33
41
  key_status: AppKeyMetadata["status"];
34
42
  account_status: AppKeyMetadata["accountStatus"];
@@ -46,6 +54,7 @@ const map = (row: Row): AppKeyMetadata => ({
46
54
  kind: "app_key",
47
55
  name: row.name,
48
56
  publicKey: row.public_key,
57
+ grants: row.oauth_issue_allowed ? ["oauth:issue"] : [],
49
58
  fingerprint: row.fingerprint,
50
59
  status: row.key_status,
51
60
  accountStatus: row.account_status,
@@ -88,6 +97,7 @@ export const registerAppKey = async (params: {
88
97
  publicKey: string;
89
98
  name?: string;
90
99
  expiresAt?: string;
100
+ grants?: "oauth:issue"[];
91
101
  }): Promise<
92
102
  Result<{
93
103
  keyId: string;
@@ -99,6 +109,7 @@ export const registerAppKey = async (params: {
99
109
  > => {
100
110
  const valid = validateAdmin(params.actor, params.appId);
101
111
  if (!valid.ok) return valid;
112
+ if (params.grants?.some((grant) => grant !== "oauth:issue")) return fail(err.badInput("Unknown app key grant"));
102
113
  const name = params.name?.trim() ?? "App key";
103
114
  if (!name || name.length > 120) return fail(err.badInput("Key name must contain 1 to 120 characters"));
104
115
  let fingerprint: string;
@@ -159,10 +170,11 @@ export const registerAppKey = async (params: {
159
170
  `;
160
171
  if ((count?.active ?? 0) >= 2)
161
172
  return fail({ ...err.conflict("App key"), message: "An app can have at most two active keys; revoke an old key first" });
173
+ // Grants are explicit on every new enrollment, including rotation.
162
174
  const [row] = await db<{ id: string }[]>`
163
175
  INSERT INTO auth.service_account_credentials
164
- (service_account_id, name, kind, public_key, fingerprint, created_by, expires_at)
165
- VALUES (${accountId}::uuid, ${name}, 'app_key', ${params.publicKey}, ${fingerprint}, ${params.actor.id}::uuid, ${expiresAt})
176
+ (service_account_id, name, kind, public_key, fingerprint, created_by, expires_at, oauth_issue_allowed)
177
+ VALUES (${accountId}::uuid, ${name}, 'app_key', ${params.publicKey}, ${fingerprint}, ${params.actor.id}::uuid, ${expiresAt}, ${params.grants?.includes("oauth:issue") ?? false})
166
178
  RETURNING id
167
179
  `;
168
180
  if (!row) throw new Error("App key registration returned no key");
@@ -172,7 +184,7 @@ export const registerAppKey = async (params: {
172
184
  outcome: "allowed",
173
185
  actor: actorForAudit(params.actor),
174
186
  target: { type: "service_account_credential", id: row.id, label: name },
175
- metadata: { appId: params.appId, serviceAccountId: accountId, fingerprint },
187
+ metadata: { appId: params.appId, serviceAccountId: accountId, fingerprint, grants: params.grants ?? [] },
176
188
  },
177
189
  db,
178
190
  );
@@ -201,7 +213,21 @@ export const listAppKeys = async (params: { actor: AdminActor; appId: string; pa
201
213
  `;
202
214
  total = count?.total ?? 0;
203
215
  }
204
- return ok({ installationId: await getInstallationId(), items: rows.map(map), page, perPage, total, hasNext: page * perPage < total });
216
+ const [rejection] = await sql<{ rejected_at: Date; reason: LastRejectedStart["reason"]; key_id: string | null }[]>`
217
+ SELECT rejected_at, reason, key_id FROM auth.app_start_rejections WHERE app_id = ${params.appId}
218
+ `;
219
+ const lastRejectedStart: LastRejectedStart | null = rejection
220
+ ? { at: rejection.rejected_at.toISOString(), reason: rejection.reason, keyId: rejection.key_id }
221
+ : null;
222
+ return ok({
223
+ installationId: await getInstallationId(),
224
+ items: rows.map(map),
225
+ lastRejectedStart,
226
+ page,
227
+ perPage,
228
+ total,
229
+ hasNext: page * perPage < total,
230
+ });
205
231
  };
206
232
 
207
233
  export const listAppsWithKeys = async (params: { actor: AdminActor; page?: number; perPage?: number }) => {
@@ -268,11 +294,11 @@ export const revokeAppKey = async (params: { actor: AdminActor; appId: string; k
268
294
  });
269
295
  };
270
296
 
271
- /** Bootstrap and signed workload requests must query this directly; no cache. */
272
- export const getActiveAppKey = async (
297
+ /** Core bootstrap diagnostics need the stored public key even after revocation; this does not authenticate it. */
298
+ export const getStoredAppKey = async (
273
299
  params: { appId: string; keyId: string },
274
300
  db: SQL = sql,
275
- ): Promise<(AppKeyMetadata & { connectionsAllowed: boolean }) | null> => {
301
+ ): Promise<(AppKeyMetadata & { connectionsAllowed: boolean; oauthIssueAllowed: boolean }) | null> => {
276
302
  try {
277
303
  validateAppKeyAppId(params.appId);
278
304
  } catch {
@@ -283,11 +309,16 @@ export const getActiveAppKey = async (
283
309
  const [row] = await db<(Row & { connections_allowed: boolean })[]>`
284
310
  SELECT ${db.unsafe(columns)} FROM auth.service_account_credentials c
285
311
  JOIN auth.service_accounts sa ON sa.id = c.service_account_id
286
- WHERE c.kind = 'app_key' AND c.id = ${params.keyId}::uuid AND c.status = 'active'
287
- AND (c.expires_at IS NULL OR c.expires_at > now()) AND sa.status = 'active'
312
+ WHERE c.kind = 'app_key' AND c.id = ${params.keyId}::uuid
288
313
  AND sa.kind = 'resource_bound' AND sa.app_id = ${params.appId} AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${params.appId}
289
314
  `;
290
- return row ? { ...map(row), connectionsAllowed: row.connections_allowed } : null;
315
+ return row ? { ...map(row), connectionsAllowed: row.connections_allowed, oauthIssueAllowed: row.oauth_issue_allowed } : null;
316
+ };
317
+
318
+ /** Bootstrap and signed workload requests must query this directly; no cache. */
319
+ export const getActiveAppKey = async (params: { appId: string; keyId: string }, db: SQL = sql) => {
320
+ const key = await getStoredAppKey(params, db);
321
+ return key?.status === "active" && key.accountStatus === "active" ? key : null;
291
322
  };
292
323
 
293
324
  /** Call only after successful authentication; a late touch cannot revive a key. */
@@ -1,6 +1,6 @@
1
- import { sql } from "bun";
2
1
  import { parseStoredReviewContent } from "../../capabilities/review-storage";
3
2
  import { CAPABILITY_MAX_REVIEW_CONTENT_BYTES } from "../../contracts/capabilities";
3
+ import { sql } from "../../runtime/clients";
4
4
 
5
5
  // Start with this person's indexed pending rows; a Studio page alone cannot continue a chat.
6
6
  export const openRequests = (approverUserId: string, id?: string) => sql`SELECT r.* FROM approvals.requests r
@@ -1,9 +1,10 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { capabilityRequestHash } from "../../capabilities/claims";
3
3
  import { restoreReviewSnapshot, serializeReviewSnapshot } from "../../capabilities/review-storage";
4
4
  import { APPROVAL_ACP_CLIENT } from "../../contracts/approvals";
5
5
  import type { CapabilityActionReviewData } from "../../contracts/capabilities";
6
6
  import { CapabilityActionReviewPresentationSchema } from "../../contracts/capabilities";
7
+ import { sql } from "../../runtime/clients";
7
8
  import { isAppSessionActor } from "../../server/actor";
8
9
  import type { RequestActor, RequestCredentialKind } from "../../server/middleware/auth";
9
10
  import { audit } from "../audit";
@@ -1,10 +1,10 @@
1
- import { sql } from "bun";
2
1
  import { type AiStoredActionReview, storedActionReview } from "../../ai/action-review";
3
2
  import { AI_WEBSITE_APPROVAL_TOOL, aiTurnAllowsRememberedApprovals } from "../../ai/approvals";
4
3
  import type { AiTurnRunConfig } from "../../ai/types";
5
4
  import type { RuntimeAppMeta } from "../../contracts/app";
6
5
  import { APPROVALS_PAGE_SIZE, type ApprovalCounts, type PendingApproval } from "../../contracts/approvals";
7
6
  import { type CapabilityActionPolicy, CapabilityActionReviewPresentationSchema } from "../../contracts/capabilities";
7
+ import { sql } from "../../runtime/clients";
8
8
  import type { ApprovalAssurance } from ".";
9
9
  import { compactApprovalDetailsSql, openRequests } from "./content";
10
10
  import { decodeApprovalCursor, encodeApprovalCursor } from "./cursor";
@@ -1,4 +1,4 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../../runtime/clients";
2
2
 
3
3
  export const migrateApprovals = async (): Promise<void> => {
4
4
  await sql`CREATE SCHEMA IF NOT EXISTS approvals`.simple();
@@ -1,5 +1,6 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import type { UserProvider } from "../../contracts/shared";
3
+ import { sql } from "../../runtime/clients";
3
4
  import { err, fail, type PageParams, type Paginated, paginate, type Result, type ServiceError } from "../../server/services";
4
5
  import { logger } from "../logging";
5
6
  import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";