@k2b/cloud 0.46.0 → 0.48.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 (104) hide show
  1. package/package.json +8 -4
  2. package/scripts/build.ts +5 -1
  3. package/scripts/preload.ts +5 -1
  4. package/src/_internal/app-access-manager-sql.ts +10 -0
  5. package/src/_internal/bounded-json.ts +7 -1
  6. package/src/_internal/capabilities.ts +8 -0
  7. package/src/_internal/define-app.ts +69 -2
  8. package/src/_internal/heartbeat.ts +21 -0
  9. package/src/_internal/nats-connection.ts +10 -9
  10. package/src/_internal/postgres-application-name.ts +11 -0
  11. package/src/_internal/process-sync.ts +15 -6
  12. package/src/_internal/public-surface.ts +76 -0
  13. package/src/_internal/purpose-name.ts +7 -0
  14. package/src/_internal/readiness.ts +6 -1
  15. package/src/_internal/registry-validation.ts +1 -0
  16. package/src/_internal/registry.ts +49 -3
  17. package/src/_internal/resource-principals.ts +4 -34
  18. package/src/_internal/workload-credential.ts +154 -0
  19. package/src/_internal/workload-protocol.ts +36 -0
  20. package/src/ai/application-tasks.ts +5 -2
  21. package/src/ai/chart-block.ts +36 -711
  22. package/src/ai/default-tool-contracts.ts +21 -259
  23. package/src/api/admin-app-domains.ts +62 -0
  24. package/src/api/admin-app-keys.ts +66 -0
  25. package/src/api/admin-core-settings.ts +2 -0
  26. package/src/api/admin-identity.ts +27 -1
  27. package/src/api/admin-settings-env.ts +120 -0
  28. package/src/api/app-bootstrap.ts +131 -0
  29. package/src/api/capabilities.ts +17 -4
  30. package/src/api/index.ts +8 -0
  31. package/src/api/settings-ops.ts +35 -0
  32. package/src/app-key.ts +125 -0
  33. package/src/browser/CloudResourceSearch.tsx +48 -41
  34. package/src/capabilities/discussion-providers.ts +3 -2
  35. package/src/capabilities/resource-readers.ts +13 -2
  36. package/src/capabilities/response.ts +6 -2
  37. package/src/capabilities/server-invoke.ts +73 -29
  38. package/src/capabilities/server.ts +1 -1
  39. package/src/capabilities/types.ts +1 -1
  40. package/src/cli/admin/app-credentials.ts +32 -7
  41. package/src/cli/admin/app-domains.ts +140 -0
  42. package/src/cli/admin/app-keys.ts +226 -0
  43. package/src/cli/admin/index.ts +109 -84
  44. package/src/cli/admin/settings.ts +62 -0
  45. package/src/config/define-env.ts +63 -8
  46. package/src/config/env.ts +34 -22
  47. package/src/config/index.ts +12 -1
  48. package/src/config/workflow-ai.ts +28 -0
  49. package/src/contracts/app-domains.ts +91 -0
  50. package/src/contracts/app-status.ts +104 -0
  51. package/src/contracts/capabilities.ts +22 -0
  52. package/src/contracts/index.ts +1 -0
  53. package/src/contracts/registry.ts +2 -0
  54. package/src/contracts/settings-env.ts +21 -0
  55. package/src/contracts/settings-types.ts +6 -0
  56. package/src/index.ts +10 -0
  57. package/src/runtime/bootstrap-client.ts +195 -0
  58. package/src/runtime/bootstrap-protocol.ts +60 -0
  59. package/src/runtime/clients.ts +72 -0
  60. package/src/runtime/connections.ts +46 -0
  61. package/src/runtime/resolve-connections.ts +91 -0
  62. package/src/server/app-key.ts +14 -0
  63. package/src/server/client-ip.ts +1 -1
  64. package/src/server/index.ts +1 -0
  65. package/src/services/app-access-management.ts +1 -8
  66. package/src/services/app-admin-facts.ts +160 -0
  67. package/src/services/app-bootstrap.ts +134 -0
  68. package/src/services/app-domain-check.ts +152 -0
  69. package/src/services/app-domains.ts +172 -0
  70. package/src/services/app-keys.ts +301 -0
  71. package/src/services/downstream-budget.ts +2 -2
  72. package/src/services/gateway.ts +6 -0
  73. package/src/services/identity/index.ts +3 -0
  74. package/src/services/identity/resource-readers.ts +80 -0
  75. package/src/services/identity/runtime-config.ts +14 -6
  76. package/src/services/identity/workload-runtime.ts +253 -0
  77. package/src/services/index.ts +8 -1
  78. package/src/services/links/index.ts +16 -0
  79. package/src/services/links/runtime.ts +44 -0
  80. package/src/services/links/store.ts +105 -0
  81. package/src/services/logging/index.ts +7 -5
  82. package/src/services/logging/redaction.ts +6 -1
  83. package/src/services/logging/trace.ts +8 -8
  84. package/src/services/outgoing-mail/resolve-host.ts +5 -4
  85. package/src/services/request-cache-redis.ts +2 -2
  86. package/src/services/service-account-credentials.ts +63 -21
  87. package/src/services/settings/api.ts +3 -3
  88. package/src/services/settings/binding.ts +45 -0
  89. package/src/services/settings/core-settings.ts +119 -1
  90. package/src/services/settings/crypto.ts +2 -2
  91. package/src/services/settings/defaults.ts +16 -2
  92. package/src/services/settings/env-status.ts +69 -0
  93. package/src/services/settings/index.ts +69 -26
  94. package/src/services/settings/store.ts +78 -8
  95. package/src/services/settings/validation.ts +40 -0
  96. package/src/shared/index.ts +11 -10
  97. package/src/shared/markdown/extensions/tables.ts +10 -1
  98. package/src/ssr/admin-navigation.ts +0 -1
  99. package/src/ssr/platform-messages.ts +0 -2
  100. package/src/start.ts +57 -0
  101. package/src/workflows/ai/index.ts +1 -0
  102. package/src/workflows/ai/runtime.ts +16 -8
  103. package/src/workflows/ai/settings.ts +5 -0
  104. package/src/shared/markdown/formula.ts +0 -1283
