@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.
- package/package.json +3 -3
- package/src/_internal/app-access-manager-sql.ts +10 -0
- package/src/_internal/app-access-resources.ts +2 -1
- package/src/_internal/app-capabilities.ts +73 -0
- package/src/_internal/app-event-catalog.ts +1 -1
- package/src/_internal/app-queries.ts +1 -1
- package/src/_internal/application-package.ts +2 -1
- package/src/_internal/bounded-json.ts +7 -1
- package/src/_internal/capabilities.ts +25 -2
- package/src/_internal/define-app.ts +33 -2
- package/src/_internal/heartbeat.ts +2 -0
- package/src/_internal/nats-connection.ts +10 -9
- package/src/_internal/process-sync.ts +6 -6
- package/src/_internal/public-surface.ts +76 -0
- package/src/_internal/purpose-name.ts +7 -0
- package/src/_internal/registry-validation.ts +1 -0
- package/src/_internal/registry.ts +49 -3
- package/src/_internal/workload-credential.ts +2 -1
- package/src/ai/app-skill-store.ts +2 -1
- package/src/ai/app-skills.ts +2 -1
- package/src/ai/approvals.ts +2 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/credentials.ts +2 -1
- package/src/ai/dictation-runtime.ts +1 -1
- package/src/ai/dictations.ts +1 -1
- package/src/ai/files-store.ts +2 -1
- package/src/ai/inference-calls.ts +2 -1
- package/src/ai/map-store.ts +1 -1
- package/src/ai/map-tool.ts +1 -1
- package/src/ai/memories.ts +1 -1
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +1 -1
- package/src/ai/memory-workflow-evidence.ts +1 -1
- package/src/ai/message-queue.ts +2 -1
- package/src/ai/migrate.ts +1 -1
- package/src/ai/model-access-migrate.ts +1 -1
- package/src/ai/model-access.ts +1 -1
- package/src/ai/model-catalog.ts +1 -1
- package/src/ai/model-delete.ts +1 -1
- package/src/ai/model-usage.ts +2 -1
- package/src/ai/open-questions.ts +1 -1
- package/src/ai/openai-api-slots.ts +2 -1
- package/src/ai/prefs.ts +2 -1
- package/src/ai/projects.ts +2 -1
- package/src/ai/provider-connections.ts +1 -1
- package/src/ai/quota-limits.ts +2 -1
- package/src/ai/quota-provider.ts +2 -1
- package/src/ai/quota-report.ts +2 -1
- package/src/ai/quotas-migrate.ts +2 -1
- package/src/ai/quotas.ts +2 -1
- package/src/ai/request-headers.ts +2 -1
- package/src/ai/review-content.ts +2 -1
- package/src/ai/skill-search.ts +2 -1
- package/src/ai/skills.ts +2 -1
- package/src/ai/store.ts +2 -1
- package/src/ai/structured-runs.ts +2 -1
- package/src/ai/tool-audit.ts +2 -1
- package/src/ai/turn-timing.ts +1 -1
- package/src/ai/usage.ts +2 -1
- package/src/ai/web-provenance.ts +2 -1
- package/src/api/admin-ai-quotas.ts +1 -1
- package/src/api/admin-ai.ts +1 -1
- package/src/api/admin-app-domains.ts +62 -0
- package/src/api/admin-app-keys.ts +47 -3
- package/src/api/admin-core-settings.ts +1 -1
- package/src/api/admin-outgoing-mail.ts +1 -1
- package/src/api/app-bootstrap.ts +153 -0
- package/src/api/app-events.ts +1 -1
- package/src/api/auth.ts +1 -1
- package/src/api/authenticate-app-request.ts +129 -0
- package/src/api/capabilities.ts +87 -6
- package/src/api/index.ts +7 -0
- package/src/api/me.ts +1 -1
- package/src/api/search.bench.ts +1 -1
- package/src/app-pwa/service.ts +2 -1
- package/src/app-pwa/transaction.ts +4 -2
- package/src/capabilities/claims.ts +2 -1
- package/src/capabilities/executions.ts +8 -4
- package/src/capabilities/geo-client.ts +1 -1
- package/src/capabilities/migrate.ts +3 -1
- package/src/capabilities/resource-readers.ts +2 -2
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/server-invoke.ts +73 -20
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/app-domains.ts +140 -0
- package/src/cli/admin/app-keys.ts +25 -3
- package/src/cli/admin/index.ts +107 -88
- package/src/config/env.ts +10 -4
- package/src/contracts/app-domains.ts +91 -0
- package/src/contracts/app-status.ts +104 -0
- package/src/contracts/capabilities.ts +37 -2
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-event-delivery.ts +1 -1
- package/src/events/live-delivery.ts +2 -1
- package/src/index.ts +1 -0
- package/src/runtime/bootstrap-client.ts +195 -0
- package/src/runtime/bootstrap-protocol.ts +67 -0
- package/src/runtime/clients.ts +6 -2
- package/src/runtime/connections.ts +9 -0
- package/src/server/app-key.ts +14 -0
- package/src/server/index.ts +2 -0
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access.ts +2 -1
- package/src/services/account-category-policy.ts +1 -1
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/account-lifecycle/index.ts +1 -1
- package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
- package/src/services/accounts/app.ts +1 -1
- package/src/services/accounts/avatar.ts +1 -1
- package/src/services/accounts/base-group.ts +1 -1
- package/src/services/accounts/directory-shares.ts +1 -1
- package/src/services/accounts/duplicate-emails.ts +1 -1
- package/src/services/accounts/entities.ts +2 -1
- package/src/services/accounts/group-sql.ts +2 -1
- package/src/services/accounts/groups.ts +1 -1
- package/src/services/accounts/identities.ts +13 -11
- package/src/services/accounts/ipa-data.ts +1 -1
- package/src/services/accounts/local-groups.ts +2 -1
- package/src/services/accounts/machine-group-management.ts +2 -1
- package/src/services/accounts/posix.ts +13 -12
- package/src/services/accounts/request-policy.ts +2 -1
- package/src/services/accounts/switching.ts +1 -1
- package/src/services/accounts/users.ts +2 -1
- package/src/services/announcements/index.ts +1 -1
- package/src/services/app-access-management.ts +3 -9
- package/src/services/app-access.ts +2 -1
- package/src/services/app-admin-facts.ts +160 -0
- package/src/services/app-approval.ts +34 -30
- package/src/services/app-bootstrap.ts +217 -0
- package/src/services/app-details.ts +3 -2
- package/src/services/app-domain-check.ts +152 -0
- package/src/services/app-domains.ts +172 -0
- package/src/services/app-events/accept.ts +5 -5
- package/src/services/app-events/admin.ts +15 -15
- package/src/services/app-events/declarations.ts +6 -3
- package/src/services/app-events/deliver.ts +6 -6
- package/src/services/app-events.ts +3 -2
- package/src/services/app-keys.ts +45 -11
- package/src/services/approvals/content.ts +1 -1
- package/src/services/approvals/index.ts +2 -1
- package/src/services/approvals/list.ts +1 -1
- package/src/services/approvals/migrate.ts +1 -1
- package/src/services/audit/index.ts +2 -1
- package/src/services/auth-flows/ipa.ts +1 -1
- package/src/services/auth-flows/magic-link.ts +1 -1
- package/src/services/auth-flows/password-reset.ts +1 -1
- package/src/services/auth-flows/proxy-return.ts +1 -1
- package/src/services/downstream-budget.ts +2 -2
- package/src/services/geo/admin.ts +13 -12
- package/src/services/geo/identity.ts +46 -0
- package/src/services/geo/ledger.ts +118 -106
- package/src/services/geo/migrate.ts +2 -1
- package/src/services/geo/route-chain.ts +3 -3
- package/src/services/geo/service.ts +4 -24
- package/src/services/geoip/index.ts +5 -5
- package/src/services/geoip/store.ts +24 -21
- package/src/services/help/index.ts +2 -1
- package/src/services/help/store.ts +2 -1
- package/src/services/identity/app-request.ts +44 -0
- package/src/services/identity/constants.ts +1 -0
- package/src/services/identity/index.ts +1 -0
- package/src/services/identity/invocation-actor.ts +2 -1
- package/src/services/identity/invocation-token.ts +112 -0
- package/src/services/identity/key-ring.ts +2 -2
- package/src/services/identity/resource-readers.ts +1 -1
- package/src/services/identity/routes.ts +11 -3
- package/src/services/identity/runtime-config.ts +14 -3
- package/src/services/identity/workload-auth.ts +2 -0
- package/src/services/identity/workload-runtime.ts +3 -3
- package/src/services/index.ts +4 -0
- package/src/services/ipa/groups.ts +1 -1
- package/src/services/ipa/profile.ts +1 -1
- package/src/services/ipa/search.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +1 -1
- package/src/services/known-browsers.ts +2 -1
- package/src/services/legal-consent.ts +1 -1
- package/src/services/links/index.ts +16 -0
- package/src/services/links/runtime.ts +44 -0
- package/src/services/links/store.ts +105 -0
- package/src/services/logging/index.ts +13 -8
- package/src/services/logging/redaction.ts +6 -1
- package/src/services/logging/trace.ts +9 -9
- package/src/services/logging/web-vitals.ts +1 -1
- package/src/services/mandates/index.ts +2 -1
- package/src/services/notifications/batches.ts +1 -1
- package/src/services/notifications/browser.ts +1 -1
- package/src/services/notifications/catalog.ts +1 -1
- package/src/services/notifications/dispatcher.ts +1 -1
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/observability.ts +2 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/quiet.ts +1 -1
- package/src/services/notifications/user.ts +1 -1
- package/src/services/oauth-tokens.ts +1 -1
- package/src/services/outbox.ts +15 -14
- package/src/services/outgoing-mail/admin.ts +1 -1
- package/src/services/outgoing-mail/dispatcher.ts +1 -1
- package/src/services/outgoing-mail/drain.ts +2 -1
- package/src/services/outgoing-mail/enqueue.ts +2 -1
- package/src/services/outgoing-mail/messages.ts +2 -1
- package/src/services/outgoing-mail/retention.ts +1 -1
- package/src/services/outgoing-mail/send.ts +1 -1
- package/src/services/outgoing-mail/store.ts +2 -1
- package/src/services/panel-context-consent.ts +32 -28
- package/src/services/providers/local/auth.ts +1 -1
- package/src/services/providers/local/users.ts +1 -1
- package/src/services/pwa-devices.ts +2 -1
- package/src/services/rail-preferences.ts +16 -12
- package/src/services/rail-shortcuts.ts +12 -10
- package/src/services/request-cache-redis.ts +3 -3
- package/src/services/service-account-credentials.ts +2 -1
- package/src/services/service-accounts.ts +2 -1
- package/src/services/session/index.ts +2 -1
- package/src/services/session/user.ts +2 -1
- package/src/services/settings/core-settings.ts +20 -0
- package/src/services/settings/crypto.ts +2 -2
- package/src/services/settings/index.ts +1 -1
- package/src/services/settings/store.ts +6 -1
- package/src/services/settings/versions.ts +2 -1
- package/src/services/weather/forecast.ts +2 -1
- package/src/services/weather/locations.ts +1 -1
- package/src/services/weather/migrate.ts +2 -1
- package/src/services/weather/short-id.ts +2 -1
- package/src/services/webauthn.ts +1 -1
- package/src/ssr/admin-navigation.ts +0 -1
- package/src/ssr/compact-shell.browser-harness.ts +2 -0
- package/src/ssr/platform-messages.ts +0 -2
- package/src/start.ts +35 -14
- package/src/workflows/ai/store.ts +2 -1
- package/src/workflows/store/budget.ts +2 -1
- package/src/workflows/store/definitions.ts +2 -1
- package/src/workflows/store/events.ts +2 -1
- package/src/workflows/store/observability.ts +2 -1
- package/src/workflows/store/retention.ts +2 -1
- package/src/workflows/store/runs.ts +2 -1
- package/src/workflows/store/transaction.ts +2 -1
- 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
|
-
|
|
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 {
|
|
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, {
|
package/src/services/app-keys.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
/**
|
|
272
|
-
export const
|
|
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
|
|
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 {
|
|
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,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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?.() ??
|
|
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 {
|
|
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
|
+
};
|