@k2b/cloud 0.47.0 → 0.49.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 (240) hide show
  1. package/package.json +3 -3
  2. package/src/_internal/app-access-manager-sql.ts +10 -0
  3. package/src/_internal/app-access-resources.ts +2 -1
  4. package/src/_internal/app-capabilities.ts +73 -0
  5. package/src/_internal/app-event-catalog.ts +1 -1
  6. package/src/_internal/app-queries.ts +1 -1
  7. package/src/_internal/application-package.ts +2 -1
  8. package/src/_internal/bounded-json.ts +7 -1
  9. package/src/_internal/capabilities.ts +25 -2
  10. package/src/_internal/define-app.ts +33 -2
  11. package/src/_internal/heartbeat.ts +2 -0
  12. package/src/_internal/nats-connection.ts +10 -9
  13. package/src/_internal/process-sync.ts +6 -6
  14. package/src/_internal/public-surface.ts +76 -0
  15. package/src/_internal/purpose-name.ts +7 -0
  16. package/src/_internal/registry-validation.ts +1 -0
  17. package/src/_internal/registry.ts +49 -3
  18. package/src/_internal/workload-credential.ts +2 -1
  19. package/src/ai/app-skill-store.ts +2 -1
  20. package/src/ai/app-skills.ts +2 -1
  21. package/src/ai/approvals.ts +2 -1
  22. package/src/ai/chat-tasks.ts +2 -1
  23. package/src/ai/credentials.ts +2 -1
  24. package/src/ai/dictation-runtime.ts +1 -1
  25. package/src/ai/dictations.ts +1 -1
  26. package/src/ai/files-store.ts +2 -1
  27. package/src/ai/inference-calls.ts +2 -1
  28. package/src/ai/map-store.ts +1 -1
  29. package/src/ai/map-tool.ts +1 -1
  30. package/src/ai/memories.ts +1 -1
  31. package/src/ai/memory-learning-runs.ts +1 -1
  32. package/src/ai/memory-learning.ts +1 -1
  33. package/src/ai/memory-workflow-evidence.ts +1 -1
  34. package/src/ai/message-queue.ts +2 -1
  35. package/src/ai/migrate.ts +1 -1
  36. package/src/ai/model-access-migrate.ts +1 -1
  37. package/src/ai/model-access.ts +1 -1
  38. package/src/ai/model-catalog.ts +1 -1
  39. package/src/ai/model-delete.ts +1 -1
  40. package/src/ai/model-usage.ts +2 -1
  41. package/src/ai/open-questions.ts +1 -1
  42. package/src/ai/openai-api-slots.ts +2 -1
  43. package/src/ai/prefs.ts +2 -1
  44. package/src/ai/projects.ts +2 -1
  45. package/src/ai/provider-connections.ts +1 -1
  46. package/src/ai/quota-limits.ts +2 -1
  47. package/src/ai/quota-provider.ts +2 -1
  48. package/src/ai/quota-report.ts +2 -1
  49. package/src/ai/quotas-migrate.ts +2 -1
  50. package/src/ai/quotas.ts +2 -1
  51. package/src/ai/request-headers.ts +2 -1
  52. package/src/ai/review-content.ts +2 -1
  53. package/src/ai/skill-search.ts +2 -1
  54. package/src/ai/skills.ts +2 -1
  55. package/src/ai/store.ts +2 -1
  56. package/src/ai/structured-runs.ts +2 -1
  57. package/src/ai/tool-audit.ts +2 -1
  58. package/src/ai/turn-timing.ts +1 -1
  59. package/src/ai/usage.ts +2 -1
  60. package/src/ai/web-provenance.ts +2 -1
  61. package/src/api/admin-ai-quotas.ts +1 -1
  62. package/src/api/admin-ai.ts +1 -1
  63. package/src/api/admin-app-domains.ts +62 -0
  64. package/src/api/admin-app-keys.ts +47 -3
  65. package/src/api/admin-core-settings.ts +1 -1
  66. package/src/api/admin-outgoing-mail.ts +1 -1
  67. package/src/api/app-bootstrap.ts +153 -0
  68. package/src/api/app-events.ts +1 -1
  69. package/src/api/auth.ts +1 -1
  70. package/src/api/authenticate-app-request.ts +129 -0
  71. package/src/api/capabilities.ts +87 -6
  72. package/src/api/index.ts +7 -0
  73. package/src/api/me.ts +1 -1
  74. package/src/api/search.bench.ts +1 -1
  75. package/src/app-pwa/service.ts +2 -1
  76. package/src/app-pwa/transaction.ts +4 -2
  77. package/src/capabilities/claims.ts +2 -1
  78. package/src/capabilities/executions.ts +8 -4
  79. package/src/capabilities/geo-client.ts +1 -1
  80. package/src/capabilities/migrate.ts +3 -1
  81. package/src/capabilities/resource-readers.ts +2 -2
  82. package/src/capabilities/response.ts +2 -1
  83. package/src/capabilities/server-invoke.ts +73 -20
  84. package/src/capabilities/server.ts +2 -1
  85. package/src/cli/admin/app-domains.ts +140 -0
  86. package/src/cli/admin/app-keys.ts +25 -3
  87. package/src/cli/admin/index.ts +107 -88
  88. package/src/config/env.ts +10 -4
  89. package/src/contracts/app-domains.ts +91 -0
  90. package/src/contracts/app-status.ts +104 -0
  91. package/src/contracts/capabilities.ts +37 -2
  92. package/src/contracts/index.ts +1 -0
  93. package/src/contracts/outgoing-mail.ts +1 -1
  94. package/src/contracts/registry.ts +2 -0
  95. package/src/events/app-event-delivery.ts +1 -1
  96. package/src/events/live-delivery.ts +2 -1
  97. package/src/index.ts +1 -0
  98. package/src/runtime/bootstrap-client.ts +195 -0
  99. package/src/runtime/bootstrap-protocol.ts +67 -0
  100. package/src/runtime/clients.ts +6 -2
  101. package/src/runtime/connections.ts +9 -0
  102. package/src/server/app-key.ts +14 -0
  103. package/src/server/index.ts +2 -0
  104. package/src/server/ratelimit.ts +1 -1
  105. package/src/server/services/access.ts +2 -1
  106. package/src/services/account-category-policy.ts +1 -1
  107. package/src/services/account-lifecycle/audit.ts +1 -1
  108. package/src/services/account-lifecycle/index.ts +1 -1
  109. package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
  110. package/src/services/accounts/app.ts +1 -1
  111. package/src/services/accounts/avatar.ts +1 -1
  112. package/src/services/accounts/base-group.ts +1 -1
  113. package/src/services/accounts/directory-shares.ts +1 -1
  114. package/src/services/accounts/duplicate-emails.ts +1 -1
  115. package/src/services/accounts/entities.ts +2 -1
  116. package/src/services/accounts/group-sql.ts +2 -1
  117. package/src/services/accounts/groups.ts +1 -1
  118. package/src/services/accounts/identities.ts +13 -11
  119. package/src/services/accounts/ipa-data.ts +1 -1
  120. package/src/services/accounts/local-groups.ts +2 -1
  121. package/src/services/accounts/machine-group-management.ts +2 -1
  122. package/src/services/accounts/posix.ts +13 -12
  123. package/src/services/accounts/request-policy.ts +2 -1
  124. package/src/services/accounts/switching.ts +1 -1
  125. package/src/services/accounts/users.ts +2 -1
  126. package/src/services/announcements/index.ts +1 -1
  127. package/src/services/app-access-management.ts +3 -9
  128. package/src/services/app-access.ts +2 -1
  129. package/src/services/app-admin-facts.ts +160 -0
  130. package/src/services/app-approval.ts +34 -30
  131. package/src/services/app-bootstrap.ts +217 -0
  132. package/src/services/app-details.ts +3 -2
  133. package/src/services/app-domain-check.ts +152 -0
  134. package/src/services/app-domains.ts +172 -0
  135. package/src/services/app-events/accept.ts +5 -5
  136. package/src/services/app-events/admin.ts +15 -15
  137. package/src/services/app-events/declarations.ts +6 -3
  138. package/src/services/app-events/deliver.ts +6 -6
  139. package/src/services/app-events.ts +3 -2
  140. package/src/services/app-keys.ts +45 -11
  141. package/src/services/approvals/content.ts +1 -1
  142. package/src/services/approvals/index.ts +2 -1
  143. package/src/services/approvals/list.ts +1 -1
  144. package/src/services/approvals/migrate.ts +1 -1
  145. package/src/services/audit/index.ts +2 -1
  146. package/src/services/auth-flows/ipa.ts +1 -1
  147. package/src/services/auth-flows/magic-link.ts +1 -1
  148. package/src/services/auth-flows/password-reset.ts +1 -1
  149. package/src/services/auth-flows/proxy-return.ts +1 -1
  150. package/src/services/downstream-budget.ts +2 -2
  151. package/src/services/geo/admin.ts +13 -12
  152. package/src/services/geo/identity.ts +46 -0
  153. package/src/services/geo/ledger.ts +118 -106
  154. package/src/services/geo/migrate.ts +2 -1
  155. package/src/services/geo/route-chain.ts +3 -3
  156. package/src/services/geo/service.ts +4 -24
  157. package/src/services/geoip/index.ts +5 -5
  158. package/src/services/geoip/store.ts +24 -21
  159. package/src/services/help/index.ts +2 -1
  160. package/src/services/help/store.ts +2 -1
  161. package/src/services/identity/app-request.ts +44 -0
  162. package/src/services/identity/constants.ts +1 -0
  163. package/src/services/identity/index.ts +1 -0
  164. package/src/services/identity/invocation-actor.ts +2 -1
  165. package/src/services/identity/invocation-token.ts +112 -0
  166. package/src/services/identity/key-ring.ts +2 -2
  167. package/src/services/identity/resource-readers.ts +1 -1
  168. package/src/services/identity/routes.ts +11 -3
  169. package/src/services/identity/runtime-config.ts +14 -3
  170. package/src/services/identity/workload-auth.ts +2 -0
  171. package/src/services/identity/workload-runtime.ts +3 -3
  172. package/src/services/index.ts +4 -0
  173. package/src/services/ipa/groups.ts +1 -1
  174. package/src/services/ipa/profile.ts +1 -1
  175. package/src/services/ipa/search.ts +1 -1
  176. package/src/services/ipa/sync.ts +1 -1
  177. package/src/services/ipa/users.ts +1 -1
  178. package/src/services/known-browsers.ts +2 -1
  179. package/src/services/legal-consent.ts +1 -1
  180. package/src/services/links/index.ts +16 -0
  181. package/src/services/links/runtime.ts +44 -0
  182. package/src/services/links/store.ts +105 -0
  183. package/src/services/logging/index.ts +13 -8
  184. package/src/services/logging/redaction.ts +6 -1
  185. package/src/services/logging/trace.ts +9 -9
  186. package/src/services/logging/web-vitals.ts +1 -1
  187. package/src/services/mandates/index.ts +2 -1
  188. package/src/services/notifications/batches.ts +1 -1
  189. package/src/services/notifications/browser.ts +1 -1
  190. package/src/services/notifications/catalog.ts +1 -1
  191. package/src/services/notifications/dispatcher.ts +1 -1
  192. package/src/services/notifications/index.ts +1 -1
  193. package/src/services/notifications/observability.ts +2 -1
  194. package/src/services/notifications/platform.ts +1 -1
  195. package/src/services/notifications/quiet.ts +1 -1
  196. package/src/services/notifications/user.ts +1 -1
  197. package/src/services/oauth-tokens.ts +1 -1
  198. package/src/services/outbox.ts +15 -14
  199. package/src/services/outgoing-mail/admin.ts +1 -1
  200. package/src/services/outgoing-mail/dispatcher.ts +1 -1
  201. package/src/services/outgoing-mail/drain.ts +2 -1
  202. package/src/services/outgoing-mail/enqueue.ts +2 -1
  203. package/src/services/outgoing-mail/messages.ts +2 -1
  204. package/src/services/outgoing-mail/retention.ts +1 -1
  205. package/src/services/outgoing-mail/send.ts +1 -1
  206. package/src/services/outgoing-mail/store.ts +2 -1
  207. package/src/services/panel-context-consent.ts +32 -28
  208. package/src/services/providers/local/auth.ts +1 -1
  209. package/src/services/providers/local/users.ts +1 -1
  210. package/src/services/pwa-devices.ts +2 -1
  211. package/src/services/rail-preferences.ts +16 -12
  212. package/src/services/rail-shortcuts.ts +12 -10
  213. package/src/services/request-cache-redis.ts +3 -3
  214. package/src/services/service-account-credentials.ts +2 -1
  215. package/src/services/service-accounts.ts +2 -1
  216. package/src/services/session/index.ts +2 -1
  217. package/src/services/session/user.ts +2 -1
  218. package/src/services/settings/core-settings.ts +20 -0
  219. package/src/services/settings/crypto.ts +2 -2
  220. package/src/services/settings/index.ts +1 -1
  221. package/src/services/settings/store.ts +6 -1
  222. package/src/services/settings/versions.ts +2 -1
  223. package/src/services/weather/forecast.ts +2 -1
  224. package/src/services/weather/locations.ts +1 -1
  225. package/src/services/weather/migrate.ts +2 -1
  226. package/src/services/weather/short-id.ts +2 -1
  227. package/src/services/webauthn.ts +1 -1
  228. package/src/ssr/admin-navigation.ts +0 -1
  229. package/src/ssr/compact-shell.browser-harness.ts +2 -0
  230. package/src/ssr/platform-messages.ts +0 -2
  231. package/src/start.ts +35 -14
  232. package/src/workflows/ai/store.ts +2 -1
  233. package/src/workflows/store/budget.ts +2 -1
  234. package/src/workflows/store/definitions.ts +2 -1
  235. package/src/workflows/store/events.ts +2 -1
  236. package/src/workflows/store/observability.ts +2 -1
  237. package/src/workflows/store/retention.ts +2 -1
  238. package/src/workflows/store/runs.ts +2 -1
  239. package/src/workflows/store/transaction.ts +2 -1
  240. package/src/workflows/store/worker.ts +2 -1