@@ -0,0 +1,253 @@
1
+ /** Core-only workload issuer. This module never reads identity signing keys, OAuth secrets, or sessions. */
2
+ import type { NatsConnection } from "@nats-io/transport-node";
3
+ import { sql } from "bun";
4
+ import { getProcessNats } from "../../_internal/process-sync";
5
+ import { APP_REGISTRY_TTL_MS, appInstancePrefix, appInstanceRegistry } from "../../_internal/registry";
6
+ import {
7
+ WORKLOAD_CHECK_INTERVAL_MS,
8
+ WORKLOAD_LIFETIME_MS,
9
+ WORKLOAD_ROTATE_BEFORE_MS,
10
+ type WorkloadRequest,
11
+ WorkloadRequestSchema,
12
+ type WorkloadResponse,
13
+ workloadSubject,
14
+ } from "../../_internal/workload-protocol";
15
+ import { connections } from "../../runtime/connections";
16
+ import { audit } from "../audit";
17
+ import { logger } from "../logging";
18
+ import { serviceAccountCredentials } from "../service-account-credentials";
19
+ import { serviceAccounts } from "../service-accounts";
20
+
21
+ const isRegistered = async (appId: string, instanceId: string): Promise<boolean> => {
22
+ const key = `${appInstancePrefix(appId)}${instanceId}`;
23
+ const snapshot = await appInstanceRegistry().snapshot({ prefix: key });
24
+ return snapshot.entries.some((entry) => entry.key === key && entry.value.appId === appId && entry.value.instanceId === instanceId);
25
+ };
26
+
27
+ export const issueAppWorkloadCredential = async (
28
+ request: WorkloadRequest,
29
+ registered: (appId: string, instanceId: string) => Promise<boolean> = isRegistered,
30
+ ): Promise<WorkloadResponse> => {
31
+ if (!(await registered(request.appId, request.instanceId))) return { status: "unregistered" };
32
+ const account = await serviceAccounts.getOrCreateResourceBound({
33
+ name: `${request.appId} workload`,
34
+ appId: request.appId,
35
+ resourceType: "cloud.app",
36
+ resourceId: request.appId,
37
+ });
38
+ if (!account.ok) return { status: "unavailable" };
39
+ return sql.begin(async (db) => {
40
+ // One account lock serializes issuance/rotation/removal across Core replicas. Admin revocation
41
+ // locks the credential row too, so it cannot be overwritten by an in-flight refresh.
42
+ const [owner] = await db<{ status: string }[]>`SELECT status FROM auth.service_accounts WHERE id = ${account.data.id}::uuid FOR UPDATE`;
43
+ if (owner?.status !== "active") return { status: "revoked" };
44
+ const [current] = await db<{ id: string; status: string; expires_at: Date; previous_credential_id: string | null }[]>`
45
+ SELECT c.id, c.status, c.expires_at, i.previous_credential_id FROM auth.app_workload_instances i
46
+ JOIN auth.service_account_credentials c ON c.id = i.credential_id
47
+ WHERE i.app_id = ${request.appId} AND i.instance_id = ${request.instanceId}
48
+ FOR UPDATE OF c
49
+ `;
50
+ if (request.operation === "release") {
51
+ if (current?.status === "active") {
52
+ for (const credentialId of [current.id, current.previous_credential_id]) {
53
+ if (!credentialId) continue;
54
+ const revoked = await serviceAccountCredentials.revoke(
55
+ { credentialId, system: { service: "app-workload", reason: "instance stopped" } },
56
+ { db },
57
+ );
58
+ if (!revoked.ok) throw new Error("Stopped workload credential could not be revoked");
59
+ }
60
+ await db`DELETE FROM auth.app_workload_instances WHERE app_id = ${request.appId} AND instance_id = ${request.instanceId}`;
61
+ await db`DELETE FROM auth.service_account_credentials WHERE id = ${current.id}::uuid OR id = ${current.previous_credential_id}::uuid`;
62
+ }
63
+ return { status: "revoked" };
64
+ }
65
+ if (!(await registered(request.appId, request.instanceId))) return { status: "unregistered" };
66
+ await db`UPDATE auth.app_workload_instances SET last_refreshed_at = now()
67
+ WHERE app_id = ${request.appId} AND instance_id = ${request.instanceId}`;
68
+ if (
69
+ current?.status === "active" &&
70
+ current.id === request.credentialId &&
71
+ current.expires_at.getTime() - Date.now() > WORKLOAD_ROTATE_BEFORE_MS
72
+ ) {
73
+ return { status: "current", credentialId: current.id };
74
+ }
75
+ const expiresAt = new Date(Date.now() + WORKLOAD_LIFETIME_MS).toISOString();
76
+ const issued = await serviceAccountCredentials.createApiToken(
77
+ {
78
+ serviceAccountId: account.data.id,
79
+ name: `Runtime ${request.instanceId}`,
80
+ scopes: ["identity:invoke"],
81
+ expiresAt,
82
+ },
83
+ { db },
84
+ );
85
+ if (!issued.ok) return { status: "unavailable" };
86
+ // An acknowledged current token overlaps its successor until expiry. When a reply was
87
+ // lost, replace only the undelivered generation and keep the token the caller still knows.
88
+ const retainedId = current
89
+ ? request.credentialId === current.id
90
+ ? current.id
91
+ : current.previous_credential_id === request.credentialId
92
+ ? current.previous_credential_id
93
+ : null
94
+ : null;
95
+ for (const retiredId of [current?.id, current?.previous_credential_id]) {
96
+ if (!retiredId || retiredId === retainedId) continue;
97
+ const revoked = await serviceAccountCredentials.revoke(
98
+ { credentialId: retiredId, system: { service: "app-workload", reason: "rotation" } },
99
+ { db },
100
+ );
101
+ if (!revoked.ok) throw new Error("Workload rotation could not retire previous credential");
102
+ }
103
+ await db`
104
+ INSERT INTO auth.app_workload_instances (app_id, instance_id, credential_id, previous_credential_id)
105
+ VALUES (${request.appId}, ${request.instanceId}, ${issued.data.credential.id}::uuid, ${retainedId}::uuid)
106
+ ON CONFLICT (app_id, instance_id) DO UPDATE
107
+ SET credential_id = EXCLUDED.credential_id, previous_credential_id = EXCLUDED.previous_credential_id, last_refreshed_at = now()
108
+ `;
109
+ for (const retiredId of [current?.id, current?.previous_credential_id]) {
110
+ if (retiredId && retiredId !== retainedId) await db`DELETE FROM auth.service_account_credentials WHERE id = ${retiredId}::uuid`;
111
+ }
112
+ await audit.record(
113
+ {
114
+ action: "service_account_credential.create",
115
+ outcome: "allowed",
116
+ target: { type: "service_account_credential", id: issued.data.credential.id, label: issued.data.credential.name },
117
+ metadata: {
118
+ provenance: "system",
119
+ service: "app-workload",
120
+ appId: request.appId,
121
+ instanceId: request.instanceId,
122
+ serviceAccountId: account.data.id,
123
+ },
124
+ },
125
+ db,
126
+ );
127
+ // Deliberately construct the response: only a newly minted app-bound token and its coordinates.
128
+ return {
129
+ status: "issued",
130
+ appId: request.appId,
131
+ instanceId: request.instanceId,
132
+ credentialId: issued.data.credential.id,
133
+ token: issued.data.token,
134
+ expiresAt,
135
+ };
136
+ });
137
+ };
138
+
139
+ /** Revoke disappeared instances, including TTL expiry after an ungraceful stop. Pages bound memory. */
140
+ export const revokeRemovedAppWorkloads = async (
141
+ registered: (appId: string, instanceId: string) => Promise<boolean> = isRegistered,
142
+ ): Promise<void> => {
143
+ let afterApp = "";
144
+ let afterInstance = "";
145
+ for (;;) {
146
+ // Same page size as the credential administration API; never materialize credential history.
147
+ const rows = await sql<{ app_id: string; instance_id: string; credential_id: string; service_account_id: string }[]>`
148
+ SELECT i.app_id, i.instance_id, i.credential_id, c.service_account_id
149
+ FROM auth.app_workload_instances i JOIN auth.service_account_credentials c ON c.id = i.credential_id
150
+ WHERE c.status = 'active' AND i.last_refreshed_at < ${new Date(Date.now() - APP_REGISTRY_TTL_MS)} AND (i.app_id, i.instance_id) > (${afterApp}, ${afterInstance})
151
+ ORDER BY i.app_id, i.instance_id LIMIT 100
152
+ `;
153
+ if (!rows.length) return;
154
+ for (const row of rows) {
155
+ afterApp = row.app_id;
156
+ afterInstance = row.instance_id;
157
+ if (await registered(row.app_id, row.instance_id)) continue;
158
+ await sql.begin(async (db) => {
159
+ await db`SELECT id FROM auth.service_accounts WHERE id = ${row.service_account_id}::uuid FOR UPDATE`;
160
+ if (await registered(row.app_id, row.instance_id)) return;
161
+ // Read again under the account lock, in case another Core replica rotated it meanwhile.
162
+ const [current] = await db<
163
+ { credential_id: string; previous_credential_id: string | null; status: string; last_refreshed_at: Date }[]
164
+ >`
165
+ SELECT i.credential_id, i.previous_credential_id, c.status, i.last_refreshed_at FROM auth.app_workload_instances i
166
+ JOIN auth.service_account_credentials c ON c.id = i.credential_id
167
+ WHERE i.app_id = ${row.app_id} AND i.instance_id = ${row.instance_id}
168
+ FOR UPDATE OF c
169
+ `;
170
+ if (current?.status === "active" && current.last_refreshed_at.getTime() < Date.now() - APP_REGISTRY_TTL_MS) {
171
+ for (const credentialId of [current.credential_id, current.previous_credential_id]) {
172
+ if (!credentialId) continue;
173
+ const revoked = await serviceAccountCredentials.revoke(
174
+ { credentialId, system: { service: "app-workload", reason: "instance removed" } },
175
+ { db },
176
+ );
177
+ if (!revoked.ok) throw new Error("Removed workload credential could not be revoked");
178
+ }
179
+ // An automatically retired instance may register again after a registry reset. An
180
+ // admin-revoked credential keeps its mapping and disabled account until explicit recovery.
181
+ await db`DELETE FROM auth.app_workload_instances WHERE app_id = ${row.app_id} AND instance_id = ${row.instance_id}`;
182
+ await db`DELETE FROM auth.service_account_credentials WHERE id = ${current.credential_id}::uuid OR id = ${current.previous_credential_id}::uuid`;
183
+ }
184
+ });
185
+ }
186
+ }
187
+ };
188
+
189
+ export const startAppWorkloadBroker = async (
190
+ options: {
191
+ connection?: NatsConnection;
192
+ namespace?: string;
193
+ issue?: (request: WorkloadRequest) => Promise<WorkloadResponse>;
194
+ reconcile?: () => Promise<void>;
195
+ } = {},
196
+ ): Promise<() => Promise<void>> => {
197
+ const connection = options.connection ?? getProcessNats();
198
+ const issue = options.issue ?? issueAppWorkloadCredential;
199
+ const reconcile = options.reconcile ?? revokeRemovedAppWorkloads;
200
+ const log = logger("app-workload");
201
+ // Issuance gets four concurrent DB transactions per Core replica, reserving the rest of
202
+ // Bun SQL's default ten-connection pool for identity requests. Overload retries at the caller.
203
+ const inFlight = new Set<Promise<void>>();
204
+ let stopped = false;
205
+ let maintenance: Promise<void> | undefined;
206
+ let timer: Timer | undefined;
207
+ const subscription = connection.subscribe(workloadSubject(options.namespace ?? connections().nats.namespace), {
208
+ queue: "core-workload-v1",
209
+ callback: (error, message) => {
210
+ if (error || !message.reply) return;
211
+ // App id, instance id and UUID fit in 1 KiB including JSON escaping. Reject larger input before parsing.
212
+ if (stopped || inFlight.size >= 4 || message.data.length > 1024) {
213
+ message.respond(JSON.stringify({ status: "unavailable" }));
214
+ return;
215
+ }
216
+ const operation = (async () => {
217
+ let response: WorkloadResponse = { status: "unavailable" };
218
+ try {
219
+ const parsed = WorkloadRequestSchema.safeParse(message.json<unknown>());
220
+ if (parsed.success) response = await issue(parsed.data);
221
+ } catch {
222
+ log.error("Workload credential issuance failed; caller will retry");
223
+ }
224
+ if (!stopped) message.respond(JSON.stringify(response));
225
+ })();
226
+ inFlight.add(operation);
227
+ void operation.finally(() => inFlight.delete(operation));
228
+ },
229
+ });
230
+ const maintain = () => {
231
+ maintenance = reconcile()
232
+ .catch(() => {
233
+ log.error("Workload removal reconciliation failed; retrying");
234
+ })
235
+ .finally(() => {
236
+ if (!stopped) timer = setTimeout(maintain, WORKLOAD_CHECK_INTERVAL_MS);
237
+ });
238
+ };
239
+ const stop = async () => {
240
+ stopped = true;
241
+ clearTimeout(timer);
242
+ subscription.unsubscribe();
243
+ await Promise.allSettled([...inFlight, ...(maintenance ? [maintenance] : [])]);
244
+ };
245
+ maintain();
246
+ try {
247
+ await connection.flush();
248
+ } catch (error) {
249
+ await stop();
250
+ throw error;
251
+ }
252
+ return stop;
253
+ };
@@ -8,6 +8,7 @@ export type { PosixCandidate } from "./accounts/posix";
8
8
  export { accountIdentities, AccountIdentityError } from "./accounts/identities";
