@k2b/cloud 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- 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/capabilities.ts +17 -2
- package/src/_internal/define-app.ts +2 -0
- 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-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 +23 -1
- 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 +79 -5
- package/src/api/index.ts +2 -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/server-invoke.ts +45 -18
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/app-keys.ts +25 -3
- package/src/config/env.ts +4 -4
- package/src/contracts/capabilities.ts +15 -2
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/events/app-event-delivery.ts +1 -1
- package/src/events/live-delivery.ts +2 -1
- package/src/runtime/bootstrap-protocol.ts +11 -4
- package/src/runtime/clients.ts +6 -2
- package/src/server/index.ts +1 -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 +2 -1
- package/src/services/app-access.ts +2 -1
- package/src/services/app-admin-facts.ts +11 -3
- package/src/services/app-approval.ts +34 -30
- package/src/services/app-bootstrap.ts +104 -21
- package/src/services/app-details.ts +3 -2
- 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 +42 -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/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 +1 -1
- 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/logging/index.ts +6 -3
- package/src/services/logging/trace.ts +1 -1
- 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/index.ts +1 -1
- package/src/services/settings/store.ts +1 -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/shared-text/documents.ts +2 -2
- package/src/ssr/compact-shell.browser-harness.ts +2 -0
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import { decodeJwt, decodeProtectedHeader, type JWTPayload, jwtVerify } from "jose";
|
|
3
|
-
import { importAppPublicKey } from "../app-key";
|
|
3
|
+
import { importAppPublicKey, validateAppKeyAppId } from "../app-key";
|
|
4
4
|
import {
|
|
5
5
|
BOOTSTRAP_PATH,
|
|
6
6
|
BOOTSTRAP_PROOF_BYTES,
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
BOOTSTRAP_REPLAY_RETENTION_SECONDS,
|
|
9
9
|
BOOTSTRAP_TYPE,
|
|
10
10
|
type BootstrapIdentity,
|
|
11
|
+
BootstrapIdentitySchema,
|
|
11
12
|
bootstrapBodyDigest,
|
|
12
13
|
} from "../runtime/bootstrap-protocol";
|
|
13
|
-
import {
|
|
14
|
+
import { sql } from "../runtime/clients";
|
|
15
|
+
import { getActiveAppKey, getInstallationId, getStoredAppKey, type LastRejectedStart } from "./app-keys";
|
|
14
16
|
import { get } from "./settings";
|
|
15
17
|
|
|
16
18
|
export class BootstrapProofError extends Error {
|
|
@@ -19,8 +21,34 @@ export class BootstrapProofError extends Error {
|
|
|
19
21
|
this.name = "BootstrapProofError";
|
|
20
22
|
}
|
|
21
23
|
}
|
|
24
|
+
/** Evidence attached only after signature and every request claim have been verified. */
|
|
25
|
+
export class RejectedBootstrapStart extends BootstrapProofError {
|
|
26
|
+
constructor(
|
|
27
|
+
readonly identity: BootstrapIdentity,
|
|
28
|
+
readonly jti: string,
|
|
29
|
+
readonly reason: LastRejectedStart["reason"],
|
|
30
|
+
) {
|
|
31
|
+
super(reason === "connections_not_permitted" ? reason : "invalid_proof");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
const invalid = () => new BootstrapProofError("invalid_proof");
|
|
23
35
|
|
|
36
|
+
/** A refused proof is never consumed. Already-consumed proofs cannot change administrator state. */
|
|
37
|
+
export const recordRejectedBootstrapStart = async (rejection: RejectedBootstrapStart, db: SQL = sql): Promise<void> => {
|
|
38
|
+
const { identity, jti, reason } = rejection;
|
|
39
|
+
await db`
|
|
40
|
+
INSERT INTO auth.app_start_rejections (app_id, rejected_at, reason, key_id)
|
|
41
|
+
SELECT ${identity.appId}, clock_timestamp(), ${reason}, ${identity.keyId}::uuid
|
|
42
|
+
WHERE NOT EXISTS (SELECT 1 FROM auth.app_proof_replays WHERE key_id = ${identity.keyId}::uuid AND jti = ${jti})
|
|
43
|
+
ON CONFLICT (app_id) DO UPDATE
|
|
44
|
+
SET rejected_at = EXCLUDED.rejected_at, reason = EXCLUDED.reason, key_id = EXCLUDED.key_id
|
|
45
|
+
`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const clearRejectedBootstrapStart = async (appId: string, db: SQL = sql): Promise<void> => {
|
|
49
|
+
await db`DELETE FROM auth.app_start_rejections WHERE app_id = ${appId}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
24
52
|
/** Shared Postgres uniqueness rejects a proof used concurrently on different Core replicas. */
|
|
25
53
|
export const consumeBootstrapProof = async (keyId: string, jti: string, exp: number, db: SQL = sql): Promise<boolean> => {
|
|
26
54
|
const rows = await db`
|
|
@@ -72,14 +100,10 @@ export const cleanupBootstrapProofs = async (
|
|
|
72
100
|
return { deleted, remaining: backlog?.count ?? 0 };
|
|
73
101
|
};
|
|
74
102
|
|
|
75
|
-
/**
|
|
76
|
-
export const
|
|
77
|
-
proof
|
|
78
|
-
|
|
79
|
-
db: SQL = sql,
|
|
80
|
-
now = Date.now(),
|
|
81
|
-
): Promise<{ identity: BootstrapIdentity; jti: string; exp: number }> => {
|
|
82
|
-
if (Buffer.byteLength(proof) > BOOTSTRAP_PROOF_BYTES) throw invalid();
|
|
103
|
+
/** Reject malformed or expired envelopes before allocating or reading a signed request body. Not authentication. */
|
|
104
|
+
export const inspectAppProof = (proof: string, now = Date.now()): { appId: string; keyId: string } => {
|
|
105
|
+
if (Buffer.byteLength(proof) > BOOTSTRAP_PROOF_BYTES || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{86}$/.test(proof))
|
|
106
|
+
throw invalid();
|
|
83
107
|
let appId: string;
|
|
84
108
|
let keyId: string;
|
|
85
109
|
try {
|
|
@@ -87,14 +111,47 @@ export const authenticateBootstrap = async (
|
|
|
87
111
|
const claims = decodeJwt(proof);
|
|
88
112
|
if (header.alg !== "EdDSA" || header.typ !== BOOTSTRAP_TYPE || typeof header.kid !== "string" || typeof claims.iss !== "string")
|
|
89
113
|
throw invalid();
|
|
114
|
+
validateAppKeyAppId(claims.iss);
|
|
115
|
+
if (!BootstrapIdentitySchema.shape.keyId.safeParse(header.kid).success) throw invalid();
|
|
116
|
+
const seconds = Math.floor(now / 1000);
|
|
117
|
+
if (
|
|
118
|
+
!Number.isSafeInteger(claims.iat) ||
|
|
119
|
+
!Number.isSafeInteger(claims.exp) ||
|
|
120
|
+
claims.iat! > seconds + 2 ||
|
|
121
|
+
claims.exp! <= seconds ||
|
|
122
|
+
claims.exp! <= claims.iat! ||
|
|
123
|
+
claims.exp! - claims.iat! > BOOTSTRAP_PROOF_SECONDS
|
|
124
|
+
)
|
|
125
|
+
throw invalid();
|
|
90
126
|
appId = claims.iss;
|
|
91
127
|
keyId = header.kid;
|
|
92
128
|
} catch {
|
|
93
129
|
throw invalid();
|
|
94
130
|
}
|
|
131
|
+
return { appId, keyId };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** No caches: every HTTP transmission rechecks revocation, expiry and account state in Postgres. */
|
|
135
|
+
export const authenticateAppProof = async (
|
|
136
|
+
proof: string,
|
|
137
|
+
request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
|
|
138
|
+
db: SQL = sql,
|
|
139
|
+
now = Date.now(),
|
|
140
|
+
): Promise<{ identity: BootstrapIdentity; key: NonNullable<Awaited<ReturnType<typeof getActiveAppKey>>>; jti: string; exp: number }> => {
|
|
141
|
+
const { appId, keyId } = inspectAppProof(proof, now);
|
|
95
142
|
// Only validated IDs reach SQL, and the app identity must own this active credential.
|
|
96
143
|
const key = await getActiveAppKey({ appId, keyId }, db);
|
|
97
144
|
if (!key) throw invalid();
|
|
145
|
+
return verifyAppProof(proof, request, key, db, now);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const verifyAppProof = async (
|
|
149
|
+
proof: string,
|
|
150
|
+
request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
|
|
151
|
+
key: NonNullable<Awaited<ReturnType<typeof getStoredAppKey>>>,
|
|
152
|
+
db: SQL,
|
|
153
|
+
now: number,
|
|
154
|
+
) => {
|
|
98
155
|
let claims: JWTPayload;
|
|
99
156
|
try {
|
|
100
157
|
const verified = await jwtVerify(proof, await importAppPublicKey(key.publicKey), {
|
|
@@ -107,19 +164,11 @@ export const authenticateBootstrap = async (
|
|
|
107
164
|
} catch {
|
|
108
165
|
throw invalid();
|
|
109
166
|
}
|
|
110
|
-
const seconds = Math.floor(now / 1000);
|
|
111
167
|
if (
|
|
112
|
-
!Number.isSafeInteger(claims.iat) ||
|
|
113
|
-
!Number.isSafeInteger(claims.exp) ||
|
|
114
|
-
claims.iat! > seconds + 2 ||
|
|
115
|
-
claims.exp! <= claims.iat! ||
|
|
116
|
-
claims.exp! - claims.iat! > BOOTSTRAP_PROOF_SECONDS ||
|
|
117
168
|
typeof claims.jti !== "string" ||
|
|
118
169
|
claims.jti.length === 0 ||
|
|
119
170
|
claims.jti.length > 64 ||
|
|
120
171
|
claims.jti.includes("\0") ||
|
|
121
|
-
request.method !== "POST" ||
|
|
122
|
-
request.path !== BOOTSTRAP_PATH ||
|
|
123
172
|
claims.htm !== request.method ||
|
|
124
173
|
claims.htu !== request.path ||
|
|
125
174
|
claims.bdh !== (await bootstrapBodyDigest(request.body))
|
|
@@ -128,7 +177,41 @@ export const authenticateBootstrap = async (
|
|
|
128
177
|
if (claims.iss !== key.appId || claims.sub !== key.appId) throw new BootstrapProofError("wrong_app");
|
|
129
178
|
const installationId = await getInstallationId(db);
|
|
130
179
|
if (claims.aud !== `cloud-core:${installationId}`) throw new BootstrapProofError("wrong_installation");
|
|
131
|
-
if (!key.connectionsAllowed) throw new BootstrapProofError("connections_not_permitted");
|
|
132
180
|
// Admission must succeed before the route atomically consumes this verified nonce.
|
|
133
|
-
return { identity: { installationId, appId, keyId }, jti: claims.jti, exp: claims.exp! };
|
|
181
|
+
return { identity: { installationId, appId: key.appId, keyId: key.keyId }, key, jti: claims.jti, exp: claims.exp! };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Only connection delivery requires connections_allowed; runners can still sign requests. */
|
|
185
|
+
export const authenticateBootstrap = async (
|
|
186
|
+
proof: string,
|
|
187
|
+
request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
|
|
188
|
+
db: SQL = sql,
|
|
189
|
+
now = Date.now(),
|
|
190
|
+
): Promise<{ identity: BootstrapIdentity; jti: string; exp: number }> => {
|
|
191
|
+
const ids = inspectAppProof(proof, now);
|
|
192
|
+
const key = await getStoredAppKey(ids, db);
|
|
193
|
+
if (!key) throw invalid();
|
|
194
|
+
const inactive = key.status !== "active" || key.accountStatus !== "active";
|
|
195
|
+
let verified: Awaited<ReturnType<typeof verifyAppProof>>;
|
|
196
|
+
try {
|
|
197
|
+
verified = await verifyAppProof(proof, request, key, db, now);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// Inactive keys have always returned invalid_proof, regardless of their claims.
|
|
200
|
+
if (inactive) throw invalid();
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (request.method !== "POST" || request.path !== BOOTSTRAP_PATH) throw invalid();
|
|
204
|
+
const reason =
|
|
205
|
+
key.status === "revoked"
|
|
206
|
+
? "revoked"
|
|
207
|
+
: key.status === "expired"
|
|
208
|
+
? "expired"
|
|
209
|
+
: key.accountStatus === "disabled"
|
|
210
|
+
? "disabled"
|
|
211
|
+
: !key.connectionsAllowed
|
|
212
|
+
? "connections_not_permitted"
|
|
213
|
+
: null;
|
|
214
|
+
if (reason) throw new RejectedBootstrapStart(verified.identity, verified.jti, reason);
|
|
215
|
+
const { identity, jti, exp } = verified;
|
|
216
|
+
return { identity, jti, exp };
|
|
134
217
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
3
3
|
import { getApp, listCapabilities } from "../_internal/registry";
|
|
4
4
|
import {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../contracts/app-details";
|
|
10
10
|
import { type NotificationDefinitionPresentationCatalog, resolveNotificationDefinitionPresentation } from "../contracts/notification-types";
|
|
11
11
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
12
|
+
import { sql } from "../runtime/clients";
|
|
12
13
|
import type { RequestAuthority } from "../server/middleware/auth";
|
|
13
14
|
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
14
15
|
import { AppAccessManagementError, appAccessGrantCounts, canRepairAppAccess } from "./app-access-management";
|
|
@@ -48,8 +49,8 @@ type Dependencies = {
|
|
|
48
49
|
|
|
49
50
|
/** Core-owned administration read shared by SSR and HTTP; app access is not administrator authority. */
|
|
50
51
|
export function createAppDetailsService(dependencies: Dependencies = {}) {
|
|
51
|
-
const db = dependencies.db ?? sql;
|
|
52
52
|
return async (appId: string, options: { authority: RequestAuthority; locale?: string }): Promise<AppDetails | null> => {
|
|
53
|
+
const db = dependencies.db ?? sql;
|
|
53
54
|
if (!canRepairAppAccess(options.authority))
|
|
54
55
|
throw new AppAccessManagementError("FORBIDDEN", "Cloud administration authority is required.");
|
|
55
56
|
const locale = options.locale ?? "en";
|
|
@@ -15,8 +15,8 @@ const Metadata = z.object({
|
|
|
15
15
|
version: Envelope.shape.version.catch(1),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
|
|
19
|
-
const markSeen = async (sequence: number, tx: SQL = db) => {
|
|
18
|
+
export function createEventAcceptance(db: () => SQL, reconcile: () => Promise<void>) {
|
|
19
|
+
const markSeen = async (sequence: number, tx: SQL = db()) => {
|
|
20
20
|
// Keep holes until replay or the periodic DLQ/retention check accounts for them.
|
|
21
21
|
// Advancing the high watermark alone would hide eviction before the next tick.
|
|
22
22
|
await tx`
|
|
@@ -36,7 +36,7 @@ export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
const { id, type, source, version } = metadata.data;
|
|
39
|
-
await db.begin(async (tx) => {
|
|
39
|
+
await db().begin(async (tx) => {
|
|
40
40
|
// Invalid or oversized input is represented by metadata only, never a second failing payload insert.
|
|
41
41
|
await tx`
|
|
42
42
|
INSERT INTO events.app_events(id, type, version, source, subject_key, envelope, state, code)
|
|
@@ -61,7 +61,7 @@ export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
|
|
|
61
61
|
} catch {
|
|
62
62
|
log.warn("Declaration refresh failed; accepting against persisted declarations", { sequence });
|
|
63
63
|
}
|
|
64
|
-
const [declaration] = await db<StoredDeclaration[]>`
|
|
64
|
+
const [declaration] = await db()<StoredDeclaration[]>`
|
|
65
65
|
SELECT * FROM events.app_declarations
|
|
66
66
|
WHERE app_id = ${envelope.source} AND type = ${envelope.type} AND version = ${envelope.version}
|
|
67
67
|
`;
|
|
@@ -85,7 +85,7 @@ export function createEventAcceptance(db: SQL, reconcile: () => Promise<void>) {
|
|
|
85
85
|
}
|
|
86
86
|
const subjectKey = `${envelope.subject.type}:${envelope.subject.id}`;
|
|
87
87
|
try {
|
|
88
|
-
await db.begin(async (tx) => {
|
|
88
|
+
await db().begin(async (tx) => {
|
|
89
89
|
const inserted = await tx`
|
|
90
90
|
INSERT INTO events.app_events(id, type, version, source, subject_key, envelope, state, code)
|
|
91
91
|
VALUES (${envelope.id}::uuid, ${envelope.type}, ${envelope.version}, ${envelope.source}, ${subjectKey},
|
|
@@ -13,12 +13,12 @@ type Dependencies = {
|
|
|
13
13
|
deadLetters: (after?: string) => Promise<TopicDeadLetter<AppEventEnvelope>[]>;
|
|
14
14
|
streams: () => AsyncIterable<Pick<TopicInventoryEntry, "id" | "firstSequence">>;
|
|
15
15
|
};
|
|
16
|
-
export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
16
|
+
export function createEventAdministration(db: () => SQL, dependencies: Dependencies) {
|
|
17
17
|
const catalog = async (locale = "en"): Promise<{ events: CapabilityEventCatalogEntry[] }> => {
|
|
18
18
|
const [entries, declarations, storedSubscriptions] = await Promise.all([
|
|
19
19
|
dependencies.registry(),
|
|
20
|
-
db<StoredDeclaration[]>`SELECT * FROM events.app_declarations WHERE retired_at IS NULL ORDER BY type, version`,
|
|
21
|
-
db<
|
|
20
|
+
db()<StoredDeclaration[]>`SELECT * FROM events.app_declarations WHERE retired_at IS NULL ORDER BY type, version`,
|
|
21
|
+
db()<
|
|
22
22
|
{
|
|
23
23
|
appId: string;
|
|
24
24
|
subscriptionId: string;
|
|
@@ -62,7 +62,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
62
62
|
};
|
|
63
63
|
const subscriptions = async (app?: string) => {
|
|
64
64
|
return {
|
|
65
|
-
items: await db`
|
|
65
|
+
items: await db()`
|
|
66
66
|
SELECT subscription.id, subscription.app_id AS "appId", subscription.event_type AS "eventType", subscription.version,
|
|
67
67
|
subscription.state, subscription.reason, subscription.first_seen_at AS "firstSeenAt",
|
|
68
68
|
(SELECT jsonb_object_agg(state, count) FROM (
|
|
@@ -77,7 +77,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
77
77
|
const deliveries = async (filter: { app?: string; state?: string; eventType?: string; offset?: number; limit?: number } = {}) => {
|
|
78
78
|
const limit = Math.max(1, Math.min(100, filter.limit ?? 50));
|
|
79
79
|
const offset = Math.max(0, filter.offset ?? 0);
|
|
80
|
-
const rows = await db`
|
|
80
|
+
const rows = await db()`
|
|
81
81
|
SELECT delivery.id, delivery.event_id AS "eventId", delivery.subscription_id AS "subscriptionId",
|
|
82
82
|
subscription.app_id AS "appId", event.type AS "eventType", delivery.state, delivery.code, delivery.attempts,
|
|
83
83
|
delivery.last_error AS "lastError", delivery.last_attempt_at AS "lastAttemptAt",
|
|
@@ -104,7 +104,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
104
104
|
};
|
|
105
105
|
};
|
|
106
106
|
const redeliver = async (id: string, actor: AuditActor) =>
|
|
107
|
-
db.begin(async (tx) => {
|
|
107
|
+
db().begin(async (tx) => {
|
|
108
108
|
const changed = await tx`
|
|
109
109
|
UPDATE events.app_deliveries delivery SET state = 'pending', code = NULL, last_error = NULL,
|
|
110
110
|
attempts = 0, unreachable_count = 0, retained_from = now(), next_attempt_at = now(), claimed_until = NULL, claim_token = NULL, updated_at = now()
|
|
@@ -129,12 +129,12 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
const status = async () => {
|
|
132
|
-
const [stored] = await db`
|
|
132
|
+
const [stored] = await db()`
|
|
133
133
|
SELECT state, code, last_sequence AS "lastSequence", gap_from AS "gapFrom", gap_to AS "gapTo", updated_at AS "updatedAt"
|
|
134
134
|
FROM events.app_broker_status WHERE id = true
|
|
135
135
|
`;
|
|
136
|
-
const [rejected] = await db`SELECT count(*)::int AS count FROM events.app_events WHERE state = 'rejected'`;
|
|
137
|
-
const rejections = await db`
|
|
136
|
+
const [rejected] = await db()`SELECT count(*)::int AS count FROM events.app_events WHERE state = 'rejected'`;
|
|
137
|
+
const rejections = await db()`
|
|
138
138
|
SELECT type, source, code, received_at AS time FROM events.app_events
|
|
139
139
|
WHERE state = 'rejected' ORDER BY seq DESC LIMIT 10
|
|
140
140
|
`;
|
|
@@ -156,7 +156,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
156
156
|
};
|
|
157
157
|
};
|
|
158
158
|
const checkGap = async () => {
|
|
159
|
-
const [stored] = await db<{ last_sequence: string }[]>`
|
|
159
|
+
const [stored] = await db()<{ last_sequence: string }[]>`
|
|
160
160
|
SELECT last_sequence FROM events.app_broker_status WHERE id = true
|
|
161
161
|
`;
|
|
162
162
|
let position = Number(stored?.last_sequence ?? 0);
|
|
@@ -175,7 +175,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
175
175
|
after = next;
|
|
176
176
|
}
|
|
177
177
|
if (seen.size) {
|
|
178
|
-
await db`
|
|
178
|
+
await db()`
|
|
179
179
|
UPDATE events.app_broker_status SET unseen_sequences = unseen_sequences - (
|
|
180
180
|
SELECT range_agg(int8range(sequence, sequence + 1, '[)'))
|
|
181
181
|
FROM unnest(${toPgTextArray([...seen].map(String))}::bigint[]) AS sequence
|
|
@@ -183,13 +183,13 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
183
183
|
`;
|
|
184
184
|
}
|
|
185
185
|
while (seen.has(position + 1)) position++;
|
|
186
|
-
await db`
|
|
186
|
+
await db()`
|
|
187
187
|
UPDATE events.app_broker_status SET last_sequence = ${position}, updated_at = now()
|
|
188
188
|
WHERE id = true AND last_sequence < ${position}
|
|
189
189
|
`;
|
|
190
190
|
for await (const stream of dependencies.streams()) {
|
|
191
191
|
if (stream.id !== "cloud:app-events") continue;
|
|
192
|
-
await db`
|
|
192
|
+
await db()`
|
|
193
193
|
UPDATE events.app_broker_status SET state = 'gap_detected', code = 'reconcile_required',
|
|
194
194
|
gap_from = COALESCE(gap_from, LEAST(lower(unseen_sequences), last_sequence + 1)), gap_to = ${stream.firstSequence - 1}, updated_at = now()
|
|
195
195
|
WHERE id = true AND LEAST(lower(unseen_sequences), last_sequence + 1) < ${stream.firstSequence}
|
|
@@ -199,7 +199,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
199
199
|
}
|
|
200
200
|
};
|
|
201
201
|
const acknowledgeGap = async (actor: AuditActor) =>
|
|
202
|
-
db.begin(async (tx) => {
|
|
202
|
+
db().begin(async (tx) => {
|
|
203
203
|
const [previous] = await tx`
|
|
204
204
|
SELECT state, gap_from, gap_to FROM events.app_broker_status WHERE id = true FOR UPDATE
|
|
205
205
|
`;
|
|
@@ -223,7 +223,7 @@ export function createEventAdministration(db: SQL, dependencies: Dependencies) {
|
|
|
223
223
|
return result;
|
|
224
224
|
});
|
|
225
225
|
const retain = async () =>
|
|
226
|
-
db.begin(async (tx) => {
|
|
226
|
+
db().begin(async (tx) => {
|
|
227
227
|
await tx`
|
|
228
228
|
UPDATE events.app_deliveries SET state = 'cancelled', code = 'retention_expired',
|
|
229
229
|
claimed_until = NULL, claim_token = NULL, updated_at = now()
|
|
@@ -34,13 +34,14 @@ async function auditTransitions(tx: SQL, transitions: Transition[]) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function createDeclarationSync(db: SQL, registry: () => Promise<CapabilityRegistryEntry[]>) {
|
|
37
|
+
export function createDeclarationSync(db: () => SQL, registry: () => Promise<CapabilityRegistryEntry[]>) {
|
|
38
38
|
let previousSnapshot: string | undefined;
|
|
39
|
+
let previousDatabase: SQL | undefined;
|
|
39
40
|
let lastCheckedAt = -Infinity;
|
|
40
41
|
let running: Promise<void> | undefined;
|
|
41
42
|
|
|
42
43
|
async function sync(entries: CapabilityRegistryEntry[]) {
|
|
43
|
-
await db.begin(async (tx) => {
|
|
44
|
+
await db().begin(async (tx) => {
|
|
44
45
|
for (const entry of entries.toSorted((left, right) => left.appId.localeCompare(right.appId))) {
|
|
45
46
|
const hash = capabilityHash({ events: entry.manifest.events ?? [], subscribes: entry.manifest.subscribes ?? [] });
|
|
46
47
|
const [known] = await tx`SELECT hash FROM events.app_manifests WHERE app_id = ${entry.appId}`;
|
|
@@ -173,11 +174,13 @@ export function createDeclarationSync(db: SQL, registry: () => Promise<Capabilit
|
|
|
173
174
|
}))
|
|
174
175
|
.sort((left, right) => left.appId.localeCompare(right.appId)),
|
|
175
176
|
);
|
|
176
|
-
|
|
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,11 +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 (
|
|
273
299
|
params: { appId: string; keyId: string },
|
|
274
300
|
db: SQL = sql,
|
|
275
|
-
): Promise<(AppKeyMetadata & { connectionsAllowed: boolean }) | null> => {
|
|
301
|
+
): Promise<(AppKeyMetadata & { connectionsAllowed: boolean; oauthIssueAllowed: boolean }) | null> => {
|
|
276
302
|
try {
|
|
277
303
|
validateAppKeyAppId(params.appId);
|
|
278
304
|
} catch {
|
|
@@ -283,11 +309,16 @@ export const getActiveAppKey = async (
|
|
|
283
309
|
const [row] = await db<(Row & { connections_allowed: boolean })[]>`
|
|
284
310
|
SELECT ${db.unsafe(columns)} FROM auth.service_account_credentials c
|
|
285
311
|
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
286
|
-
WHERE c.kind = 'app_key' AND c.id = ${params.keyId}::uuid
|
|
287
|
-
AND (c.expires_at IS NULL OR c.expires_at > now()) AND sa.status = 'active'
|
|
312
|
+
WHERE c.kind = 'app_key' AND c.id = ${params.keyId}::uuid
|
|
288
313
|
AND sa.kind = 'resource_bound' AND sa.app_id = ${params.appId} AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${params.appId}
|
|
289
314
|
`;
|
|
290
|
-
return row ? { ...map(row), connectionsAllowed: row.connections_allowed } : null;
|
|
315
|
+
return row ? { ...map(row), connectionsAllowed: row.connections_allowed, oauthIssueAllowed: row.oauth_issue_allowed } : null;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/** Bootstrap and signed workload requests must query this directly; no cache. */
|
|
319
|
+
export const getActiveAppKey = async (params: { appId: string; keyId: string }, db: SQL = sql) => {
|
|
320
|
+
const key = await getStoredAppKey(params, db);
|
|
321
|
+
return key?.status === "active" && key.accountStatus === "active" ? key : null;
|
|
291
322
|
};
|
|
292
323
|
|
|
293
324
|
/** Call only after successful authentication; a late touch cannot revive a key. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { parseStoredReviewContent } from "../../capabilities/review-storage";
|
|
3
2
|
import { CAPABILITY_MAX_REVIEW_CONTENT_BYTES } from "../../contracts/capabilities";
|
|
3
|
+
import { sql } from "../../runtime/clients";
|
|
4
4
|
|
|
5
5
|
// Start with this person's indexed pending rows; a Studio page alone cannot continue a chat.
|
|
6
6
|
export const openRequests = (approverUserId: string, id?: string) => sql`SELECT r.* FROM approvals.requests r
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
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";
|