@@ -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,8 +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 (params: { appId: string; keyId: string }, db: SQL = sql): Promise<AppKeyMetadata | null> => {
297
+ /** Core bootstrap diagnostics need the stored public key even after revocation; this does not authenticate it. */
298
+ export const getStoredAppKey = async (
299
+ params: { appId: string; keyId: string },
300
+ db: SQL = sql,
301
+ ): Promise<(AppKeyMetadata & { connectionsAllowed: boolean; oauthIssueAllowed: boolean }) | null> => {
273
302
  try {
274
303
  validateAppKeyAppId(params.appId);
275
304
  } catch {
@@ -277,14 +306,19 @@ export const getActiveAppKey = async (params: { appId: string; keyId: string },
277
306
  }
278
307
  if (params.keyId.length !== 36 || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(params.keyId))
279
308
  return null;
280
- const [row] = await db<Row[]>`
309
+ const [row] = await db<(Row & { connections_allowed: boolean })[]>`
281
310
  SELECT ${db.unsafe(columns)} FROM auth.service_account_credentials c
282
311
  JOIN auth.service_accounts sa ON sa.id = c.service_account_id
283
- WHERE c.kind = 'app_key' AND c.id = ${params.keyId}::uuid AND c.status = 'active'
284
- 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
285
313
  AND sa.kind = 'resource_bound' AND sa.app_id = ${params.appId} AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${params.appId}
286
314
  `;
287
- return row ? map(row) : 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;
288
322
  };
289
323
 
290
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";
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { User } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
  import { isAccountCategoryAllowed } from "../account-category-policy";
4
4
  import { accounts } from "../accounts";
5
5
  import { logger } from "../logging";
@@ -1,6 +1,6 @@
1
- import { redis, sql } from "bun";
2
1
  import { accountCategory } from "../../contracts/account-categories";
3
2
  import type { User } from "../../contracts/shared";
3
+ import { redis, sql } from "../../runtime/clients";
4
4
  import { createAuthLoginUrl } from "../../shared/redirect";
5
5
  import { isAccountCategoryAllowed } from "../account-category-policy";
6
6
  import { accounts } from "../accounts";
@@ -1,5 +1,5 @@
1
- import { redis, sql } from "bun";
2
1
  import type { User } from "../../contracts/shared";
2
+ import { redis, sql } from "../../runtime/clients";
3
3
  import { createAuthPasswordResetUrl } from "../../shared/redirect";
4
4
  import { isAccountCategoryAllowed } from "../account-category-policy";
5
5
  import { getFreeIpaConfig } from "../freeipa-config";
@@ -1,4 +1,4 @@
1
- import { redis } from "bun";
1
+ import { redis } from "../../runtime/clients";
2
2
 
3
3
  const KEY_PREFIX = "auth:proxy-return:";
4
4
  const DEFAULT_TTL_SECONDS = 300;
@@ -1,4 +1,4 @@
1
- import { env } from "../config/env";
1
+ import { connections } from "../runtime/connections";
2
2
  import { logger } from "./logging";
3
3
  import { requestCacheRedis } from "./request-cache-redis";
4
4
 
@@ -31,7 +31,7 @@ export const createDownstreamBudget = (options: { client?: () => Promise<Client>
31
31
  async acquire(input: { downstream: Downstream; timeoutMs: number; signal: AbortSignal }): Promise<() => void> {
32
32
  input.signal.throwIfAborted();
33
33
  if (!Number.isFinite(input.timeoutMs) || input.timeoutMs <= 0) throw new RangeError("Downstream timeout must be positive and finite");
34
- const namespace = options.namespace?.() ?? env.SYNC_NAMESPACE;
34
+ const namespace = options.namespace?.() ?? connections().nats.namespace;
35
35
  if (!namespace.trim()) throw new Error("SYNC_NAMESPACE is required for shared downstream budgets");
36
36
  const key = `cloud:downstream:{${namespace}}:${input.downstream}`;
37
37
  const token = crypto.randomUUID();
@@ -1,4 +1,4 @@
1
- import { type SQL, sql } from "bun";
1
+ import type { SQL } from "bun";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  import {
4
4
  GEO_USAGE_GROUPS,
@@ -12,6 +12,7 @@ import {
12
12
  geoKeyProblem,
13
13
  validateGeoBudget,
14
14
  } from "../../contracts/geo-admin";
15
+ import { sql } from "../../runtime/clients";
15
16
  import { audit } from "../audit";
16
17
  import { toPgTextArray } from "../postgres";
17
18
  import * as settings from "../settings";
@@ -83,7 +84,7 @@ export const verifyGeoapifyKey =
83
84
  };
84
85
 
85
86
  export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () => Date } = {}) => {
86
- const db = options.db ?? sql;
87
+ const db = () => options.db ?? sql;
87
88
  const verify = options.verify ?? verifyGeoapifyKey();
88
89
  const now = options.now ?? (() => new Date());
89
90
 
@@ -92,7 +93,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
92
93
  * carries that transaction's exact timestamp, and so does every row that event wrote.
93
94
  */
94
95
  const changedBy = async (action: string, keys: readonly string[]): Promise<GeoAdminPerson | null> => {
95
- const [row] = await db<{ actor_user_id: string; name: string | null }[]>`
96
+ const [row] = await db()<{ actor_user_id: string; name: string | null }[]>`
96
97
  SELECT e.actor_user_id, COALESCE(NULLIF(u.display_name, ''), u.uid) AS name
97
98
  FROM audit.events e LEFT JOIN auth.users u ON u.id = e.actor_user_id
98
99
  WHERE e.action = ${action} AND e.actor_user_id IS NOT NULL
@@ -111,7 +112,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
111
112
  const [config, states, rows, keyBy, budgetBy] = await Promise.all([
112
113
  readGeoConfig(),
113
114
  settings.readStates([KEY, ...BUDGET_KEYS]),
114
- db<{ key: string; updated_at: Date }[]>`
115
+ db()<{ key: string; updated_at: Date }[]>`
115
116
  SELECT key, updated_at FROM settings.entries WHERE key = ANY(${toPgTextArray([KEY, ...BUDGET_KEYS])}::text[])`,
116
117
  changedBy(KEY_ACTION, [KEY]),
117
118
  changedBy(BUDGET_ACTION, BUDGET_KEYS),
@@ -147,15 +148,15 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
147
148
  const periods = geoPeriods(now(), config.timezone);
148
149
  const plan = config.plan === "custom" ? { dailyCredits: config.customDailyCredits } : GEO_PLANS[config.plan];
149
150
  const [[month], [today], shares, [overruns]] = await Promise.all([
150
- db<
151
+ db()<
151
152
  { used: number }[]
152
153
  >`SELECT used::float8 AS used FROM geo.usage WHERE scope = 'cloud' AND subject = '*' AND period = ${periods.cloud.period}`,
153
- db<{ used: number; at_limit: number }[]>`
154
- SELECT COALESCE(sum(used), 0)::float8 AS used, count(*) FILTER (WHERE used >= ${config.personDailyCredits * 1000})::int AS at_limit
154
+ db()<{ used: number; at_limit: number }[]>`
155
+ SELECT COALESCE(sum(used), 0)::float8 AS used, count(*) FILTER (WHERE used >= ${config.personDailyCredits * 1000} AND subject NOT LIKE 'app:%')::int AS at_limit
155
156
  FROM geo.usage WHERE scope = 'person' AND period = ${periods.person.period}`,
156
- db<{ function_group: string; used: number }[]>`
157
+ db()<{ function_group: string; used: number }[]>`
157
158
  SELECT function_group, used::float8 AS used FROM geo.shares WHERE scope = 'cloud' AND subject = '*' AND period = ${periods.cloud.period}`,
158
- db<{ count: number }[]>`SELECT count(*)::int AS count FROM audit.events
159
+ db()<{ count: number }[]>`SELECT count(*)::int AS count FROM audit.events
159
160
  WHERE action = 'core.geo.route_cost_overrun' AND metadata->>'period' = ${periods.cloud.period}`,
160
161
  ]);
161
162
  return {
@@ -193,7 +194,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
193
194
  await verify(apiKey, signal);
194
195
  signal?.throwIfAborted();
195
196
  const before = (await settings.readStates([KEY])).get(KEY)?.stored ?? "none";
196
- await db.begin(async (tx) => {
197
+ await db().begin(async (tx) => {
197
198
  // The person replaces whatever is stored, including a value this installation cannot read.
198
199
  await settings.set(KEY, apiKey, tx, { replaceUnreadable: true });
199
200
  await audit.record(
@@ -206,7 +207,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
206
207
  },
207
208
  async removeKey(actor: Actor): Promise<GeoAdminStatus> {
208
209
  requireAdmin(actor);
209
- await db.begin(async (tx) => {
210
+ await db().begin(async (tx) => {
210
211
  const [row] = await tx<{ key: string }[]>`DELETE FROM settings.entries WHERE key = ${KEY} RETURNING key`;
211
212
  if (row) await audit.record({ action: "core.geo.key_removed", outcome: "allowed", actor: auditActor(actor) }, tx);
212
213
  });
@@ -230,7 +231,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
230
231
  // resets their unreadable values to the defaults instead of leaving geo off behind a field nobody sees.
231
232
  const inactive = BUDGET_FIELDS.flatMap(([, name]) => (name in updates ? [] : [name]));
232
233
  const resets: string[] = [];
233
- await db.begin(async (tx) => {
234
+ await db().begin(async (tx) => {
234
235
  // Same lock and final-pair rule as every other writer of these settings.
235
236
  await assertGeoBudgetPair(tx, updates, []);
236
237
  // Saving the form is the explicit replacement of a value this installation cannot read.
@@ -0,0 +1,46 @@
1
+ import type { AppPwaActor } from "../../app-pwa/contracts";
2
+ import type { AccessSubject, CapabilityAppExecutionContext, RequestActor } from "../../contracts";
3
+ import { GeoError } from "./errors";
4
+
5
+ export type GeoContext = {
6
+ actor?: RequestActor | CapabilityAppExecutionContext["actor"];
7
+ accessSubject?: AccessSubject | { type: "none" };
8
+ locale?: string;
9
+ signal?: AbortSignal;
10
+ requestId?: string;
11
+ };
12
+ export type GeoBudgetSubject = AccessSubject | { type: "app"; appId: string };
13
+ export type AppPwaGeoContext = { actor: AppPwaActor; locale?: string; signal?: AbortSignal; requestId?: string };
14
+ export type GeoCallContext = GeoContext | AppPwaGeoContext;
15
+ export const geoIdentity = (
16
+ context: GeoContext,
17
+ ): {
18
+ actor: RequestActor | CapabilityAppExecutionContext["actor"];
19
+ accessSubject: GeoBudgetSubject;
20
+ } => {
21
+ const { actor, accessSubject } = context;
22
+ if (actor?.kind === "app" && actor.keyId && accessSubject?.type === "none")
23
+ return { actor, accessSubject: { type: "app", appId: actor.appId } };
24
+ if (
25
+ !actor ||
26
+ actor.kind === "app" ||
27
+ !accessSubject ||
28
+ accessSubject.type === "none" ||
29
+ !(actor.kind === "user" ? actor.user?.id : actor.serviceAccount?.id) ||
30
+ !(accessSubject.type === "user" ? accessSubject.userId : accessSubject.serviceAccountId)
31
+ )
32
+ throw new GeoError("GEO_FORBIDDEN");
33
+ return { actor, accessSubject };
34
+ };
35
+ export const geoCallIdentity = (
36
+ context: GeoCallContext,
37
+ ): {
38
+ actor: RequestActor | CapabilityAppExecutionContext["actor"] | AppPwaActor;
39
+ accessSubject: GeoBudgetSubject;
40
+ } => {
41
+ if (context.actor?.kind === "device" || context.actor?.kind === "visit") {
42
+ if (!context.actor.ownerUserId) throw new GeoError("GEO_FORBIDDEN");
43
+ return { actor: context.actor, accessSubject: { type: "user", userId: context.actor.ownerUserId } };
44
+ }
45
+ return geoIdentity({ actor: context.actor, accessSubject: "accessSubject" in context ? context.accessSubject : undefined });
46
+ };