9
9
  export type { AccountIdentityUser, AccountIdentityGroup, AccountIdentityPage, AccountIdentityAvailability } from "./accounts/identities";
10
10
  export type { AccountIdentityReconciliation } from "./accounts/identity-reconciliation";
11
+ export { getAppWorkloadCredential } from "../_internal/workload-credential";
11
12
  export { providers } from "./providers";
12
13
  export { createAllowedNetworkFetch } from "./allowed-network-fetch";
13
14
  export { requestPublicHttps, type PublicHttpInput } from "./public-http";
@@ -252,6 +253,7 @@ export type { NetworkLookup, PublicNetworkAddress } from "./network-security";
252
253
 
253
254
  export { geoip } from "./geoip";
254
255
  export { appDetails } from "./app-details";
256
+ export { appAdminFacts, type AppAdminFacts } from "./app-admin-facts";
255
257
  export {
256
258
  knownBrowsers,
257
259
  KNOWN_BROWSER_LIMITS,
@@ -281,7 +283,9 @@ export {
281
283
  type AppAccessSnapshot,
282
284
  } from "./app-access-management";
283
285
 
284
- export { storage } from "./object-store";
286
+ import { storage as platformStorage } from "./object-store";
287
+ /** @deprecated Import storage from @k2b/cloud. */
288
+ export const storage = platformStorage;
285
289
  export type {
286
290
  ObjectStoreFile,
287
291
  ObjectVisibility,
@@ -291,3 +295,6 @@ export type {
291
295
  ObjectListOptions,
292
296
  ObjectListResult,
293
297
  } from "./object-store";
298
+
299
+ export { links } from "./links";
300
+ export type { CreateLinkInput, CreatedLink, RedeemedLink, LinkOptions, PeekedLink, PeekLinkResult, RedeemLinkResult } from "./links";
@@ -0,0 +1,16 @@
1
+ import { getProcessApplicationPackage } from "../../_internal/process-identity";
2
+ import { sql } from "../../runtime/clients";
3
+ import { type CreateLinkInput, createLinksStore, type LinkOptions } from "./store";
4
+
5
+ const store = () => {
6
+ const packageName = getProcessApplicationPackage();
7
+ if (!packageName) throw new Error("Start the application before using one-time links");
8
+ return createLinksStore(sql, packageName);
9
+ };
10
+
11
+ export const links = {
12
+ create: (input: CreateLinkInput, options?: LinkOptions) => store().create(input, options),
13
+ peek: (token: string, input: { purpose: string }, options?: LinkOptions) => store().peek(token, input, options),
14
+ redeem: (token: string, input: { purpose: string }, options?: LinkOptions) => store().redeem(token, input, options),
15
+ };
16
+ export type { CreatedLink, CreateLinkInput, LinkOptions, PeekedLink, PeekLinkResult, RedeemedLink, RedeemLinkResult } from "./store";
@@ -0,0 +1,44 @@
1
+ import { lazySync } from "../../_internal/process-sync";
2
+ import { sql } from "../../runtime/clients";
3
+ import { logger } from "../logging";
4
+ import { cleanupLinksBatch, LINK_CLEANUP_BATCH, setupLinks } from "./store";
5
+
6
+ // All apps compete for one platform schedule, including replicas and apps sharing a package.
7
+ const scheduler = lazySync((sync) =>
8
+ sync.scheduler({
9
+ id: "cloud-links-retention",
10
+ owner: "platform",
11
+ delivery: { maxAttempts: 2, backoffMs: [30_000], ackWaitMs: 60_000 },
12
+ }),
13
+ );
14
+
15
+ /** Provision before app setup and readiness, then drain this worker before NATS during shutdown. */
16
+ export const startLinksRuntime = async (): Promise<() => Promise<void>> => {
17
+ await setupLinks(sql);
18
+ const stopping = new AbortController();
19
+ await scheduler().create({
20
+ id: "retention",
21
+ cron: "0 * * * *",
22
+ timezone: "UTC",
23
+ misfire: "latest",
24
+ meta: { family: "links", label: "One-time link cleanup" },
25
+ process: async ({ signal, heartbeat }) => {
26
+ const runSignal = AbortSignal.any([signal, stopping.signal]);
27
+ const deadline = performance.now() + 5 * 60_000;
28
+ let deleted = 0;
29
+ while (!runSignal.aborted && performance.now() < deadline) {
30
+ const count = await cleanupLinksBatch(sql);
31
+ deleted += count;
32
+ if (count < LINK_CLEANUP_BATCH) break;
33
+ await heartbeat();
34
+ }
35
+ if (deleted > 0) logger("links:cleanup").info("Removed old one-time links", { deleted });
36
+ },
37
+ });
38
+ const worker = await scheduler().process({ concurrency: 1 });
39
+ return async () => {
40
+ stopping.abort();
41
+ worker.stop();
42
+ await worker.drain({ timeoutMs: 30_000 });
43
+ };
44
+ };
@@ -0,0 +1,105 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import type { SQL, TransactionSQL } from "bun";
3
+ import { isPurposeName, validatePurposeName } from "../../_internal/purpose-name";
4
+ import { ensureSchema } from "../postgres";
5
+
6
+ export type CreateLinkInput = { purpose: string; subject: string; ttl: number };
7
+ export type CreatedLink = { token: string; expiresAt: Date };
8
+ export type RedeemedLink = { subject: string; createdAt: Date };
9
+ export type PeekedLink = RedeemedLink & { expiresAt: Date };
10
+ export type LinkOptions = { tx?: TransactionSQL };
11
+ type LinkResult<T> = { ok: true; data: T } | { ok: false; error: { code: "LINK_INVALID" } };
12
+ export type RedeemLinkResult = LinkResult<RedeemedLink>;
13
+ export type PeekLinkResult = LinkResult<PeekedLink>;
14
+
15
+ const TOKEN = /^cl_[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
16
+ // Match the existing outgoing-mail retention batch budget.
17
+ export const LINK_CLEANUP_BATCH = 1000;
18
+ const hashToken = (token: string): string => createHash("sha256").update(token).digest("hex");
19
+
20
+ const linkCandidate = (token: string, purpose: string) => {
21
+ const value = typeof token === "string" ? token : "";
22
+ const digest = hashToken(value);
23
+ const validPurpose = isPurposeName(purpose);
24
+ return { digest, purpose: validPurpose ? purpose : "", valid: TOKEN.test(value) && validPurpose };
25
+ };
26
+
27
+ export const validateCreateLink = (input: CreateLinkInput): void => {
28
+ validatePurposeName(input.purpose);
29
+ if (typeof input.subject !== "string" || Buffer.byteLength(input.subject, "utf8") > 1024) {
30
+ throw new TypeError("Link subject must be a string of at most 1024 UTF-8 bytes");
31
+ }
32
+ if (!Number.isFinite(input.ttl) || input.ttl < 60 || input.ttl > 30 * 24 * 60 * 60) {
33
+ throw new RangeError("Link ttl must be seconds from 60 to 2592000 (30 days)");
34
+ }
35
+ };
36
+
37
+ /** One reserved platform table in the same database as the application's domain transactions. */
38
+ export const setupLinks = async (db: SQL): Promise<void> => {
39
+ await db.begin(async (tx) => {
40
+ await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.links'))`;
41
+ await ensureSchema(tx, "platform");
42
+ const [existing] = await tx<{ table_exists: boolean; index_exists: boolean }[]>`
43
+ SELECT to_regclass('platform._cloud_links') IS NOT NULL AS table_exists,
44
+ to_regclass('platform._cloud_links_retention') IS NOT NULL AS index_exists`;
45
+ if (!existing?.table_exists)
46
+ await tx`CREATE TABLE IF NOT EXISTS platform._cloud_links (
47
+ package text NOT NULL, token_hash text NOT NULL, purpose text NOT NULL, subject bytea NOT NULL,
48
+ created_at timestamptz NOT NULL, expires_at timestamptz NOT NULL, used_at timestamptz,
49
+ PRIMARY KEY (package, token_hash)
50
+ )`.simple();
51
+ // Even IF NOT EXISTS takes a ShareLock; skip index DDL on ordinary app restarts.
52
+ if (!existing?.index_exists)
53
+ await tx`CREATE INDEX IF NOT EXISTS _cloud_links_retention ON platform._cloud_links (LEAST(expires_at, used_at))`.simple();
54
+ });
55
+ };
56
+
57
+ export const createLinksStore = (db: SQL, packageName: string) => ({
58
+ create: async (input: CreateLinkInput, options: LinkOptions = {}): Promise<CreatedLink> => {
59
+ validateCreateLink(input);
60
+ const token = `cl_${randomBytes(32).toString("base64url")}`;
61
+ const [row] = await (options.tx ?? db)<{ expires_at: Date }[]>`
62
+ INSERT INTO platform._cloud_links (package, token_hash, purpose, subject, created_at, expires_at)
63
+ VALUES (${packageName}, ${hashToken(token)}, ${input.purpose}, ${Buffer.from(input.subject, "utf8")},
64
+ statement_timestamp(), statement_timestamp() + ${input.ttl} * interval '1 second')
65
+ RETURNING expires_at`;
66
+ if (!row) throw new Error("One-time link insert returned no expiry");
67
+ return { token, expiresAt: row.expires_at };
68
+ },
69
+ peek: async (token: string, input: { purpose: string }, options: LinkOptions = {}): Promise<PeekLinkResult> => {
70
+ const candidate = linkCandidate(token, input.purpose);
71
+ const [row] = await (options.tx ?? db)<{ subject: Uint8Array; created_at: Date; expires_at: Date }[]>`
72
+ SELECT subject, created_at, expires_at FROM platform._cloud_links
73
+ WHERE package = ${packageName} AND token_hash = ${candidate.digest} AND purpose = ${candidate.purpose}
74
+ AND ${candidate.valid}::boolean AND used_at IS NULL AND expires_at > clock_timestamp()`;
75
+ return row
76
+ ? { ok: true, data: { subject: Buffer.from(row.subject).toString("utf8"), createdAt: row.created_at, expiresAt: row.expires_at } }
77
+ : { ok: false, error: { code: "LINK_INVALID" } };
78
+ },
79
+ redeem: async (token: string, input: { purpose: string }, options: LinkOptions = {}): Promise<RedeemLinkResult> => {
80
+ // Every candidate is hashed and reaches one indexed UPDATE, including malformed credentials.
81
+ const candidate = linkCandidate(token, input.purpose);
82
+ const [row] = await (options.tx ?? db)<{ subject: Uint8Array; created_at: Date }[]>`
83
+ UPDATE platform._cloud_links SET used_at = clock_timestamp()
84
+ WHERE package = ${packageName} AND token_hash = ${candidate.digest} AND purpose = ${candidate.purpose}
85
+ AND ${candidate.valid}::boolean AND used_at IS NULL AND expires_at > clock_timestamp()
86
+ RETURNING subject, created_at`;
87
+ return row
88
+ ? { ok: true, data: { subject: Buffer.from(row.subject).toString("utf8"), createdAt: row.created_at } }
89
+ : { ok: false, error: { code: "LINK_INVALID" } };
90
+ },
91
+ });
92
+
93
+ /** Platform maintenance spans packages; the application facade never exposes this operation. */
94
+ export const cleanupLinksBatch = async (db: SQL): Promise<number> =>
95
+ db.begin(async (tx) => {
96
+ await tx`SET LOCAL statement_timeout = '10s'`;
97
+ const rows = await tx<{ token_hash: string }[]>`
98
+ WITH expired AS (
99
+ SELECT package, token_hash FROM platform._cloud_links
100
+ WHERE LEAST(expires_at, used_at) < statement_timestamp() - interval '24 hours'
101
+ ORDER BY LEAST(expires_at, used_at) LIMIT ${LINK_CLEANUP_BATCH} FOR UPDATE SKIP LOCKED
102
+ ) DELETE FROM platform._cloud_links link USING expired
103
+ WHERE link.package = expired.package AND link.token_hash = expired.token_hash RETURNING link.token_hash`;
104
+ return rows.length;
105
+ });
@@ -2,7 +2,7 @@ import { sql } from "bun";
2
2
  import type { PaginationParams } from "../../contracts/shared";
3
3
  import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
4
4
  import { registerSettings } from "../settings/defaults";
5
- import { redactMetadata } from "./redaction";
5
+ import { redactLinkTokens, redactMetadata } from "./redaction";
6
6
  import { observeSyncEvent, TRACE_STUCK_AFTER_MS, trace } from "./trace";
7
7
  import { readWebVitals } from "./web-vitals";
8
8
 
@@ -120,16 +120,18 @@ const mapRow = (row: DbLogRow): LogEntry => ({
120
120
  /** Fire-and-forget write — mirrors to console, then inserts to DB async. */
121
121
  function write(params: WriteParams): void {
122
122
  const safeMetadata = params.metadata ? (redactMetadata(params.metadata) as Record<string, unknown>) : undefined;
123
- const prefix = `[${params.source}]`;
123
+ const source = redactLinkTokens(params.source);
124
+ const message = redactLinkTokens(params.message);
125
+ const prefix = `[${source}]`;
124
126
  const consoleFn = params.level === "error" ? console.error : params.level === "warn" ? console.warn : console.log;
125
- consoleFn(prefix, params.message, ...(safeMetadata ? [safeMetadata] : []));
127
+ consoleFn(prefix, message, ...(safeMetadata ? [safeMetadata] : []));
126
128
 
127
129
  sql`
128
130
  INSERT INTO logging.entries (level, source, message, metadata)
129
131
  VALUES (
130
132
  ${params.level},
131
- ${params.source},
132
- ${params.message},
133
+ ${source},
134
+ ${message},
133
135
  (${safeMetadata ? JSON.stringify(safeMetadata) : null}::text)::jsonb
134
136
  )
135
137
  `.catch((err: Error) => console.error("[logging] DB write failed:", err.message));
@@ -7,14 +7,18 @@ const SENSITIVE_KEY_PATTERN = /(password|secret|token|cookie|authorization|api[_
7
7
 
8
8
  export const REDACTED = "[REDACTED]";
9
9
 
10
+ /** Recognizable one-time credentials are scrubbed even under ordinary metadata keys. */
11
+ export const redactLinkTokens = (value: string): string => value.replace(/cl_[A-Za-z0-9_-]{43}/g, REDACTED);
12
+
10
13
  export const isSensitiveMetadataKey = (key: string): boolean => SENSITIVE_KEY_PATTERN.test(key);
11
14
 
12
15
  export const redactMetadata = (input: unknown): unknown => {
16
+ if (typeof input === "string") return redactLinkTokens(input);
13
17
  if (input === null || typeof input !== "object") return input;
14
18
  if (Array.isArray(input)) return input.map(redactMetadata);
15
19
  const out: Record<string, unknown> = {};
16
20
  for (const [key, value] of Object.entries(input)) {
17
- out[key] = isSensitiveMetadataKey(key) ? REDACTED : redactMetadata(value);
21
+ out[redactLinkTokens(key)] = isSensitiveMetadataKey(key) ? REDACTED : redactMetadata(value);
18
22
  }
19
23
  return out;
20
24
  };
@@ -60,6 +64,7 @@ export const redactSensitivePath = (pathname: string): string => {
60
64
  .split("/")
61
65
  .map((segment) => {
62
66
  const value = decodeSegment(segment);
67
+ if (redactLinkTokens(value) !== value) return ":token";
63
68
  if (UUID.test(value)) return ":id";
64
69
  // Dot-separated parts cover signed tokens and credential filenames such as .ics.
65
70
  // Lowercase word slugs preserve readable routes like getting-started-with-grids.
@@ -4,7 +4,7 @@ import { sql } from "bun";
4
4
  import { getProcessApplicationId } from "../../_internal/process-identity";
5
5
  import type { PaginationParams } from "../../contracts/shared";
6
6
  import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
7
- import { isSensitiveMetadataKey, REDACTED, redactMetadata } from "./redaction";
7
+ import { isSensitiveMetadataKey, REDACTED, redactLinkTokens, redactMetadata } from "./redaction";
8
8
 
9
9
  export type TraceAttributeValue = string | number | boolean | null | undefined;
10
10
  export type TraceAttributes = Record<string, TraceAttributeValue>;
@@ -499,10 +499,10 @@ const start = async (params: TraceStartParams, preservePresentation = false): Pr
499
499
  VALUES (
500
500
  ${context.traceId},
501
501
  ${context.spanId},
502
- ${params.spanKey ?? null},
502
+ ${params.spanKey ? redactLinkTokens(params.spanKey) : null},
503
503
  ${params.parent?.spanId ?? null},
504
- ${params.name},
505
- ${params.source},
504
+ ${redactLinkTokens(params.name)},
505
+ ${redactLinkTokens(params.source)},
506
506
  ${params.appId ?? null},
507
507
  ${params.category ?? "custom"},
508
508
  ${params.kind ?? "internal"},
@@ -567,10 +567,10 @@ const complete = async (params: TraceCompleteParams, preservePresentation = fals
567
567
  VALUES (
568
568
  ${context.traceId},
569
569
  ${context.spanId},
570
- ${params.spanKey ?? null},
570
+ ${params.spanKey ? redactLinkTokens(params.spanKey) : null},
571
571
  ${params.parent?.spanId ?? null},
572
- ${params.name},
573
- ${params.source},
572
+ ${redactLinkTokens(params.name)},
573
+ ${redactLinkTokens(params.source)},
574
574
  ${params.appId ?? null},
575
575
  ${params.category ?? "custom"},
576
576
  ${params.kind ?? "internal"},
@@ -636,7 +636,7 @@ const record = async (params: TraceRecordParams, preservePresentation = false):
636
636
  VALUES (
637
637
  ${context.traceId},
638
638
  ${context.spanId},
639
- ${params.event},
639
+ ${redactLinkTokens(params.event)},
640
640
  ${params.severity ?? "info"},
641
641
  (${attributesJson}::text)::jsonb,
642
642
  ${body},
@@ -1,8 +1,9 @@
1
- import { env } from "../../config/env";
2
1
  import { networkAddressFamily, normalizeNetworkHostname, parseNetworkAllowlistEnv } from "../../shared/network-address";
3
2
  import { NetworkTargetBlockedError, resolveAllowedNetworkAddresses } from "../network-security";
3
+ import { coreSettings } from "../settings/api";
4
4
 
5
- export const outgoingMailAllowlist = () => parseNetworkAllowlistEnv("OUTGOING_MAIL_ALLOWED_TARGETS", env.OUTGOING_MAIL_ALLOWED_TARGETS);
5
+ export const outgoingMailAllowlist = async () =>
6
+ parseNetworkAllowlistEnv("outgoing_mail.allowed_targets", await coreSettings.get<string[]>("outgoing_mail.allowed_targets"));
6
7
 
7
8
  /** One bounded lookup shared by SMTP and bounce IMAP; clients use only the checked addresses. */
8
9
  export const resolveMailHost = async (host: string, signal?: AbortSignal, resolve = resolveAllowedNetworkAddresses, port?: number) => {
@@ -14,7 +15,7 @@ export const resolveMailHost = async (host: string, signal?: AbortSignal, resolv
14
15
  try {
15
16
  cancellation.throwIfAborted();
16
17
  const addresses = await Promise.race([
17
- resolve(hostname, outgoingMailAllowlist(), undefined, port),
18
+ outgoingMailAllowlist().then((allowlist) => resolve(hostname, allowlist, undefined, port)),
18
19
  new Promise<never>((_, reject) => {
19
20
  abort = () => reject(cancellation.reason);
20
21
  cancellation.addEventListener("abort", abort, { once: true });
@@ -29,7 +30,7 @@ export const resolveMailHost = async (host: string, signal?: AbortSignal, resolv
29
30
  } catch (error) {
30
31
  if (error instanceof NetworkTargetBlockedError)
31
32
  throw new NetworkTargetBlockedError(
32
- "Outgoing mail target is on an internal network. An operator can allow it with OUTGOING_MAIL_ALLOWED_TARGETS.",
33
+ "Outgoing mail target is on an internal network. An operator can allow it with outgoing_mail.allowed_targets.",
33
34
  );
34
35
  throw error;
35
36
  } finally {
@@ -1,5 +1,5 @@
1
1
  import { RedisClient } from "bun";
2
- import { env } from "../config/env";
2
+ import { connections } from "../runtime/connections";
3
3
 
4
4
  // Cache commands must not queue across a disconnect: delayed fills/invalidations
5
5
  // are not useful, and reads must be able to fall back to Postgres. Keep this
@@ -10,7 +10,7 @@ let reconnecting: Promise<void> | undefined;
10
10
 
11
11
  export const requestCacheRedis = async (): Promise<RedisClient> => {
12
12
  if (!client) {
13
- client = new RedisClient(env.REDIS_URL, { enableOfflineQueue: false });
13
+ client = new RedisClient(connections().valkey.url, { enableOfflineQueue: false });
14
14
  // The first operation waits for normal connection establishment (Bun's
15
15
  // connection timeout applies). Later disconnected operations fail promptly.
16
16
  initialConnection = client.connect().catch(() => {});