@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,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQLQuery } from "bun";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { DEFAULT_LINUX_IDENTITY_CONFIGURATION, LinuxIdentityConfigurationSchema } from "../../contracts/posix";
|
|
4
4
|
import type { RequestActor, UserProfile, UserProvider } from "../../contracts/shared";
|
|
5
|
+
import { sql } from "../../runtime/clients";
|
|
5
6
|
import { userFromActor } from "../../server/actor";
|
|
6
7
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
7
8
|
import { toPgTextArray } from "../postgres";
|
|
@@ -66,9 +67,10 @@ const cursor = (after?: string): string | null => {
|
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
/** Server-side identity reads. Authentication and application resource authorization remain caller-owned. */
|
|
69
|
-
export const createAccountIdentityService = (
|
|
70
|
+
export const createAccountIdentityService = (injectedDb?: typeof sql, upstreamIdentity = readUpstreamIdentity) => {
|
|
71
|
+
const db = () => injectedDb ?? sql;
|
|
70
72
|
const setting = async (key: string, fallback: unknown): Promise<unknown> => {
|
|
71
|
-
const [row] = await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
|
|
73
|
+
const [row] = await db()<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
|
|
72
74
|
return row ? decryptValue(row.value) : fallback;
|
|
73
75
|
};
|
|
74
76
|
const availability = async (): Promise<AccountIdentityAvailability> => {
|
|
@@ -84,7 +86,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
84
86
|
throw new AccountIdentityError("invalid_identity_configuration");
|
|
85
87
|
}
|
|
86
88
|
};
|
|
87
|
-
const userRows = (filter: SQLQuery) => db<UserRow[]>`
|
|
89
|
+
const userRows = (filter: SQLQuery) => db()<UserRow[]>`
|
|
88
90
|
SELECT u.id, u.provider, u.uid, u.profile, u.admin,
|
|
89
91
|
(u.account_expires IS NOT NULL AND u.account_expires <= now()) AS expired,
|
|
90
92
|
p.managed_by, p.uid_number, p.primary_gid_number
|
|
@@ -98,7 +100,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
98
100
|
const [row] = await userRows(sql`u.id = ${supplied.id}::uuid`);
|
|
99
101
|
if (!row || row.expired || row.provider !== supplied.provider || row.uid !== supplied.uid || row.profile !== supplied.profile)
|
|
100
102
|
throw new AccountIdentityError("identity_unavailable");
|
|
101
|
-
if (!(await isAccountCategoryAllowed(row, db))) throw new AccountIdentityError("identity_unavailable");
|
|
103
|
+
if (!(await isAccountCategoryAllowed(row, db()))) throw new AccountIdentityError("identity_unavailable");
|
|
102
104
|
const state = await availability();
|
|
103
105
|
if (row.provider === "ipa" && !state.freeipaEnabled) throw new AccountIdentityError("identity_unavailable");
|
|
104
106
|
return { row, state, supplied };
|
|
@@ -113,14 +115,14 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
113
115
|
if (!Array.isArray(configured) || !configured.every((value): value is string => typeof value === "string"))
|
|
114
116
|
throw new AccountIdentityError("invalid_identity_configuration");
|
|
115
117
|
const names = configured.length > 0 ? configured : ["admins"];
|
|
116
|
-
const [membership] = await db`SELECT 1 FROM auth.ipa_user_effective_groups
|
|
118
|
+
const [membership] = await db()`SELECT 1 FROM auth.ipa_user_effective_groups
|
|
117
119
|
WHERE user_id = ${current.row.id}::uuid AND group_name = ANY(${toPgTextArray(names)}::text[]) LIMIT 1`;
|
|
118
120
|
if (!membership) throw new AccountIdentityError("admin_required");
|
|
119
121
|
}
|
|
120
122
|
return current;
|
|
121
123
|
};
|
|
122
124
|
const groupRows = async (filter: SQLQuery) => {
|
|
123
|
-
const rows = await db<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
|
|
125
|
+
const rows = await db()<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
|
|
124
126
|
SELECT g.id, g.provider, g.name, g.gid_number,
|
|
125
127
|
EXISTS(SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id) AS personal
|
|
126
128
|
FROM auth.groups g
|
|
@@ -189,7 +191,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
189
191
|
eligible: current.profile === "user",
|
|
190
192
|
};
|
|
191
193
|
}
|
|
192
|
-
const [ownership] = await db<{ personal: boolean }[]>`SELECT EXISTS (
|
|
194
|
+
const [ownership] = await db()<{ personal: boolean }[]>`SELECT EXISTS (
|
|
193
195
|
SELECT 1 FROM auth.personal_groups p
|
|
194
196
|
WHERE (${input.identityId ?? null}::uuid IS NOT NULL AND p.group_id = ${input.identityId ?? null}::uuid)
|
|
195
197
|
OR (${input.identityId ?? null}::uuid IS NULL AND p.group_name = ${input.name})
|
|
@@ -238,10 +240,10 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
238
240
|
const after = cursor(input.after);
|
|
239
241
|
const state = await availability();
|
|
240
242
|
const [localAllowed, ipaAllowed] = await Promise.all([
|
|
241
|
-
isAccountCategoryAllowed({ provider: "local", profile: "user" }, db),
|
|
242
|
-
isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db),
|
|
243
|
+
isAccountCategoryAllowed({ provider: "local", profile: "user" }, db()),
|
|
244
|
+
isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db()),
|
|
243
245
|
]);
|
|
244
|
-
const rows = await db<
|
|
246
|
+
const rows = await db()<
|
|
245
247
|
{
|
|
246
248
|
id: string;
|
|
247
249
|
provider: UserProvider;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TransactionSQL } from "bun";
|
|
2
2
|
import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
|
|
3
|
+
import { sql } from "../../runtime/clients";
|
|
3
4
|
import { escapeLikePattern, isUniqueViolation } from "../postgres";
|
|
4
5
|
import { buildBaseGroup, personalOwnerJoin } from "./base-group";
|
|
5
6
|
import { recursiveServiceAccountGroupIdsSubquery } from "./group-sql";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import {
|
|
3
2
|
DEFAULT_LINUX_IDENTITY_CONFIGURATION,
|
|
4
3
|
isPosixName,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
PosixOverridesSchema,
|
|
9
8
|
} from "../../contracts/posix";
|
|
10
9
|
import type { MutationResult } from "../../contracts/shared";
|
|
10
|
+
import { sql } from "../../runtime/clients";
|
|
11
11
|
import { freeipa } from "../../server/services";
|
|
12
12
|
import { type AuditActor, audit } from "../audit";
|
|
13
13
|
import { getFreeIpaConfig } from "../freeipa-config";
|
|
@@ -128,8 +128,9 @@ const candidate = (row: IdentityRow): PosixCandidate => {
|
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
// Database injection is used by isolated integration tests; production uses the shared pool.
|
|
131
|
-
export const createPosixRuntime = (
|
|
132
|
-
const
|
|
131
|
+
export const createPosixRuntime = (injectedDb?: typeof sql, readRanges = readIpaIdRanges) => {
|
|
132
|
+
const db = () => injectedDb ?? sql;
|
|
133
|
+
const readConfig = async (connection = db()): Promise<LinuxIdentityConfiguration> => {
|
|
133
134
|
// Provisioning must not use a stale cache after an operator disables or changes the range.
|
|
134
135
|
const [row] = await connection<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${CONFIG_KEY}`;
|
|
135
136
|
try {
|
|
@@ -256,7 +257,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
256
257
|
actor?: AuditActor,
|
|
257
258
|
): Promise<MutationResult<{ id: string }>> => {
|
|
258
259
|
try {
|
|
259
|
-
return await db.begin(async (tx) => {
|
|
260
|
+
return await db().begin(async (tx) => {
|
|
260
261
|
await lock(tx);
|
|
261
262
|
const result = await write(tx);
|
|
262
263
|
if (!result.ok) return result;
|
|
@@ -316,13 +317,13 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
316
317
|
},
|
|
317
318
|
async overview(actor: Actor, after: string | null = null, filters: { search?: string; scope?: "ready" | "all" } = {}) {
|
|
318
319
|
requireAdmin(actor);
|
|
319
|
-
const result = await rows(db, null, after, filters);
|
|
320
|
+
const result = await rows(db(), null, after, filters);
|
|
320
321
|
const items = result.slice(0, POSIX_PAGE_SIZE).map(candidate);
|
|
321
322
|
return { config: await service.configuration(actor), items, nextCursor: result.length > POSIX_PAGE_SIZE ? items.at(-1)!.id : null };
|
|
322
323
|
},
|
|
323
324
|
async get(actor: Actor, id: string) {
|
|
324
325
|
requireAdmin(actor);
|
|
325
|
-
const [row] = await rows(db, id, null);
|
|
326
|
+
const [row] = await rows(db(), id, null);
|
|
326
327
|
if (!row) throw new PosixError("user_not_found", 404);
|
|
327
328
|
return { config: await readConfig(), user: candidate(row) };
|
|
328
329
|
},
|
|
@@ -331,7 +332,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
331
332
|
const parsed = LinuxIdentityConfigurationSchema.safeParse(input);
|
|
332
333
|
if (!parsed.success) throw new PosixError("invalid_configuration", 400);
|
|
333
334
|
const ranges = parsed.data.enabled ? await readRanges() : [];
|
|
334
|
-
await db.begin(async (tx) => {
|
|
335
|
+
await db().begin(async (tx) => {
|
|
335
336
|
// Same order as provisioning; settings write upgrades this lock in the same transaction.
|
|
336
337
|
await lock(tx);
|
|
337
338
|
if (options?.replaceUnreadable !== true) {
|
|
@@ -360,7 +361,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
360
361
|
},
|
|
361
362
|
async provision(actor: Actor, id: string) {
|
|
362
363
|
requireAdmin(actor);
|
|
363
|
-
return db.begin(async (tx) => {
|
|
364
|
+
return db().begin(async (tx) => {
|
|
364
365
|
await lock(tx);
|
|
365
366
|
const config = await readConfig(tx);
|
|
366
367
|
if (!config.enabled) throw new PosixError("setup_disabled");
|
|
@@ -371,7 +372,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
371
372
|
requireAdmin(actor);
|
|
372
373
|
const parsed = PosixOverridesSchema.safeParse(input);
|
|
373
374
|
if (!parsed.success) throw new PosixError("invalid_paths", 400);
|
|
374
|
-
return db.begin(async (tx) => {
|
|
375
|
+
return db().begin(async (tx) => {
|
|
375
376
|
await lock(tx);
|
|
376
377
|
const [row] = await rows(tx, id, null);
|
|
377
378
|
if (!row) throw new PosixError("user_not_found", 404);
|
|
@@ -394,7 +395,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
394
395
|
async createGroup(actor: Actor, input: { name: string; description?: string }) {
|
|
395
396
|
requireAdmin(actor);
|
|
396
397
|
const ranges = await readRanges();
|
|
397
|
-
return db.begin(async (tx) => {
|
|
398
|
+
return db().begin(async (tx) => {
|
|
398
399
|
await lock(tx);
|
|
399
400
|
const created = await createLocalGroup(input, tx);
|
|
400
401
|
if (!created.ok) throw new PosixError("group_conflict");
|
|
@@ -405,7 +406,7 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
405
406
|
async provisionGroup(actor: Actor, id: string) {
|
|
406
407
|
requireAdmin(actor);
|
|
407
408
|
const ranges = await readRanges();
|
|
408
|
-
return db.begin(async (tx) => {
|
|
409
|
+
return db().begin(async (tx) => {
|
|
409
410
|
await lock(tx);
|
|
410
411
|
return assignGroup(tx, actor, id, ranges);
|
|
411
412
|
});
|
|
@@ -413,5 +414,5 @@ export const createPosixRuntime = (db: typeof sql = sql, readRanges = readIpaIdR
|
|
|
413
414
|
};
|
|
414
415
|
return { service, writeLocalAccount };
|
|
415
416
|
};
|
|
416
|
-
export const createPosixService = (db
|
|
417
|
+
export const createPosixService = (db?: typeof sql, readRanges = readIpaIdRanges) => createPosixRuntime(db, readRanges).service;
|
|
417
418
|
export const { service: posix, writeLocalAccount } = createPosixRuntime();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import type {
|
|
3
3
|
BaseUser,
|
|
4
4
|
MutationResult,
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
UserProfile,
|
|
10
10
|
UserProvider,
|
|
11
11
|
} from "../../contracts/shared";
|
|
12
|
+
import { sql } from "../../runtime/clients";
|
|
12
13
|
import { freeipa } from "../../server/services";
|
|
13
14
|
import { createAuthLoginUrl } from "../../shared/redirect";
|
|
14
15
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import { HTTPException } from "hono/http-exception";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import type {
|
|
@@ -11,6 +10,7 @@ import type {
|
|
|
11
10
|
} from "../../contracts/announcements";
|
|
12
11
|
import { AnnouncementDisplayEntrySchema } from "../../contracts/announcements";
|
|
13
12
|
import { hasRole, type User } from "../../contracts/shared";
|
|
13
|
+
import { sql } from "../../runtime/clients";
|
|
14
14
|
import { markdown } from "../../shared/markdown";
|
|
15
15
|
import { claimCacheFill, completeCacheFill } from "../cache-fill";
|
|
16
16
|
import { logger } from "../logging";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import { managerEligibility } from "../_internal/app-access-manager-sql";
|
|
3
3
|
import type { AppMeta } from "../contracts/app";
|
|
4
4
|
import type { AppAccessDeclaration, AppAccessSnapshot } from "../contracts/app-access";
|
|
5
5
|
import { publishAppAccessChanged, wakeAppAccessChanged } from "../events/app-access";
|
|
6
|
+
import { sql } from "../runtime/clients";
|
|
6
7
|
import { isAppSessionActor, userFromActor } from "../server/actor";
|
|
7
8
|
import type { RequestAuthority } from "../server/middleware/auth";
|
|
8
9
|
import {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import type { Context } from "hono";
|
|
3
3
|
import { RESOURCE_ACCESS_TIMEOUT_MS, resourceAppAccess } from "../_internal/app-access-resources";
|
|
4
4
|
import type { AppMeta } from "../contracts/app";
|
|
5
5
|
import { AppAccessRuleSchema, type FixedAppAccessRule } from "../contracts/app-access";
|
|
6
|
+
import { sql } from "../runtime/clients";
|
|
6
7
|
import { userFromActor } from "../server/actor";
|
|
7
8
|
import { type AuthContext, auth, type RequestAuthority } from "../server/middleware/auth";
|
|
8
9
|
import { buildAccessPrincipalCondition } from "../server/services/access";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { managerEligibility } from "../_internal/app-access-manager-sql";
|
|
2
|
+
import { sql } from "../runtime/clients";
|
|
3
3
|
import type { RequestAuthority } from "../server/middleware/auth";
|
|
4
4
|
import { AppAccessManagementError, canRepairAppAccess } from "./app-access-management";
|
|
5
|
+
import type { LastRejectedStart } from "./app-keys";
|
|
5
6
|
|
|
6
7
|
export type AppAdminFacts = {
|
|
7
8
|
access: { people: number; groups: number; serviceAccounts: number; managers: number; configured: boolean } | null;
|
|
@@ -14,12 +15,14 @@ export type AppAdminFacts = {
|
|
|
14
15
|
accountStatus: "active" | "disabled" | null;
|
|
15
16
|
};
|
|
16
17
|
tokens: { active: number; total: number };
|
|
18
|
+
/** Core's last refusal of a start with a verified proof; cleared by the next successful start. */
|
|
19
|
+
lastRejectedStart: Omit<LastRejectedStart, "keyId"> | null;
|
|
17
20
|
events: { hears: number; sends: number | null; failedDeliveries: number; waitingDeliveries: number };
|
|
18
21
|
notifications: number | null;
|
|
19
22
|
firstKnownAt: string | null;
|
|
20
23
|
};
|
|
21
24
|
|
|
22
|
-
/** Core-owned administration summary: at most 500 input IDs and
|
|
25
|
+
/** Core-owned administration summary: at most 500 input IDs and five batched SQL reads. */
|
|
23
26
|
export const appAdminFacts = async (authority: RequestAuthority, appIds: readonly string[]): Promise<Map<string, AppAdminFacts>> => {
|
|
24
27
|
if (!canRepairAppAccess(authority)) throw new AppAccessManagementError("FORBIDDEN", "Cloud administration authority is required.");
|
|
25
28
|
if (appIds.length > 500) throw new AppAccessManagementError("TOO_MANY_APPS", "At most 500 application IDs may be read at once.", 400);
|
|
@@ -31,6 +34,7 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
|
|
|
31
34
|
access: null,
|
|
32
35
|
keys: { active: 0, expired: 0, revoked: 0, lastUsedAt: null, firstCreatedAt: null, accountStatus: null },
|
|
33
36
|
tokens: { active: 0, total: 0 },
|
|
37
|
+
lastRejectedStart: null,
|
|
34
38
|
events: { hears: 0, sends: 0, failedDeliveries: 0, waitingDeliveries: 0 },
|
|
35
39
|
notifications: 0,
|
|
36
40
|
firstKnownAt: null,
|
|
@@ -39,7 +43,7 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
|
|
|
39
43
|
);
|
|
40
44
|
if (!ids.length) return facts;
|
|
41
45
|
|
|
42
|
-
const [access, credentials, events, notifications] = await Promise.all([
|
|
46
|
+
const [access, credentials, events, notifications, rejections] = await Promise.all([
|
|
43
47
|
sql<
|
|
44
48
|
{
|
|
45
49
|
app_id: string;
|
|
@@ -125,6 +129,9 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
|
|
|
125
129
|
SELECT app_id, count(*)::int AS count FROM notifications.definitions
|
|
126
130
|
WHERE app_id IN ${sql(ids)} AND active = true GROUP BY app_id
|
|
127
131
|
`,
|
|
132
|
+
sql<{ app_id: string; rejected_at: Date; reason: LastRejectedStart["reason"] }[]>`
|
|
133
|
+
SELECT app_id, rejected_at, reason FROM auth.app_start_rejections WHERE app_id IN ${sql(ids)}
|
|
134
|
+
`,
|
|
128
135
|
]);
|
|
129
136
|
|
|
130
137
|
for (const row of access) {
|
|
@@ -156,5 +163,6 @@ export const appAdminFacts = async (authority: RequestAuthority, appIds: readonl
|
|
|
156
163
|
facts.get(row.app_id)!.events = { hears: row.hears, sends: row.sends, failedDeliveries: row.failed, waitingDeliveries: row.waiting };
|
|
157
164
|
}
|
|
158
165
|
for (const row of notifications) facts.get(row.app_id)!.notifications = row.count;
|
|
166
|
+
for (const row of rejections) facts.get(row.app_id)!.lastRejectedStart = { at: row.rejected_at.toISOString(), reason: row.reason };
|
|
159
167
|
return facts;
|
|
160
168
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import type { z } from "zod";
|
|
3
3
|
import { lazySync } from "../_internal/process-sync";
|
|
4
4
|
import { env } from "../config/env";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
appPairingProofMessage,
|
|
26
26
|
APP_APPROVAL_LIMITS as limits,
|
|
27
27
|
} from "../contracts/app-approval";
|
|
28
|
+
import { sql } from "../runtime/clients";
|
|
28
29
|
import { addressPrefix } from "../server/request-context/address";
|
|
29
30
|
import { publicCloudOrigin } from "../shared/app-url";
|
|
30
31
|
import { isAccountCategoryAllowed } from "./account-category-policy";
|
|
@@ -296,12 +297,13 @@ const verify = async (key: AppDevicePublicKey, message: string, signature: strin
|
|
|
296
297
|
/** One owner for production API and isolated HTTP consumers. Config is re-read
|
|
297
298
|
* per operation; SQL transactions serialize device decisions and revocation. */
|
|
298
299
|
export const createAppApprovalService = (
|
|
299
|
-
|
|
300
|
-
configuration = (requireEnabled = true) => readAppApprovalConfig(
|
|
300
|
+
injectedDb?: SQL,
|
|
301
|
+
configuration = (requireEnabled = true) => readAppApprovalConfig(injectedDb ?? sql, requireEnabled),
|
|
301
302
|
hints: AppLoginDecisionHints = syncLoginDecisionHints,
|
|
302
303
|
push: AppLoginPushSender = fetchPushSender,
|
|
303
304
|
addresses: AppLoginAddressStore = valkeyAppLoginAddressStore,
|
|
304
305
|
) => {
|
|
306
|
+
const db = () => injectedDb ?? sql;
|
|
305
307
|
// Error messages may contain their inputs; log only the error class, never addresses or proofs.
|
|
306
308
|
const addressFailure = (error: unknown) =>
|
|
307
309
|
log.warn("App login address hint unavailable", { error: error instanceof Error ? error.name : "UnknownError" });
|
|
@@ -332,12 +334,12 @@ export const createAppApprovalService = (
|
|
|
332
334
|
const cleanup = async () => {
|
|
333
335
|
// Bounded opportunistic maintenance: each accepted write retires up to 100
|
|
334
336
|
// expired rows per table, so bursts cannot trigger an unbounded sweep.
|
|
335
|
-
await db`DELETE FROM auth.app_device_changes WHERE id IN (SELECT id FROM auth.app_device_changes WHERE expires_at < now() - interval '1 minute' ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
|
|
336
|
-
await db`DELETE FROM auth.app_pairings WHERE id IN (SELECT p.id FROM auth.app_pairings p
|
|
337
|
+
await db()`DELETE FROM auth.app_device_changes WHERE id IN (SELECT id FROM auth.app_device_changes WHERE expires_at < now() - interval '1 minute' ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
|
|
338
|
+
await db()`DELETE FROM auth.app_pairings WHERE id IN (SELECT p.id FROM auth.app_pairings p
|
|
337
339
|
WHERE p.expires_at < now() - interval '1 minute' AND NOT EXISTS (SELECT 1 FROM auth.app_device_changes c WHERE c.pairing_id=p.id)
|
|
338
340
|
ORDER BY p.expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
|
|
339
|
-
await db`DELETE FROM auth.app_logins WHERE id IN (SELECT id FROM auth.app_logins WHERE expires_at < now() AND NOT (kind='terminal' AND state='approved') ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
|
|
340
|
-
await db`DELETE FROM auth.app_device_proofs WHERE (device_id,jti) IN (SELECT device_id,jti FROM auth.app_device_proofs WHERE expires_at < now() ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
|
|
341
|
+
await db()`DELETE FROM auth.app_logins WHERE id IN (SELECT id FROM auth.app_logins WHERE expires_at < now() AND NOT (kind='terminal' AND state='approved') ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
|
|
342
|
+
await db()`DELETE FROM auth.app_device_proofs WHERE (device_id,jti) IN (SELECT device_id,jti FROM auth.app_device_proofs WHERE expires_at < now() ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
|
|
341
343
|
};
|
|
342
344
|
const pairingOwner = async (tx: SQL, actor: AppApprovalActor, pairing: PairingRow, cfg: AppApprovalConfig) => {
|
|
343
345
|
if (
|
|
@@ -409,7 +411,7 @@ export const createAppApprovalService = (
|
|
|
409
411
|
if (!cfg.enabled || !cfg.appOrigin) return;
|
|
410
412
|
try {
|
|
411
413
|
// A withdrawn device change keeps its window, so withdrawing and asking again cannot push without bound.
|
|
412
|
-
const rows = await db<{ push_token: string }[]>`
|
|
414
|
+
const rows = await db()<{ push_token: string }[]>`
|
|
413
415
|
UPDATE auth.app_devices d SET login_wake_at=now(), login_wake_ref=${requestId}::uuid
|
|
414
416
|
WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND revoked_at IS NULL AND push_token IS NOT NULL
|
|
415
417
|
AND (${options?.accept ?? null}::text IS NULL OR ${options?.accept ?? null}::text=ANY(d.accepts))
|
|
@@ -423,7 +425,7 @@ export const createAppApprovalService = (
|
|
|
423
425
|
Array.from(new Set(rows.map((row) => row.push_token)), async (token) => {
|
|
424
426
|
const status = await push(cfg.appOrigin, { token, cloudOrigin: cfg.issuer, requestRef: requestId });
|
|
425
427
|
if (status === 410)
|
|
426
|
-
await db`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
|
|
428
|
+
await db()`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
|
|
427
429
|
}),
|
|
428
430
|
);
|
|
429
431
|
} catch (error) {
|
|
@@ -463,7 +465,7 @@ export const createAppApprovalService = (
|
|
|
463
465
|
try {
|
|
464
466
|
const cfg = await config(false);
|
|
465
467
|
if (!cfg.enabled || !cfg.appOrigin) return;
|
|
466
|
-
const rows = await db<{ push_token: string; user_id: string }[]>`
|
|
468
|
+
const rows = await db()<{ push_token: string; user_id: string }[]>`
|
|
467
469
|
UPDATE auth.app_devices d SET action_wake_at=now(), action_wake_ref=${approvalId}::uuid
|
|
468
470
|
WHERE d.id IN (
|
|
469
471
|
SELECT device.id FROM auth.app_devices device JOIN approvals.requests r ON r.approver_user_id=device.user_id
|
|
@@ -483,7 +485,7 @@ export const createAppApprovalService = (
|
|
|
483
485
|
Array.from(tokens, async ([token, userId]) => {
|
|
484
486
|
const status = await push(cfg.appOrigin, { token, cloudOrigin: cfg.issuer, requestRef: approvalId, kind: "action" });
|
|
485
487
|
if (status === 410)
|
|
486
|
-
await db`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
|
|
488
|
+
await db()`UPDATE auth.app_devices SET push_token=NULL WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND push_token=${token}`;
|
|
487
489
|
}),
|
|
488
490
|
);
|
|
489
491
|
} catch (error) {
|
|
@@ -492,7 +494,7 @@ export const createAppApprovalService = (
|
|
|
492
494
|
},
|
|
493
495
|
maintain: async (notify: (notice: AppDeviceEnrollmentNotice) => Promise<unknown>, signal?: AbortSignal) => {
|
|
494
496
|
await cleanup();
|
|
495
|
-
const rows = await db<(DeviceRow & { has_email: boolean })[]>`
|
|
497
|
+
const rows = await db()<(DeviceRow & { has_email: boolean })[]>`
|
|
496
498
|
SELECT d.*, COALESCE(btrim(u.mail), '') <> '' AS has_email
|
|
497
499
|
FROM auth.app_devices d JOIN auth.users u ON u.id = d.user_id
|
|
498
500
|
WHERE d.notified_at IS NULL ORDER BY d.created_at LIMIT ${limits.pageSize}`;
|
|
@@ -503,7 +505,7 @@ export const createAppApprovalService = (
|
|
|
503
505
|
// The notice is email-only, so an account without email relies on the
|
|
504
506
|
// enrollment audit entry instead of a delivery that can only fail.
|
|
505
507
|
if (row.has_email) await notify({ deviceId: row.id, userId: row.user_id, name: row.name, assisted: row.assisted });
|
|
506
|
-
await db`UPDATE auth.app_devices SET notified_at=now() WHERE id=${row.id}::uuid AND notified_at IS NULL`;
|
|
508
|
+
await db()`UPDATE auth.app_devices SET notified_at=now() WHERE id=${row.id}::uuid AND notified_at IS NULL`;
|
|
507
509
|
}
|
|
508
510
|
},
|
|
509
511
|
info: async () => {
|
|
@@ -524,7 +526,7 @@ export const createAppApprovalService = (
|
|
|
524
526
|
const id = crypto.randomUUID(),
|
|
525
527
|
token = secret(),
|
|
526
528
|
expiresAt = future(limits.pairingSeconds);
|
|
527
|
-
return db.begin(async (tx) => {
|
|
529
|
+
return db().begin(async (tx) => {
|
|
528
530
|
await fresh(tx, actor);
|
|
529
531
|
if (targetId !== actor.userId && (!actor.admin || !cfg.adminPairing)) return reject("FORBIDDEN", 403);
|
|
530
532
|
await tx`SELECT id FROM auth.users WHERE id = ${targetId}::uuid FOR UPDATE`;
|
|
@@ -549,7 +551,7 @@ export const createAppApprovalService = (
|
|
|
549
551
|
claimPairing: async (claim: { pairingId: string; secret: string; publicKey: AppDevicePublicKey; name: string; signature: string }) => {
|
|
550
552
|
const cfg = await config();
|
|
551
553
|
if (!(await verify(claim.publicKey, appPairingProofMessage(cfg.issuer, claim), claim.signature))) return reject("FORBIDDEN", 403);
|
|
552
|
-
return db.begin(async (tx) => {
|
|
554
|
+
return db().begin(async (tx) => {
|
|
553
555
|
const [p] = await tx<
|
|
554
556
|
PairingRow[]
|
|
555
557
|
>`SELECT * FROM auth.app_pairings WHERE id = ${claim.pairingId}::uuid AND issuer = ${cfg.issuer} FOR UPDATE`;
|
|
@@ -564,7 +566,7 @@ export const createAppApprovalService = (
|
|
|
564
566
|
},
|
|
565
567
|
pairingResult: async (id: string, token: string, key: AppDevicePublicKey) => {
|
|
566
568
|
const cfg = await config();
|
|
567
|
-
const [p] = await db<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
|
|
569
|
+
const [p] = await db()<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
|
|
568
570
|
if (!p || !matches(token, p.secret_hash)) return reject("FORBIDDEN", 403);
|
|
569
571
|
const age = Date.now() - new Date(p.expires_at).getTime();
|
|
570
572
|
if (age >= 0 && !(age < 60_000 && (p.state === "confirmed" || p.state === "cancelled"))) return reject("FORBIDDEN", 403);
|
|
@@ -576,7 +578,7 @@ export const createAppApprovalService = (
|
|
|
576
578
|
},
|
|
577
579
|
inspectPairing: async (actor: AppApprovalActor, id: string) => {
|
|
578
580
|
const cfg = await config();
|
|
579
|
-
return db.begin(async (tx) => {
|
|
581
|
+
return db().begin(async (tx) => {
|
|
580
582
|
const [p] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
|
|
581
583
|
if (!p) return reject("UNAVAILABLE", 404);
|
|
582
584
|
const [change] = await tx<ChangeRow[]>`SELECT * FROM auth.app_device_changes WHERE pairing_id=${p.id}::uuid`;
|
|
@@ -598,7 +600,7 @@ export const createAppApprovalService = (
|
|
|
598
600
|
confirmPairing: async (actor: AppApprovalActor, id: string, code: string): Promise<z.infer<typeof AppPairingConfirmResultSchema>> => {
|
|
599
601
|
const cfg = await config();
|
|
600
602
|
let wakeId: string | undefined;
|
|
601
|
-
const result = await db.begin(async (tx) => {
|
|
603
|
+
const result = await db().begin(async (tx) => {
|
|
602
604
|
const [snapshot] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id=${id}::uuid AND issuer=${cfg.issuer}`;
|
|
603
605
|
if (!snapshot) return reject("CONFLICT", 409);
|
|
604
606
|
await tx`SELECT id FROM auth.users WHERE id=${snapshot.user_id}::uuid FOR NO KEY UPDATE`;
|
|
@@ -636,7 +638,7 @@ export const createAppApprovalService = (
|
|
|
636
638
|
},
|
|
637
639
|
cancelPairing: async (actor: AppApprovalActor, id: string) => {
|
|
638
640
|
const cfg = await config();
|
|
639
|
-
return db.begin(async (tx) => {
|
|
641
|
+
return db().begin(async (tx) => {
|
|
640
642
|
const [snapshot] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id=${id}::uuid AND issuer=${cfg.issuer}`;
|
|
641
643
|
if (!snapshot) return reject("UNAVAILABLE", 404);
|
|
642
644
|
await tx`SELECT id FROM auth.users WHERE id=${snapshot.user_id}::uuid FOR NO KEY UPDATE`;
|
|
@@ -656,7 +658,7 @@ export const createAppApprovalService = (
|
|
|
656
658
|
const cfg = await config(false);
|
|
657
659
|
const changeId = "changeId" in lookup ? lookup.changeId : null,
|
|
658
660
|
deviceId = "deviceId" in lookup ? lookup.deviceId : null;
|
|
659
|
-
const [row] = await db<ChangeRow[]>`SELECT * FROM auth.app_device_changes
|
|
661
|
+
const [row] = await db()<ChangeRow[]>`SELECT * FROM auth.app_device_changes
|
|
660
662
|
WHERE issuer=${cfg.issuer} AND user_id=${actor.userId}::uuid AND requested_by_sid=${actor.sid}::uuid AND kind='revoke'
|
|
661
663
|
AND (${changeId}::uuid IS NULL OR id=${changeId}::uuid) AND (${deviceId}::uuid IS NULL OR device_id=${deviceId}::uuid)
|
|
662
664
|
ORDER BY created_at DESC, id DESC LIMIT 1`;
|
|
@@ -665,7 +667,7 @@ export const createAppApprovalService = (
|
|
|
665
667
|
},
|
|
666
668
|
cancelChange: async (actor: AppApprovalActor, id: string) => {
|
|
667
669
|
const cfg = await config(false);
|
|
668
|
-
return db.begin(async (tx) => {
|
|
670
|
+
return db().begin(async (tx) => {
|
|
669
671
|
await tx`SELECT id FROM auth.users WHERE id=${actor.userId}::uuid FOR NO KEY UPDATE`;
|
|
670
672
|
const change = await ownedChange(tx, cfg, actor, id);
|
|
671
673
|
if (changeStatus(change).state !== "pending") return reject("CONFLICT", 409);
|
|
@@ -676,7 +678,7 @@ export const createAppApprovalService = (
|
|
|
676
678
|
},
|
|
677
679
|
listDevices: async (actor: AppApprovalActor, after?: string) => {
|
|
678
680
|
const cfg = await config(false);
|
|
679
|
-
const rows = await db<
|
|
681
|
+
const rows = await db()<
|
|
680
682
|
DeviceRow[]
|
|
681
683
|
>`SELECT * FROM auth.app_devices WHERE issuer=${cfg.issuer} AND user_id=${actor.userId}::uuid AND (${after ?? null}::uuid IS NULL OR id > ${after ?? null}::uuid) ORDER BY id LIMIT ${limits.pageSize + 1}`;
|
|
682
684
|
return {
|
|
@@ -692,7 +694,7 @@ export const createAppApprovalService = (
|
|
|
692
694
|
): Promise<z.infer<typeof AppDeviceRevokeResultSchema> | undefined> => {
|
|
693
695
|
const cfg = await config(false);
|
|
694
696
|
let wakeId: string | undefined;
|
|
695
|
-
const result = await db.begin(async (tx) => {
|
|
697
|
+
const result = await db().begin(async (tx) => {
|
|
696
698
|
await tx`SELECT id FROM auth.users WHERE id=${actor.userId}::uuid FOR NO KEY UPDATE`;
|
|
697
699
|
const user = await fresh(tx, actor);
|
|
698
700
|
const [device] = await tx<DeviceRow[]>`SELECT * FROM auth.app_devices
|
|
@@ -723,7 +725,7 @@ export const createAppApprovalService = (
|
|
|
723
725
|
listUserDevices: async (actor: AppDeviceAdministrator, userId: string): Promise<AppDeviceView[]> => {
|
|
724
726
|
if (!actor.admin) return reject("FORBIDDEN", 403);
|
|
725
727
|
const cfg = await config(false);
|
|
726
|
-
const rows = await db<
|
|
728
|
+
const rows = await db()<
|
|
727
729
|
DeviceRow[]
|
|
728
730
|
>`SELECT * FROM auth.app_devices WHERE issuer=${cfg.issuer} AND user_id=${userId}::uuid AND revoked_at IS NULL ORDER BY created_at, id LIMIT ${limits.devicesPerAccount}`;
|
|
729
731
|
return rows.map(view);
|
|
@@ -733,7 +735,7 @@ export const createAppApprovalService = (
|
|
|
733
735
|
revokeUserDevice: async (actor: AppDeviceAdministrator, userId: string, id: string) => {
|
|
734
736
|
if (!actor.admin) return reject("FORBIDDEN", 403);
|
|
735
737
|
const cfg = await config(false);
|
|
736
|
-
return db.begin(async (tx) => {
|
|
738
|
+
return db().begin(async (tx) => {
|
|
737
739
|
const [device] = await tx<
|
|
738
740
|
DeviceRow[]
|
|
739
741
|
>`SELECT * FROM auth.app_devices WHERE id=${id}::uuid AND issuer=${cfg.issuer} AND user_id=${userId}::uuid FOR UPDATE`;
|
|
@@ -753,7 +755,7 @@ export const createAppApprovalService = (
|
|
|
753
755
|
code = comparison(),
|
|
754
756
|
expiresAt = future(limits.loginSeconds);
|
|
755
757
|
let owner: string | undefined;
|
|
756
|
-
await db.begin(async (tx) => {
|
|
758
|
+
await db().begin(async (tx) => {
|
|
757
759
|
let candidate = await loginCandidate(tx, identifier, category);
|
|
758
760
|
if (candidate && !(await loginBudget(tx, cfg.issuer, candidate.id))) candidate = undefined;
|
|
759
761
|
const now = new Date();
|
|
@@ -789,7 +791,7 @@ export const createAppApprovalService = (
|
|
|
789
791
|
code = comparison(),
|
|
790
792
|
expiresAt = future(limits.loginSeconds);
|
|
791
793
|
let owner: string | undefined;
|
|
792
|
-
await db.begin(async (tx) => {
|
|
794
|
+
await db().begin(async (tx) => {
|
|
793
795
|
let candidate = await loginCandidate(tx, input.identifier);
|
|
794
796
|
const subject = candidate ?? { id: crypto.randomUUID(), profile: "user" as const };
|
|
795
797
|
// Even a decoy performs the client-access, device and shared-budget queries.
|
|
@@ -830,7 +832,7 @@ export const createAppApprovalService = (
|
|
|
830
832
|
return reject("FORBIDDEN", 403);
|
|
831
833
|
await cleanup();
|
|
832
834
|
let decidedBrowserId: string | undefined;
|
|
833
|
-
const outcome = await db.begin(async (tx) => {
|
|
835
|
+
const outcome = await db().begin(async (tx) => {
|
|
834
836
|
let changeUserId: string | undefined;
|
|
835
837
|
if (proof.command.operation === "change") {
|
|
836
838
|
const [snapshot] = await tx<
|
|
@@ -1105,7 +1107,9 @@ export const createAppApprovalService = (
|
|
|
1105
1107
|
browserStatus: async (id: string, token: string, hold?: { ms: number; signal?: AbortSignal }) => {
|
|
1106
1108
|
const cfg = await config();
|
|
1107
1109
|
const read = async () => {
|
|
1108
|
-
const [row] = await db<
|
|
1110
|
+
const [row] = await db()<
|
|
1111
|
+
LoginRow[]
|
|
1112
|
+
>`SELECT * FROM auth.app_logins WHERE kind='browser' AND id=${id}::uuid AND issuer=${cfg.issuer}`;
|
|
1109
1113
|
if (!row || !matches(token, row.browser_hash)) return reject("UNAVAILABLE", 404);
|
|
1110
1114
|
const expiresIn = new Date(row.expires_at).getTime() - Date.now();
|
|
1111
1115
|
return { state: expiresIn <= 0 ? "expired" : row.state, expiresIn };
|
|
@@ -1134,7 +1138,7 @@ export const createAppApprovalService = (
|
|
|
1134
1138
|
},
|
|
1135
1139
|
consumeLogin: async (id: string, token: string) => {
|
|
1136
1140
|
const cfg = await config();
|
|
1137
|
-
return db.begin(async (tx) => {
|
|
1141
|
+
return db().begin(async (tx) => {
|
|
1138
1142
|
// Lock order is device -> request for both approval and completion.
|
|
1139
1143
|
const [snapshot] = await tx<
|
|
1140
1144
|
LoginRow[]
|