@k2b/cloud 0.45.0 → 0.47.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.
- package/package.json +11 -4
- package/scripts/build.ts +5 -1
- package/scripts/preload.ts +5 -1
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +40 -0
- package/src/_internal/heartbeat.ts +19 -0
- package/src/_internal/postgres-application-name.ts +11 -0
- package/src/_internal/process-sync.ts +9 -0
- package/src/_internal/readiness.ts +6 -1
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +6 -0
- package/src/_internal/workload-credential.ts +153 -0
- package/src/_internal/workload-protocol.ts +36 -0
- package/src/ai/application-tasks.ts +5 -2
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chart-block.ts +36 -711
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tool-contracts.ts +21 -259
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/executor.ts +13 -1
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/protocol.ts +3 -1
- package/src/ai/routes.ts +2 -0
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/admin-app-keys.ts +66 -0
- package/src/api/admin-core-settings.ts +2 -0
- package/src/api/admin-identity.ts +27 -1
- package/src/api/admin-settings-env.ts +120 -0
- package/src/api/capabilities.ts +99 -15
- package/src/api/index.ts +3 -0
- package/src/api/mcp.ts +4 -2
- package/src/api/settings-ops.ts +35 -0
- package/src/app-key.ts +125 -0
- package/src/browser/CloudResourceSearch.tsx +48 -41
- package/src/browser/notifications.ts +25 -8
- package/src/capabilities/discussion-providers.ts +100 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +69 -0
- package/src/capabilities/response.ts +4 -1
- package/src/capabilities/server-invoke.ts +64 -36
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/capabilities/types.ts +1 -1
- package/src/cli/admin/app-credentials.ts +32 -7
- package/src/cli/admin/app-keys.ts +226 -0
- package/src/cli/admin/index.ts +7 -1
- package/src/cli/admin/settings.ts +62 -0
- package/src/config/define-env.ts +63 -8
- package/src/config/env.ts +27 -21
- package/src/config/index.ts +12 -1
- package/src/config/workflow-ai.ts +28 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/settings-env.ts +21 -0
- package/src/contracts/settings-types.ts +6 -0
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/index.ts +9 -0
- package/src/runtime/clients.ts +72 -0
- package/src/runtime/connections.ts +37 -0
- package/src/runtime/resolve-connections.ts +91 -0
- package/src/server/client-ip.ts +1 -1
- package/src/services/accounts/app.ts +28 -6
- package/src/services/accounts/entities.ts +11 -4
- package/src/services/accounts/groups.ts +10 -4
- package/src/services/accounts/local-groups.ts +87 -38
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/app-keys.ts +298 -0
- package/src/services/gateway.ts +6 -0
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/index.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/identity/resource-readers.ts +80 -0
- package/src/services/identity/runtime-config.ts +14 -6
- package/src/services/identity/workload-runtime.ts +253 -0
- package/src/services/index.ts +4 -1
- package/src/services/outgoing-mail/resolve-host.ts +5 -4
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/service-account-credentials.ts +63 -21
- package/src/services/settings/api.ts +3 -3
- package/src/services/settings/binding.ts +45 -0
- package/src/services/settings/core-settings.ts +99 -1
- package/src/services/settings/defaults.ts +16 -2
- package/src/services/settings/env-status.ts +69 -0
- package/src/services/settings/index.ts +69 -26
- package/src/services/settings/store.ts +73 -8
- package/src/services/settings/validation.ts +40 -0
- package/src/shared/index.ts +11 -10
- package/src/shared/markdown/extensions/tables.ts +10 -1
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/start.ts +36 -0
- package/src/styles/global.css +2 -0
- package/src/workflows/ai/index.ts +1 -0
- package/src/workflows/ai/runtime.ts +16 -8
- package/src/workflows/ai/settings.ts +5 -0
- package/src/shared/markdown/formula.ts +0 -1283
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
2
|
+
import { type SQL, sql } from "bun";
|
|
3
|
+
import { appKeyFingerprint, validateAppKeyAppId } from "../app-key";
|
|
4
|
+
import type { User } from "../contracts/shared";
|
|
5
|
+
import { audit } from "./audit";
|
|
6
|
+
import { serviceAccounts } from "./service-accounts";
|
|
7
|
+
|
|
8
|
+
type AdminActor = Pick<User, "id" | "uid" | "provider" | "roles">;
|
|
9
|
+
export type AppKeyMetadata = {
|
|
10
|
+
keyId: string;
|
|
11
|
+
appId: string;
|
|
12
|
+
serviceAccountId: string;
|
|
13
|
+
kind: "app_key";
|
|
14
|
+
name: string;
|
|
15
|
+
publicKey: string;
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
status: "active" | "expired" | "revoked";
|
|
18
|
+
accountStatus: "active" | "disabled";
|
|
19
|
+
createdAt: string;
|
|
20
|
+
createdBy: string | null;
|
|
21
|
+
expiresAt: string | null;
|
|
22
|
+
revokedAt: string | null;
|
|
23
|
+
revokedBy: string | null;
|
|
24
|
+
lastUsedAt: string | null;
|
|
25
|
+
};
|
|
26
|
+
type Row = {
|
|
27
|
+
id: string;
|
|
28
|
+
app_id: string;
|
|
29
|
+
service_account_id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
public_key: string;
|
|
32
|
+
fingerprint: string;
|
|
33
|
+
key_status: AppKeyMetadata["status"];
|
|
34
|
+
account_status: AppKeyMetadata["accountStatus"];
|
|
35
|
+
created_at: Date;
|
|
36
|
+
created_by: string | null;
|
|
37
|
+
expires_at: Date | null;
|
|
38
|
+
revoked_at: Date | null;
|
|
39
|
+
revoked_by: string | null;
|
|
40
|
+
last_used_at: Date | null;
|
|
41
|
+
};
|
|
42
|
+
const map = (row: Row): AppKeyMetadata => ({
|
|
43
|
+
keyId: row.id,
|
|
44
|
+
appId: row.app_id,
|
|
45
|
+
serviceAccountId: row.service_account_id,
|
|
46
|
+
kind: "app_key",
|
|
47
|
+
name: row.name,
|
|
48
|
+
publicKey: row.public_key,
|
|
49
|
+
fingerprint: row.fingerprint,
|
|
50
|
+
status: row.key_status,
|
|
51
|
+
accountStatus: row.account_status,
|
|
52
|
+
createdAt: row.created_at.toISOString(),
|
|
53
|
+
createdBy: row.created_by,
|
|
54
|
+
expiresAt: row.expires_at?.toISOString() ?? null,
|
|
55
|
+
revokedAt: row.revoked_at?.toISOString() ?? null,
|
|
56
|
+
revokedBy: row.revoked_by,
|
|
57
|
+
lastUsedAt: row.last_used_at?.toISOString() ?? null,
|
|
58
|
+
});
|
|
59
|
+
const columns = `c.*, sa.app_id, sa.status AS account_status,
|
|
60
|
+
CASE WHEN c.status = 'revoked' THEN 'revoked' WHEN c.expires_at <= now() THEN 'expired' ELSE 'active' END AS key_status`;
|
|
61
|
+
const actorForAudit = (actor: AdminActor) => ({ userId: actor.id, uid: actor.uid, provider: actor.provider, roles: actor.roles });
|
|
62
|
+
const validateAdmin = (actor: AdminActor, appId?: string): Result<void> => {
|
|
63
|
+
if (!actor.roles.includes("admin")) return fail(err.forbidden("Administrator access required"));
|
|
64
|
+
if (appId !== undefined) {
|
|
65
|
+
try {
|
|
66
|
+
validateAppKeyAppId(appId);
|
|
67
|
+
} catch {
|
|
68
|
+
return fail(err.badInput("Invalid or reserved app ID"));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return ok();
|
|
72
|
+
};
|
|
73
|
+
const pageParams = (params: { page?: number; perPage?: number }) => ({
|
|
74
|
+
page: Math.max(1, Math.trunc(params.page ?? 1)),
|
|
75
|
+
perPage: Math.min(500, Math.max(1, Math.trunc(params.perPage ?? 100))),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Durable DB identity, initialized transactionally by Core's migration; never cached or read from env. */
|
|
79
|
+
export const getInstallationId = async (db: SQL = sql): Promise<string> => {
|
|
80
|
+
const [row] = await db<{ id: string }[]>`SELECT id FROM auth.installation WHERE singleton = true`;
|
|
81
|
+
if (!row) throw new Error("Core installation identity is not initialized; run Core migrations");
|
|
82
|
+
return row.id;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const registerAppKey = async (params: {
|
|
86
|
+
actor: AdminActor;
|
|
87
|
+
appId: string;
|
|
88
|
+
publicKey: string;
|
|
89
|
+
name?: string;
|
|
90
|
+
expiresAt?: string;
|
|
91
|
+
}): Promise<
|
|
92
|
+
Result<{
|
|
93
|
+
keyId: string;
|
|
94
|
+
installationId: string;
|
|
95
|
+
fingerprint: string;
|
|
96
|
+
status: AppKeyMetadata["status"];
|
|
97
|
+
accountStatus: AppKeyMetadata["accountStatus"];
|
|
98
|
+
}>
|
|
99
|
+
> => {
|
|
100
|
+
const valid = validateAdmin(params.actor, params.appId);
|
|
101
|
+
if (!valid.ok) return valid;
|
|
102
|
+
const name = params.name?.trim() ?? "App key";
|
|
103
|
+
if (!name || name.length > 120) return fail(err.badInput("Key name must contain 1 to 120 characters"));
|
|
104
|
+
let fingerprint: string;
|
|
105
|
+
try {
|
|
106
|
+
fingerprint = await appKeyFingerprint(params.publicKey);
|
|
107
|
+
} catch {
|
|
108
|
+
return fail(err.badInput("Invalid Ed25519 public key: expected a canonical 32-byte base64url point"));
|
|
109
|
+
}
|
|
110
|
+
const expiresAt = params.expiresAt ? new Date(params.expiresAt) : null;
|
|
111
|
+
if (expiresAt && !Number.isFinite(expiresAt.getTime())) return fail(err.badInput("Expiry must be in the future"));
|
|
112
|
+
const account = await serviceAccounts.getOrCreateResourceBound({
|
|
113
|
+
name: `${params.appId} workload`,
|
|
114
|
+
appId: params.appId,
|
|
115
|
+
resourceType: "cloud.app",
|
|
116
|
+
resourceId: params.appId,
|
|
117
|
+
createdBy: params.actor.id,
|
|
118
|
+
});
|
|
119
|
+
if (!account.ok) return account;
|
|
120
|
+
return sql.begin(async (db) => {
|
|
121
|
+
// Resource-bound accounts carry no unique index, so concurrent first enrollments can
|
|
122
|
+
// create more than one account for an app. One transaction lock per app serializes
|
|
123
|
+
// enrollment across Core replicas; keys count and land on the oldest account, the
|
|
124
|
+
// same one getByResource returns.
|
|
125
|
+
await db`SELECT pg_advisory_xact_lock(hashtextextended(${`app_key:${params.appId}`}, 0))`;
|
|
126
|
+
const [locked] = await db<{ id: string; status: AppKeyMetadata["accountStatus"] }[]>`
|
|
127
|
+
SELECT id, status FROM auth.service_accounts
|
|
128
|
+
WHERE kind = 'resource_bound' AND app_id = ${params.appId} AND resource_type = 'cloud.app' AND resource_id = ${params.appId}
|
|
129
|
+
ORDER BY created_at ASC
|
|
130
|
+
LIMIT 1
|
|
131
|
+
FOR UPDATE`;
|
|
132
|
+
if (!locked) return fail(err.notFound("Service account"));
|
|
133
|
+
const accountId = locked.id;
|
|
134
|
+
const accountStatus = locked.status;
|
|
135
|
+
const installationId = await getInstallationId(db);
|
|
136
|
+
const [existing] = await db<{ id: string; fingerprint: string; status: AppKeyMetadata["status"] }[]>`
|
|
137
|
+
SELECT c.id, c.fingerprint,
|
|
138
|
+
CASE WHEN c.status = 'revoked' THEN 'revoked' WHEN c.expires_at <= now() THEN 'expired' ELSE 'active' END AS status
|
|
139
|
+
FROM auth.service_account_credentials c
|
|
140
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
141
|
+
WHERE sa.kind = 'resource_bound' AND sa.app_id = ${params.appId} AND sa.resource_type = 'cloud.app'
|
|
142
|
+
AND sa.resource_id = ${params.appId} AND c.kind = 'app_key' AND c.public_key = ${params.publicKey}
|
|
143
|
+
`;
|
|
144
|
+
// Revoked/expired keys remain idempotent and are never reactivated.
|
|
145
|
+
if (existing)
|
|
146
|
+
return ok({ keyId: existing.id, installationId, fingerprint: existing.fingerprint, status: existing.status, accountStatus });
|
|
147
|
+
if (accountStatus !== "active")
|
|
148
|
+
return fail({
|
|
149
|
+
...err.conflict("Service account"),
|
|
150
|
+
message: "The app service account is disabled; enable it before enrolling a new key",
|
|
151
|
+
});
|
|
152
|
+
if (expiresAt && expiresAt.getTime() <= Date.now()) return fail(err.badInput("Expiry must be in the future"));
|
|
153
|
+
const [count] = await db<{ active: number }[]>`
|
|
154
|
+
SELECT count(*)::int AS active FROM auth.service_account_credentials c
|
|
155
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
156
|
+
WHERE sa.kind = 'resource_bound' AND sa.app_id = ${params.appId} AND sa.resource_type = 'cloud.app'
|
|
157
|
+
AND sa.resource_id = ${params.appId} AND c.kind = 'app_key' AND c.status = 'active'
|
|
158
|
+
AND (c.expires_at IS NULL OR c.expires_at > now())
|
|
159
|
+
`;
|
|
160
|
+
if ((count?.active ?? 0) >= 2)
|
|
161
|
+
return fail({ ...err.conflict("App key"), message: "An app can have at most two active keys; revoke an old key first" });
|
|
162
|
+
const [row] = await db<{ id: string }[]>`
|
|
163
|
+
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})
|
|
166
|
+
RETURNING id
|
|
167
|
+
`;
|
|
168
|
+
if (!row) throw new Error("App key registration returned no key");
|
|
169
|
+
await audit.record(
|
|
170
|
+
{
|
|
171
|
+
action: "app_key.create",
|
|
172
|
+
outcome: "allowed",
|
|
173
|
+
actor: actorForAudit(params.actor),
|
|
174
|
+
target: { type: "service_account_credential", id: row.id, label: name },
|
|
175
|
+
metadata: { appId: params.appId, serviceAccountId: accountId, fingerprint },
|
|
176
|
+
},
|
|
177
|
+
db,
|
|
178
|
+
);
|
|
179
|
+
return ok({ keyId: row.id, installationId, fingerprint, status: "active" as const, accountStatus });
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const listAppKeys = async (params: { actor: AdminActor; appId: string; page?: number; perPage?: number }) => {
|
|
184
|
+
const valid = validateAdmin(params.actor, params.appId);
|
|
185
|
+
if (!valid.ok) return valid;
|
|
186
|
+
const { page, perPage } = pageParams(params);
|
|
187
|
+
const rows = await sql<(Row & { total: number })[]>`
|
|
188
|
+
SELECT ${sql.unsafe(columns)}, count(*) OVER()::int AS total FROM auth.service_account_credentials c
|
|
189
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
190
|
+
WHERE c.kind = 'app_key' AND sa.kind = 'resource_bound' AND sa.app_id = ${params.appId}
|
|
191
|
+
AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${params.appId}
|
|
192
|
+
ORDER BY c.created_at DESC, c.id DESC LIMIT ${perPage} OFFSET ${(page - 1) * perPage}
|
|
193
|
+
`;
|
|
194
|
+
let total = rows[0]?.total ?? 0;
|
|
195
|
+
if (rows.length === 0 && page > 1) {
|
|
196
|
+
const [count] = await sql<{ total: number }[]>`
|
|
197
|
+
SELECT count(*)::int AS total FROM auth.service_account_credentials c
|
|
198
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
199
|
+
WHERE c.kind = 'app_key' AND sa.kind = 'resource_bound' AND sa.app_id = ${params.appId}
|
|
200
|
+
AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${params.appId}
|
|
201
|
+
`;
|
|
202
|
+
total = count?.total ?? 0;
|
|
203
|
+
}
|
|
204
|
+
return ok({ installationId: await getInstallationId(), items: rows.map(map), page, perPage, total, hasNext: page * perPage < total });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const listAppsWithKeys = async (params: { actor: AdminActor; page?: number; perPage?: number }) => {
|
|
208
|
+
const valid = validateAdmin(params.actor);
|
|
209
|
+
if (!valid.ok) return valid;
|
|
210
|
+
const { page, perPage } = pageParams(params);
|
|
211
|
+
const rows = await sql<
|
|
212
|
+
{ appId: string; accountStatus: AppKeyMetadata["accountStatus"]; totalKeys: number; activeKeys: number; total: number }[]
|
|
213
|
+
>`
|
|
214
|
+
SELECT sa.app_id AS "appId", sa.status AS "accountStatus", count(*)::int AS "totalKeys",
|
|
215
|
+
count(*) FILTER (WHERE c.status = 'active' AND (c.expires_at IS NULL OR c.expires_at > now()))::int AS "activeKeys",
|
|
216
|
+
count(*) OVER()::int AS total
|
|
217
|
+
FROM auth.service_account_credentials c JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
218
|
+
WHERE c.kind = 'app_key' AND sa.kind = 'resource_bound' AND sa.resource_type = 'cloud.app' AND sa.resource_id = sa.app_id
|
|
219
|
+
GROUP BY sa.app_id, sa.status ORDER BY sa.app_id LIMIT ${perPage} OFFSET ${(page - 1) * perPage}
|
|
220
|
+
`;
|
|
221
|
+
let total = rows[0]?.total ?? 0;
|
|
222
|
+
if (rows.length === 0 && page > 1) {
|
|
223
|
+
const [count] = await sql<{ total: number }[]>`
|
|
224
|
+
SELECT count(DISTINCT sa.app_id)::int AS total FROM auth.service_account_credentials c
|
|
225
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
226
|
+
WHERE c.kind = 'app_key' AND sa.kind = 'resource_bound' AND sa.resource_type = 'cloud.app' AND sa.resource_id = sa.app_id
|
|
227
|
+
`;
|
|
228
|
+
total = count?.total ?? 0;
|
|
229
|
+
}
|
|
230
|
+
return ok({
|
|
231
|
+
installationId: await getInstallationId(),
|
|
232
|
+
items: rows.map(({ total: _, ...row }) => row),
|
|
233
|
+
page,
|
|
234
|
+
perPage,
|
|
235
|
+
total,
|
|
236
|
+
hasNext: page * perPage < total,
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const revokeAppKey = async (params: { actor: AdminActor; appId: string; keyId: string }): Promise<Result<void>> => {
|
|
241
|
+
const valid = validateAdmin(params.actor, params.appId);
|
|
242
|
+
if (!valid.ok) return valid;
|
|
243
|
+
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))
|
|
244
|
+
return fail(err.notFound("App key"));
|
|
245
|
+
return sql.begin(async (db) => {
|
|
246
|
+
const [key] = await db<{ id: string; service_account_id: string; status: string; name: string; fingerprint: string }[]>`
|
|
247
|
+
SELECT c.id, c.service_account_id, c.status, c.name, c.fingerprint FROM auth.service_account_credentials c
|
|
248
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
249
|
+
WHERE c.id = ${params.keyId}::uuid AND c.kind = 'app_key' AND sa.app_id = ${params.appId}
|
|
250
|
+
AND sa.kind = 'resource_bound' AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${params.appId}
|
|
251
|
+
FOR UPDATE OF sa, c
|
|
252
|
+
`;
|
|
253
|
+
if (!key) return fail(err.notFound("App key"));
|
|
254
|
+
if (key.status === "revoked") return ok();
|
|
255
|
+
await db`UPDATE auth.service_account_credentials SET status = 'revoked', revoked_at = now(), revoked_by = ${params.actor.id}::uuid
|
|
256
|
+
WHERE id = ${key.id}::uuid`;
|
|
257
|
+
await audit.record(
|
|
258
|
+
{
|
|
259
|
+
action: "app_key.revoke",
|
|
260
|
+
outcome: "allowed",
|
|
261
|
+
actor: actorForAudit(params.actor),
|
|
262
|
+
target: { type: "service_account_credential", id: key.id, label: key.name },
|
|
263
|
+
metadata: { appId: params.appId, serviceAccountId: key.service_account_id, fingerprint: key.fingerprint },
|
|
264
|
+
},
|
|
265
|
+
db,
|
|
266
|
+
);
|
|
267
|
+
return ok();
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
|
|
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> => {
|
|
273
|
+
try {
|
|
274
|
+
validateAppKeyAppId(params.appId);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
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
|
+
return null;
|
|
280
|
+
const [row] = await db<Row[]>`
|
|
281
|
+
SELECT ${db.unsafe(columns)} FROM auth.service_account_credentials c
|
|
282
|
+
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'
|
|
285
|
+
AND sa.kind = 'resource_bound' AND sa.app_id = ${params.appId} AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${params.appId}
|
|
286
|
+
`;
|
|
287
|
+
return row ? map(row) : null;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** Call only after successful authentication; a late touch cannot revive a key. */
|
|
291
|
+
export const touchAppKey = async (params: { appId: string; keyId: string }, db: SQL = sql): Promise<void> => {
|
|
292
|
+
const key = await getActiveAppKey(params, db);
|
|
293
|
+
if (!key) return;
|
|
294
|
+
await db`UPDATE auth.service_account_credentials SET last_used_at = now() WHERE id = ${key.keyId}::uuid AND status = 'active'
|
|
295
|
+
AND (expires_at IS NULL OR expires_at > now())`;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const appKeys = { register: registerAppKey, list: listAppKeys, listApps: listAppsWithKeys, revoke: revokeAppKey };
|
package/src/services/gateway.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lazySync } from "../_internal/process-sync";
|
|
2
|
+
import type { EnvSettingStatus } from "../contracts/settings-env";
|
|
2
3
|
import { logger } from "./logging";
|
|
3
4
|
|
|
4
5
|
/** Written by the gateway router, read by gateway-ops: one owner, one declaration. */
|
|
@@ -24,6 +25,8 @@ export type GatewayRouteWarning = {
|
|
|
24
25
|
|
|
25
26
|
export type GatewayRouteSnapshotInput = {
|
|
26
27
|
instanceId: string;
|
|
28
|
+
/** Sanitized migration diagnostics; omitted by pre-migration gateways. */
|
|
29
|
+
envSettings?: EnvSettingStatus[];
|
|
27
30
|
baseUrl: string;
|
|
28
31
|
startedAt: number;
|
|
29
32
|
routeHash: string;
|
|
@@ -44,6 +47,8 @@ export type GatewayRouteSnapshotInput = {
|
|
|
44
47
|
|
|
45
48
|
export type GatewayRouteSnapshot = {
|
|
46
49
|
instanceId: string;
|
|
50
|
+
/** Sanitized migration diagnostics; omitted by pre-migration gateways. */
|
|
51
|
+
envSettings?: EnvSettingStatus[];
|
|
47
52
|
baseUrl: string;
|
|
48
53
|
startedAt: number;
|
|
49
54
|
updatedAt: number;
|
|
@@ -97,6 +102,7 @@ const snapshots = lazySync((sync) =>
|
|
|
97
102
|
|
|
98
103
|
export const buildGatewayRouteSnapshot = (input: GatewayRouteSnapshotInput): GatewayRouteSnapshot => ({
|
|
99
104
|
instanceId: input.instanceId,
|
|
105
|
+
...(input.envSettings ? { envSettings: input.envSettings } : {}),
|
|
100
106
|
baseUrl: input.baseUrl,
|
|
101
107
|
startedAt: input.startedAt,
|
|
102
108
|
updatedAt: Date.now(),
|
|
@@ -4,13 +4,14 @@ import type { GeoFunction } from "../../contracts/geo";
|
|
|
4
4
|
import { requestCacheRedis } from "../request-cache-redis";
|
|
5
5
|
import type { GeoRedis } from "./bucket";
|
|
6
6
|
|
|
7
|
-
export const GEO_ADAPTER_VERSION = "geoapify-
|
|
7
|
+
export const GEO_ADAPTER_VERSION = "geoapify-v3";
|
|
8
8
|
export const GEO_MAX_JSON_BYTES = 256 * 1024;
|
|
9
9
|
// Raw provider geometry is compacted before entering the public result/cache budget.
|
|
10
10
|
export const GEO_MAX_ROUTE_RESPONSE_BYTES = 26 * GEO_MAX_JSON_BYTES;
|
|
11
11
|
const hash = (value: string) => createHash("sha256").update(value).digest("hex");
|
|
12
12
|
export const normalizeGeoInput = (input: unknown, fn: GeoFunction, path = ""): unknown => {
|
|
13
13
|
const field = path.split(".").at(-1);
|
|
14
|
+
if (typeof input === "string" && fn === "places" && field === "name") return input.trim();
|
|
14
15
|
if (typeof input === "string" && fn === "route") return field === "polyline6" ? hash(input) : input;
|
|
15
16
|
if (typeof input === "string") return field === "polyline6" ? hash(input) : input.toLowerCase().trim().replace(/\s+/g, " ");
|
|
16
17
|
if (typeof input === "number") {
|
|
@@ -86,6 +86,7 @@ const address = (p: RawProperties, provenance: GeoProvenance): GeoAddress => ({
|
|
|
86
86
|
point: { lat: p.lat, lon: p.lon },
|
|
87
87
|
formatted: p.formatted ?? p.address_line1 ?? "",
|
|
88
88
|
confidence: p.rank?.confidence ?? null,
|
|
89
|
+
buildingConfidence: p.rank?.confidence_building_level ?? null,
|
|
89
90
|
provenance,
|
|
90
91
|
});
|
|
91
92
|
const areaFilter = (area: NonNullable<GeoAutocompleteInput["area"]>) =>
|
|
@@ -247,6 +248,7 @@ export class GeoapifyAdapter {
|
|
|
247
248
|
if (input.along && line)
|
|
248
249
|
body = {
|
|
249
250
|
categories,
|
|
251
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
250
252
|
limit: input.limit ?? 20,
|
|
251
253
|
filter: {
|
|
252
254
|
type: "polyline",
|
|
@@ -256,6 +258,7 @@ export class GeoapifyAdapter {
|
|
|
256
258
|
};
|
|
257
259
|
else {
|
|
258
260
|
params.set("categories", categories.join(","));
|
|
261
|
+
if (input.name !== undefined) params.set("name", input.name);
|
|
259
262
|
if (input.near) params.set("bias", `proximity:${input.near.lon},${input.near.lat}`);
|
|
260
263
|
if (input.area) params.set("filter", areaFilter(input.area));
|
|
261
264
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { getAppWorkloadCredential } from "../../_internal/workload-credential";
|
|
1
2
|
export {
|
|
2
3
|
CLOUD_INVOCATION_JWKS_PATH,
|
|
3
4
|
CLOUD_INVOCATION_PROTOCOL_VERSION,
|
|
@@ -37,6 +38,7 @@ export {
|
|
|
37
38
|
} from "./key-ring";
|
|
38
39
|
export { identityMetrics } from "./metrics";
|
|
39
40
|
export { isNestedSourceCurrent, type NestedInvocationLease, nestedInvocationLeases } from "./nested-invocation";
|
|
41
|
+
export { type ResourceReader, resolveResourceReaders } from "./resource-readers";
|
|
40
42
|
export { createIdentityPublicRoutes } from "./routes";
|
|
41
43
|
export {
|
|
42
44
|
type CloudSessionClaims,
|
|
@@ -51,3 +53,4 @@ export {
|
|
|
51
53
|
WORKLOAD_SCOPES,
|
|
52
54
|
type WorkloadScope,
|
|
53
55
|
} from "./workload-auth";
|
|
56
|
+
export { startAppWorkloadBroker } from "./workload-runtime";
|
|
@@ -485,3 +485,113 @@ export const verifyAppAccessProbe = async (
|
|
|
485
485
|
return null;
|
|
486
486
|
}
|
|
487
487
|
};
|
|
488
|
+
|
|
489
|
+
// A separate payload prevents app authority from ever entering the person/mandate resolver.
|
|
490
|
+
const AppQueryClaimsSchema = z
|
|
491
|
+
.object({
|
|
492
|
+
iss: z.string().url(),
|
|
493
|
+
aud: z.string().regex(/^app:[a-z][a-z0-9-]{0,79}$/),
|
|
494
|
+
token_use: z.literal("app_query"),
|
|
495
|
+
sub: AppIdSchema,
|
|
496
|
+
credential_id: z.uuid(),
|
|
497
|
+
service_account_id: z.uuid(),
|
|
498
|
+
op: OperationSchema.refine((value) => value.startsWith("capability.query:")),
|
|
499
|
+
schema_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
500
|
+
input_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
501
|
+
request_id: BoundedIdentifierSchema,
|
|
502
|
+
jti: z.uuid(),
|
|
503
|
+
iat: z.number().int(),
|
|
504
|
+
nbf: z.number().int(),
|
|
505
|
+
exp: z.number().int(),
|
|
506
|
+
})
|
|
507
|
+
.strict()
|
|
508
|
+
.refine((claims) => claims.nbf === claims.iat && claims.exp - claims.iat === CLOUD_INVOCATION_TOKEN_TTL_SECONDS);
|
|
509
|
+
|
|
510
|
+
export const signAppQueryInvocation = async (input: {
|
|
511
|
+
workload: import("./workload-auth").AuthenticatedWorkload;
|
|
512
|
+
targetAppId: string;
|
|
513
|
+
capabilityId: string;
|
|
514
|
+
schemaHash: string;
|
|
515
|
+
input: unknown;
|
|
516
|
+
requestId: string;
|
|
517
|
+
signer: PreparedIdentitySigner & { issuer: string };
|
|
518
|
+
}) => {
|
|
519
|
+
const now = Math.floor(Date.now() / 1000);
|
|
520
|
+
if (input.signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
|
|
521
|
+
throw new Error("Signing key expires before the app query");
|
|
522
|
+
const claims = AppQueryClaimsSchema.parse({
|
|
523
|
+
iss: input.signer.issuer,
|
|
524
|
+
aud: `app:${input.targetAppId}`,
|
|
525
|
+
token_use: "app_query",
|
|
526
|
+
sub: input.workload.appId,
|
|
527
|
+
credential_id: input.workload.credentialId,
|
|
528
|
+
service_account_id: input.workload.serviceAccountId,
|
|
529
|
+
op: `capability.query:${input.capabilityId}`,
|
|
530
|
+
schema_hash: input.schemaHash,
|
|
531
|
+
input_hash: appEventPayloadHash(input.input),
|
|
532
|
+
request_id: input.requestId,
|
|
533
|
+
jti: crypto.randomUUID(),
|
|
534
|
+
iat: now,
|
|
535
|
+
nbf: now,
|
|
536
|
+
exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
|
|
537
|
+
});
|
|
538
|
+
const token = await new SignJWT(claims)
|
|
539
|
+
.setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: input.signer.kid })
|
|
540
|
+
.sign(input.signer.key);
|
|
541
|
+
if (Buffer.byteLength(token) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) throw new Error("App query token exceeds its byte budget");
|
|
542
|
+
return { token, claims };
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
export const verifyAppQueryInvocation = async (
|
|
546
|
+
token: string,
|
|
547
|
+
expected: { appId: string; capabilityId: string; schemaHash: string; input: unknown },
|
|
548
|
+
options: { issuer?: string; key?: JWTVerifyGetKey; now?: Date } = {},
|
|
549
|
+
) => {
|
|
550
|
+
if (Buffer.byteLength(token) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
|
|
551
|
+
try {
|
|
552
|
+
assertProtectedHeader(decodeProtectedHeader(token));
|
|
553
|
+
} catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
// Configuration and key-fetch failures must remain retryable, rather than look like invalid tokens.
|
|
557
|
+
const runtime = options.issuer && options.key ? null : await getIdentityRuntimeConfig();
|
|
558
|
+
try {
|
|
559
|
+
const issuer = options.issuer ?? runtime!.issuer;
|
|
560
|
+
const state = options.key ? null : remoteSet(runtime!.invocationJwksUrl);
|
|
561
|
+
const key = options.key ?? state!.key;
|
|
562
|
+
const verified = await verifyWithKeyRefresh(
|
|
563
|
+
(candidate) =>
|
|
564
|
+
jwtVerify(token, candidate, {
|
|
565
|
+
algorithms: [CLOUD_IDENTITY_ALGORITHM],
|
|
566
|
+
issuer,
|
|
567
|
+
audience: `app:${expected.appId}`,
|
|
568
|
+
typ: CLOUD_INVOCATION_TOKEN_TYPE,
|
|
569
|
+
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
570
|
+
currentDate: options.now,
|
|
571
|
+
}),
|
|
572
|
+
key,
|
|
573
|
+
state ? { url: runtime!.invocationJwksUrl, state } : null,
|
|
574
|
+
);
|
|
575
|
+
const claims = AppQueryClaimsSchema.parse(verified.payload);
|
|
576
|
+
if (
|
|
577
|
+
claims.op !== `capability.query:${expected.capabilityId}` ||
|
|
578
|
+
claims.schema_hash !== expected.schemaHash ||
|
|
579
|
+
claims.input_hash !== appEventPayloadHash(expected.input)
|
|
580
|
+
)
|
|
581
|
+
return null;
|
|
582
|
+
return claims;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (
|
|
585
|
+
error instanceof z.ZodError ||
|
|
586
|
+
error instanceof errors.JWTExpired ||
|
|
587
|
+
error instanceof errors.JWTClaimValidationFailed ||
|
|
588
|
+
error instanceof errors.JWTInvalid ||
|
|
589
|
+
error instanceof errors.JWSInvalid ||
|
|
590
|
+
error instanceof errors.JWSSignatureVerificationFailed ||
|
|
591
|
+
error instanceof errors.JWKSNoMatchingKey ||
|
|
592
|
+
error instanceof errors.JOSENotSupported
|
|
593
|
+
)
|
|
594
|
+
return null;
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { type ResourcePrincipal, ResourcePrincipalSchema } from "../../contracts/capabilities";
|
|
4
|
+
import type { AccessSubject } from "../../contracts/shared";
|
|
5
|
+
import { decodeAccountCategoryEnabled } from "../account-category-policy";
|
|
6
|
+
import { toPgUuidArray } from "../postgres";
|
|
7
|
+
import type { ServiceAccount } from "../service-accounts";
|
|
8
|
+
|
|
9
|
+
/** Account authority only: no session, credential scopes, or content authority. */
|
|
10
|
+
export type ResourceReader = {
|
|
11
|
+
principal: ResourcePrincipal;
|
|
12
|
+
subject: AccessSubject;
|
|
13
|
+
binding: Pick<ServiceAccount, "appId" | "resourceType" | "resourceId"> | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type UserRow = { id: string; category_enabled: string | null };
|
|
17
|
+
type AccountRow = {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: ServiceAccount["kind"];
|
|
20
|
+
delegated_user_id: string | null;
|
|
21
|
+
app_id: string | null;
|
|
22
|
+
resource_type: string | null;
|
|
23
|
+
resource_id: string | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Resolve at most 500 current accounts, including delegated users and account-category policy.
|
|
27
|
+
* Grants and resource bindings remain the resource provider's responsibility.
|
|
28
|
+
*/
|
|
29
|
+
export const resolveResourceReaders = async (principals: readonly ResourcePrincipal[]): Promise<ResourceReader[]> => {
|
|
30
|
+
const input = z.array(ResourcePrincipalSchema).max(500).parse(principals);
|
|
31
|
+
if (!input.length) return [];
|
|
32
|
+
const userIds = input.flatMap((principal) => (principal.type === "user" ? [principal.userId] : []));
|
|
33
|
+
const accountIds = input.flatMap((principal) => (principal.type === "service_account" ? [principal.serviceAccountId] : []));
|
|
34
|
+
const accounts = await sql<AccountRow[]>`
|
|
35
|
+
SELECT id, kind, delegated_user_id, app_id, resource_type, resource_id
|
|
36
|
+
FROM auth.service_accounts WHERE id = ANY(${toPgUuidArray(accountIds)}::uuid[]) AND status = 'active'
|
|
37
|
+
`;
|
|
38
|
+
const users = await sql<UserRow[]>`
|
|
39
|
+
SELECT u.id, category.value AS category_enabled FROM auth.users u
|
|
40
|
+
LEFT JOIN settings.entries category ON category.key = 'user.category.' ||
|
|
41
|
+
CASE WHEN u.provider = 'ipa' THEN 'freeipa' WHEN u.profile = 'guest' THEN 'guest' ELSE 'login' END || '.enabled'
|
|
42
|
+
WHERE u.id = ANY(${toPgUuidArray([...userIds, ...accounts.flatMap((account) => (account.delegated_user_id ? [account.delegated_user_id] : []))])}::uuid[])
|
|
43
|
+
AND (u.account_expires IS NULL OR u.account_expires > clock_timestamp())
|
|
44
|
+
`;
|
|
45
|
+
const enabled = new Map<string | null, boolean>();
|
|
46
|
+
const allowedUsers = new Set<string>();
|
|
47
|
+
for (const user of users) {
|
|
48
|
+
if (!enabled.has(user.category_enabled)) enabled.set(user.category_enabled, await decodeAccountCategoryEnabled(user.category_enabled));
|
|
49
|
+
if (enabled.get(user.category_enabled)) allowedUsers.add(user.id);
|
|
50
|
+
}
|
|
51
|
+
const accountsById = new Map(accounts.map((account) => [account.id, account]));
|
|
52
|
+
return input.flatMap((principal): ResourceReader[] => {
|
|
53
|
+
if (principal.type === "user") {
|
|
54
|
+
const userId = principal.userId.toLowerCase();
|
|
55
|
+
return allowedUsers.has(userId) ? [{ principal, subject: { type: "user", userId }, binding: null }] : [];
|
|
56
|
+
}
|
|
57
|
+
const account = accountsById.get(principal.serviceAccountId.toLowerCase());
|
|
58
|
+
if (!account) return [];
|
|
59
|
+
if (account.kind === "user_delegated")
|
|
60
|
+
return account.delegated_user_id && allowedUsers.has(account.delegated_user_id)
|
|
61
|
+
? [
|
|
62
|
+
{
|
|
63
|
+
principal,
|
|
64
|
+
subject: { type: "user", userId: account.delegated_user_id, delegatedByServiceAccountId: account.id },
|
|
65
|
+
binding: null,
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
: [];
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
principal,
|
|
72
|
+
subject: { type: "service_account", serviceAccountId: account.id },
|
|
73
|
+
binding:
|
|
74
|
+
account.kind === "resource_bound"
|
|
75
|
+
? { appId: account.app_id, resourceType: account.resource_type, resourceId: account.resource_id }
|
|
76
|
+
: null,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
};
|
|
@@ -52,20 +52,28 @@ export const readIdentityGroupsAdmin = async (db: SQL): Promise<readonly string[
|
|
|
52
52
|
return Object.freeze(normalizeGroups(groups));
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
const resolveCoreJwksUrl = (
|
|
55
|
+
const resolveCoreJwksUrl = (
|
|
56
|
+
issuer: string,
|
|
57
|
+
path: string,
|
|
58
|
+
transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN ?? env.CLOUD_CORE_INTERNAL_ORIGIN,
|
|
59
|
+
): URL => {
|
|
56
60
|
if (!transportOrigin) return new URL(path, issuer);
|
|
57
61
|
const origin = new URL(transportOrigin);
|
|
58
62
|
if (origin.protocol !== "http:" && origin.protocol !== "https:") {
|
|
59
|
-
throw new Error("CLOUD_IDENTITY_JWKS_ORIGIN must use http or https");
|
|
63
|
+
throw new Error("Core JWKS origin (CLOUD_CORE_INTERNAL_ORIGIN or legacy CLOUD_IDENTITY_JWKS_ORIGIN) must use http or https");
|
|
60
64
|
}
|
|
61
65
|
return new URL(path, origin);
|
|
62
66
|
};
|
|
63
67
|
|
|
64
|
-
export const resolveSessionJwksUrl = (
|
|
65
|
-
|
|
68
|
+
export const resolveSessionJwksUrl = (
|
|
69
|
+
issuer: string,
|
|
70
|
+
transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN ?? env.CLOUD_CORE_INTERNAL_ORIGIN,
|
|
71
|
+
): URL => resolveCoreJwksUrl(issuer, CLOUD_SESSION_JWKS_PATH, transportOrigin);
|
|
66
72
|
|
|
67
|
-
export const resolveInvocationJwksUrl = (
|
|
68
|
-
|
|
73
|
+
export const resolveInvocationJwksUrl = (
|
|
74
|
+
issuer: string,
|
|
75
|
+
transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN ?? env.CLOUD_CORE_INTERNAL_ORIGIN,
|
|
76
|
+
): URL => resolveCoreJwksUrl(issuer, CLOUD_INVOCATION_JWKS_PATH, transportOrigin);
|
|
69
77
|
|
|
70
78
|
export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = env.CLOUD_OAUTH_JWKS_ORIGIN): URL => {
|
|
71
79
|
if (!transportOrigin) return new URL(CLOUD_OAUTH_JWKS_PATH, issuer);
|