@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,5 +1,5 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { User } from "../../contracts/shared";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
4
4
|
import { accounts } from "../accounts";
|
|
5
5
|
import { logger } from "../logging";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { redis, sql } from "bun";
|
|
2
1
|
import { accountCategory } from "../../contracts/account-categories";
|
|
3
2
|
import type { User } from "../../contracts/shared";
|
|
3
|
+
import { redis, sql } from "../../runtime/clients";
|
|
4
4
|
import { createAuthLoginUrl } from "../../shared/redirect";
|
|
5
5
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
6
6
|
import { accounts } from "../accounts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { redis, sql } from "bun";
|
|
2
1
|
import type { User } from "../../contracts/shared";
|
|
2
|
+
import { redis, sql } from "../../runtime/clients";
|
|
3
3
|
import { createAuthPasswordResetUrl } from "../../shared/redirect";
|
|
4
4
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
5
5
|
import { getFreeIpaConfig } from "../freeipa-config";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import { HTTPException } from "hono/http-exception";
|
|
3
3
|
import {
|
|
4
4
|
GEO_USAGE_GROUPS,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
geoKeyProblem,
|
|
13
13
|
validateGeoBudget,
|
|
14
14
|
} from "../../contracts/geo-admin";
|
|
15
|
+
import { sql } from "../../runtime/clients";
|
|
15
16
|
import { audit } from "../audit";
|
|
16
17
|
import { toPgTextArray } from "../postgres";
|
|
17
18
|
import * as settings from "../settings";
|
|
@@ -83,7 +84,7 @@ export const verifyGeoapifyKey =
|
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () => Date } = {}) => {
|
|
86
|
-
const db = options.db ?? sql;
|
|
87
|
+
const db = () => options.db ?? sql;
|
|
87
88
|
const verify = options.verify ?? verifyGeoapifyKey();
|
|
88
89
|
const now = options.now ?? (() => new Date());
|
|
89
90
|
|
|
@@ -92,7 +93,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
|
|
|
92
93
|
* carries that transaction's exact timestamp, and so does every row that event wrote.
|
|
93
94
|
*/
|
|
94
95
|
const changedBy = async (action: string, keys: readonly string[]): Promise<GeoAdminPerson | null> => {
|
|
95
|
-
const [row] = await db<{ actor_user_id: string; name: string | null }[]>`
|
|
96
|
+
const [row] = await db()<{ actor_user_id: string; name: string | null }[]>`
|
|
96
97
|
SELECT e.actor_user_id, COALESCE(NULLIF(u.display_name, ''), u.uid) AS name
|
|
97
98
|
FROM audit.events e LEFT JOIN auth.users u ON u.id = e.actor_user_id
|
|
98
99
|
WHERE e.action = ${action} AND e.actor_user_id IS NOT NULL
|
|
@@ -111,7 +112,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
|
|
|
111
112
|
const [config, states, rows, keyBy, budgetBy] = await Promise.all([
|
|
112
113
|
readGeoConfig(),
|
|
113
114
|
settings.readStates([KEY, ...BUDGET_KEYS]),
|
|
114
|
-
db<{ key: string; updated_at: Date }[]>`
|
|
115
|
+
db()<{ key: string; updated_at: Date }[]>`
|
|
115
116
|
SELECT key, updated_at FROM settings.entries WHERE key = ANY(${toPgTextArray([KEY, ...BUDGET_KEYS])}::text[])`,
|
|
116
117
|
changedBy(KEY_ACTION, [KEY]),
|
|
117
118
|
changedBy(BUDGET_ACTION, BUDGET_KEYS),
|
|
@@ -147,15 +148,15 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
|
|
|
147
148
|
const periods = geoPeriods(now(), config.timezone);
|
|
148
149
|
const plan = config.plan === "custom" ? { dailyCredits: config.customDailyCredits } : GEO_PLANS[config.plan];
|
|
149
150
|
const [[month], [today], shares, [overruns]] = await Promise.all([
|
|
150
|
-
db<
|
|
151
|
+
db()<
|
|
151
152
|
{ used: number }[]
|
|
152
153
|
>`SELECT used::float8 AS used FROM geo.usage WHERE scope = 'cloud' AND subject = '*' AND period = ${periods.cloud.period}`,
|
|
153
|
-
db<{ used: number; at_limit: number }[]>`
|
|
154
|
-
SELECT COALESCE(sum(used), 0)::float8 AS used, count(*) FILTER (WHERE used >= ${config.personDailyCredits * 1000})::int AS at_limit
|
|
154
|
+
db()<{ used: number; at_limit: number }[]>`
|
|
155
|
+
SELECT COALESCE(sum(used), 0)::float8 AS used, count(*) FILTER (WHERE used >= ${config.personDailyCredits * 1000} AND subject NOT LIKE 'app:%')::int AS at_limit
|
|
155
156
|
FROM geo.usage WHERE scope = 'person' AND period = ${periods.person.period}`,
|
|
156
|
-
db<{ function_group: string; used: number }[]>`
|
|
157
|
+
db()<{ function_group: string; used: number }[]>`
|
|
157
158
|
SELECT function_group, used::float8 AS used FROM geo.shares WHERE scope = 'cloud' AND subject = '*' AND period = ${periods.cloud.period}`,
|
|
158
|
-
db<{ count: number }[]>`SELECT count(*)::int AS count FROM audit.events
|
|
159
|
+
db()<{ count: number }[]>`SELECT count(*)::int AS count FROM audit.events
|
|
159
160
|
WHERE action = 'core.geo.route_cost_overrun' AND metadata->>'period' = ${periods.cloud.period}`,
|
|
160
161
|
]);
|
|
161
162
|
return {
|
|
@@ -193,7 +194,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
|
|
|
193
194
|
await verify(apiKey, signal);
|
|
194
195
|
signal?.throwIfAborted();
|
|
195
196
|
const before = (await settings.readStates([KEY])).get(KEY)?.stored ?? "none";
|
|
196
|
-
await db.begin(async (tx) => {
|
|
197
|
+
await db().begin(async (tx) => {
|
|
197
198
|
// The person replaces whatever is stored, including a value this installation cannot read.
|
|
198
199
|
await settings.set(KEY, apiKey, tx, { replaceUnreadable: true });
|
|
199
200
|
await audit.record(
|
|
@@ -206,7 +207,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
|
|
|
206
207
|
},
|
|
207
208
|
async removeKey(actor: Actor): Promise<GeoAdminStatus> {
|
|
208
209
|
requireAdmin(actor);
|
|
209
|
-
await db.begin(async (tx) => {
|
|
210
|
+
await db().begin(async (tx) => {
|
|
210
211
|
const [row] = await tx<{ key: string }[]>`DELETE FROM settings.entries WHERE key = ${KEY} RETURNING key`;
|
|
211
212
|
if (row) await audit.record({ action: "core.geo.key_removed", outcome: "allowed", actor: auditActor(actor) }, tx);
|
|
212
213
|
});
|
|
@@ -230,7 +231,7 @@ export const createGeoAdmin = (options: { db?: SQL; verify?: Verify; now?: () =>
|
|
|
230
231
|
// resets their unreadable values to the defaults instead of leaving geo off behind a field nobody sees.
|
|
231
232
|
const inactive = BUDGET_FIELDS.flatMap(([, name]) => (name in updates ? [] : [name]));
|
|
232
233
|
const resets: string[] = [];
|
|
233
|
-
await db.begin(async (tx) => {
|
|
234
|
+
await db().begin(async (tx) => {
|
|
234
235
|
// Same lock and final-pair rule as every other writer of these settings.
|
|
235
236
|
await assertGeoBudgetPair(tx, updates, []);
|
|
236
237
|
// Saving the form is the explicit replacement of a value this installation cannot read.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AppPwaActor } from "../../app-pwa/contracts";
|
|
2
|
+
import type { AccessSubject, CapabilityAppExecutionContext, RequestActor } from "../../contracts";
|
|
3
|
+
import { GeoError } from "./errors";
|
|
4
|
+
|
|
5
|
+
export type GeoContext = {
|
|
6
|
+
actor?: RequestActor | CapabilityAppExecutionContext["actor"];
|
|
7
|
+
accessSubject?: AccessSubject | { type: "none" };
|
|
8
|
+
locale?: string;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
requestId?: string;
|
|
11
|
+
};
|
|
12
|
+
export type GeoBudgetSubject = AccessSubject | { type: "app"; appId: string };
|
|
13
|
+
export type AppPwaGeoContext = { actor: AppPwaActor; locale?: string; signal?: AbortSignal; requestId?: string };
|
|
14
|
+
export type GeoCallContext = GeoContext | AppPwaGeoContext;
|
|
15
|
+
export const geoIdentity = (
|
|
16
|
+
context: GeoContext,
|
|
17
|
+
): {
|
|
18
|
+
actor: RequestActor | CapabilityAppExecutionContext["actor"];
|
|
19
|
+
accessSubject: GeoBudgetSubject;
|
|
20
|
+
} => {
|
|
21
|
+
const { actor, accessSubject } = context;
|
|
22
|
+
if (actor?.kind === "app" && actor.keyId && accessSubject?.type === "none")
|
|
23
|
+
return { actor, accessSubject: { type: "app", appId: actor.appId } };
|
|
24
|
+
if (
|
|
25
|
+
!actor ||
|
|
26
|
+
actor.kind === "app" ||
|
|
27
|
+
!accessSubject ||
|
|
28
|
+
accessSubject.type === "none" ||
|
|
29
|
+
!(actor.kind === "user" ? actor.user?.id : actor.serviceAccount?.id) ||
|
|
30
|
+
!(accessSubject.type === "user" ? accessSubject.userId : accessSubject.serviceAccountId)
|
|
31
|
+
)
|
|
32
|
+
throw new GeoError("GEO_FORBIDDEN");
|
|
33
|
+
return { actor, accessSubject };
|
|
34
|
+
};
|
|
35
|
+
export const geoCallIdentity = (
|
|
36
|
+
context: GeoCallContext,
|
|
37
|
+
): {
|
|
38
|
+
actor: RequestActor | CapabilityAppExecutionContext["actor"] | AppPwaActor;
|
|
39
|
+
accessSubject: GeoBudgetSubject;
|
|
40
|
+
} => {
|
|
41
|
+
if (context.actor?.kind === "device" || context.actor?.kind === "visit") {
|
|
42
|
+
if (!context.actor.ownerUserId) throw new GeoError("GEO_FORBIDDEN");
|
|
43
|
+
return { actor: context.actor, accessSubject: { type: "user", userId: context.actor.ownerUserId } };
|
|
44
|
+
}
|
|
45
|
+
return geoIdentity({ actor: context.actor, accessSubject: "accessSubject" in context ? context.accessSubject : undefined });
|
|
46
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import type { AppPwaActor } from "../../app-pwa/contracts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { CapabilityAppExecutionContext, CapabilityExecutionContext } from "../../contracts";
|
|
4
|
+
import { sql } from "../../runtime/clients";
|
|
4
5
|
import { audit } from "../audit";
|
|
5
6
|
import { GeoError } from "./errors";
|
|
7
|
+
import type { GeoBudgetSubject } from "./identity";
|
|
6
8
|
import type { GeoConfig } from "./settings";
|
|
7
9
|
|
|
8
10
|
export type GeoBudgetConfig = Pick<GeoConfig, "timezone" | "monthlyCredits" | "personDailyCredits">;
|
|
@@ -30,126 +32,136 @@ export const geoPeriods = (now: Date, timezone: string): { person: GeoPeriod; cl
|
|
|
30
32
|
cloud: { period: day.slice(0, 7), resetsAt: boundary(32 * 86400_000, 7) },
|
|
31
33
|
};
|
|
32
34
|
};
|
|
33
|
-
export const geoSubjectKey = (subject:
|
|
34
|
-
subject.type === "
|
|
35
|
+
export const geoSubjectKey = (subject: GeoBudgetSubject) =>
|
|
36
|
+
subject.type === "app"
|
|
37
|
+
? `app:${subject.appId}`
|
|
38
|
+
: subject.type === "user"
|
|
39
|
+
? `user:${subject.userId}`
|
|
40
|
+
: `service_account:${subject.serviceAccountId}`;
|
|
35
41
|
export type GeoReservation = {
|
|
36
42
|
cost: number;
|
|
37
43
|
group: string;
|
|
38
44
|
rows: Array<{ scope: "cloud" | "person"; subject: string; period: string; resetsAt: string; limit: number }>;
|
|
39
45
|
};
|
|
40
|
-
const rowsFor = (subject:
|
|
46
|
+
const rowsFor = (subject: GeoBudgetSubject, config: GeoBudgetConfig, now: Date) => {
|
|
41
47
|
const periods = geoPeriods(now, config.timezone);
|
|
42
48
|
return [
|
|
43
49
|
{ scope: "cloud", subject: "*", ...periods.cloud, limit: config.monthlyCredits * 1000 },
|
|
44
50
|
{ scope: "person", subject: geoSubjectKey(subject), ...periods.person, limit: config.personDailyCredits * 1000 },
|
|
45
51
|
] satisfies GeoReservation["rows"];
|
|
46
52
|
};
|
|
47
|
-
export const createGeoLedger = (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
53
|
+
export const createGeoLedger = (injectedDb?: SQL) => {
|
|
54
|
+
const db = () => injectedDb ?? sql;
|
|
55
|
+
return {
|
|
56
|
+
async reserve(subject: GeoBudgetSubject, config: GeoBudgetConfig, cost: number, group: string, now: Date): Promise<GeoReservation> {
|
|
57
|
+
if (!Number.isSafeInteger(cost) || cost < 0) throw new Error("Invalid geo reservation");
|
|
58
|
+
const rows = rowsFor(subject, config, now);
|
|
59
|
+
await db().begin(async (tx) => {
|
|
60
|
+
// Fixed order across upserts, updates, shares and settlement: Cloud first, person second.
|
|
61
|
+
for (const row of rows)
|
|
62
|
+
await tx`INSERT INTO geo.usage(scope,subject,period) VALUES(${row.scope},${row.subject},${row.period}) ON CONFLICT DO NOTHING`;
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
// Keep the refusal's available/shortfall numbers in the same row-lock snapshot as admission.
|
|
65
|
+
const [balance] = await tx<{ used: number }[]>`SELECT used::float8 AS used FROM geo.usage
|
|
58
66
|
WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period} FOR UPDATE`;
|
|
59
|
-
|
|
67
|
+
const changed = await tx`UPDATE geo.usage SET used=used+${cost} WHERE scope=${row.scope} AND subject=${row.subject}
|
|
60
68
|
AND period=${row.period} AND used+${cost}<=${row.limit} RETURNING used`;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
if (!changed.length) {
|
|
70
|
+
const available = row.limit - (balance?.used ?? 0);
|
|
71
|
+
throw new GeoError("GEO_BUDGET_EXHAUSTED", {
|
|
72
|
+
scope: row.scope,
|
|
73
|
+
resetsAt: row.resetsAt,
|
|
74
|
+
requiredCredits: cost / 1000,
|
|
75
|
+
availableCredits: available / 1000,
|
|
76
|
+
shortfallCredits: (cost - available) / 1000,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await tx`INSERT INTO geo.shares(scope,subject,period,function_group,used)
|
|
80
|
+
for (const row of rows)
|
|
81
|
+
await tx`INSERT INTO geo.shares(scope,subject,period,function_group,used)
|
|
74
82
|
VALUES(${row.scope},${row.subject},${row.period},${group},${cost})
|
|
75
83
|
ON CONFLICT(scope,subject,period,function_group) DO UPDATE SET used=geo.shares.used+EXCLUDED.used`;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
});
|
|
85
|
+
return { cost, group, rows };
|
|
86
|
+
},
|
|
87
|
+
async settle(reservation: GeoReservation, referenceCost: number) {
|
|
88
|
+
const refund = reservation.cost - referenceCost;
|
|
89
|
+
if (!Number.isSafeInteger(referenceCost) || referenceCost < 0 || !Number.isSafeInteger(refund))
|
|
90
|
+
throw new Error("Invalid geo settlement");
|
|
91
|
+
if (!refund) return;
|
|
92
|
+
// Negative refunds truthfully debit known routing overruns. Admission still uses the original
|
|
93
|
+
// bounded CAS transaction; future reservations see the overdrawn counters and are refused.
|
|
94
|
+
// Settle once per reservation, in the same fixed row order as admission.
|
|
95
|
+
await db().begin(async (tx) => {
|
|
96
|
+
for (const row of reservation.rows) {
|
|
97
|
+
await tx`UPDATE geo.usage SET used=used-${refund} WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period}`;
|
|
98
|
+
await tx`UPDATE geo.shares SET used=used-${refund} WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period} AND function_group=${reservation.group}`;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
async availability(subject: GeoBudgetSubject, config: GeoBudgetConfig, now: Date) {
|
|
103
|
+
const balances = [];
|
|
104
|
+
for (const row of rowsFor(subject, config, now)) {
|
|
105
|
+
const [used] = await db()<{ used: number }[]>`SELECT used::float8 AS used FROM geo.usage
|
|
98
106
|
WHERE scope=${row.scope} AND subject=${row.subject} AND period=${row.period}`;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
kind: context.actor.kind,
|
|
107
|
+
balances.push({ exhausted: (used?.used ?? 0) >= row.limit, resetsAt: row.resetsAt });
|
|
108
|
+
}
|
|
109
|
+
return { cloud: balances[0]!, person: balances[1]! };
|
|
110
|
+
},
|
|
111
|
+
async recordTransition(
|
|
112
|
+
event: "provider_key_rejected" | "monthly_budget_exhausted" | "route_cost_overrun",
|
|
113
|
+
timezone: string,
|
|
114
|
+
context: { actor: CapabilityExecutionContext["actor"] | CapabilityAppExecutionContext["actor"] | AppPwaActor; requestId?: string },
|
|
115
|
+
now: Date,
|
|
116
|
+
metadata: Record<string, number> = {},
|
|
117
|
+
) {
|
|
118
|
+
const period = geoPeriods(now, timezone).cloud.period;
|
|
119
|
+
await db().begin(async (tx) => {
|
|
120
|
+
const inserted =
|
|
121
|
+
event === "route_cost_overrun"
|
|
122
|
+
? [true]
|
|
123
|
+
: await tx`INSERT INTO geo.audit_transitions(event,period) VALUES(${event},${period}) ON CONFLICT DO NOTHING RETURNING event`;
|
|
124
|
+
if (!inserted.length) return;
|
|
125
|
+
const actor =
|
|
126
|
+
context.actor.kind === "device" || context.actor.kind === "visit"
|
|
127
|
+
? null
|
|
128
|
+
: context.actor.kind === "app"
|
|
129
|
+
? { uid: `app:${context.actor.appId}`, provider: "app", roles: [] }
|
|
130
|
+
: context.actor.kind === "user"
|
|
131
|
+
? {
|
|
132
|
+
userId: context.actor.user.id,
|
|
133
|
+
uid: context.actor.user.uid,
|
|
134
|
+
provider: context.actor.user.provider,
|
|
135
|
+
roles: context.actor.user.roles,
|
|
136
|
+
}
|
|
137
|
+
: { uid: `service-account:${context.actor.serviceAccount.id}`, provider: "service_account", roles: context.actor.scopes };
|
|
138
|
+
await audit.record(
|
|
139
|
+
{
|
|
140
|
+
action: `core.geo.${event}`,
|
|
141
|
+
outcome: event === "monthly_budget_exhausted" ? "denied" : "failed",
|
|
142
|
+
actor,
|
|
143
|
+
requestId: context.requestId,
|
|
144
|
+
metadata: {
|
|
145
|
+
period,
|
|
146
|
+
...metadata,
|
|
147
|
+
...(context.actor.kind === "app" ? { appId: context.actor.appId, keyId: context.actor.keyId } : {}),
|
|
148
|
+
...(context.actor.kind === "device" || context.actor.kind === "visit"
|
|
149
|
+
? {
|
|
143
150
|
appId: context.actor.appId,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
subject: { type: "user", userId: context.actor.ownerUserId },
|
|
152
|
+
via: {
|
|
153
|
+
kind: context.actor.kind,
|
|
154
|
+
appId: context.actor.appId,
|
|
155
|
+
deviceId: context.actor.deviceId,
|
|
156
|
+
...(context.actor.kind === "visit" ? { visitId: context.actor.visitId } : {}),
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
: {}),
|
|
160
|
+
},
|
|
149
161
|
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
162
|
+
tx,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
2
3
|
export const migrate = async (db: SQL = sql): Promise<void> => {
|
|
3
4
|
await db.begin(async (tx) => {
|
|
4
5
|
await tx`SELECT pg_advisory_xact_lock(hashtextextended('core.geo.migrations', 0))`;
|
|
@@ -6,9 +6,9 @@ import type { createGeoCache } from "./cache";
|
|
|
6
6
|
import { geoCacheKey } from "./cache";
|
|
7
7
|
import { geoReferenceCost } from "./costs";
|
|
8
8
|
import { GeoError, GeoProviderError, limitExceeded } from "./errors";
|
|
9
|
+
import { type GeoContext, geoIdentity } from "./identity";
|
|
9
10
|
import type { createGeoLedger } from "./ledger";
|
|
10
11
|
import { geoRouteReferenceCost } from "./route-costs";
|
|
11
|
-
import type { GeoContext } from "./service";
|
|
12
12
|
import { type GeoConfig, geoPlan } from "./settings";
|
|
13
13
|
|
|
14
14
|
export type GeoRouteChainDependencies = {
|
|
@@ -42,14 +42,14 @@ export const runGeoRouteChain = async <T>(
|
|
|
42
42
|
work: (call: GeoRouteChainCall) => Promise<T>,
|
|
43
43
|
): Promise<T> => {
|
|
44
44
|
const { ledger, bucket, cache, now } = dependencies;
|
|
45
|
-
|
|
45
|
+
const { accessSubject } = geoIdentity(context);
|
|
46
46
|
context.signal?.throwIfAborted();
|
|
47
47
|
const maximumCredits = Math.min(config.personDailyCredits, config.monthlyCredits);
|
|
48
48
|
if (reserveCost > maximumCredits * 1000) limitExceeded("reservationCredits", maximumCredits, reserveCost / 1000);
|
|
49
49
|
let reservation: Awaited<ReturnType<typeof ledger.reserve>>;
|
|
50
50
|
try {
|
|
51
51
|
// One atomic admission for all known geocodes and every possible paid routing step.
|
|
52
|
-
reservation = await ledger.reserve(
|
|
52
|
+
reservation = await ledger.reserve(accessSubject, config, reserveCost, "route", now());
|
|
53
53
|
} catch (error) {
|
|
54
54
|
if (error instanceof GeoError && error.code === "GEO_BUDGET_EXHAUSTED" && error.details.scope === "cloud")
|
|
55
55
|
await dependencies.recordTransition("monthly_budget_exhausted", config.timezone, context);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
-
import type { AppPwaActor } from "../../app-pwa/contracts";
|
|
3
|
-
import type { AccessSubject, CapabilityExecutionContext, RequestActor } from "../../contracts";
|
|
4
2
|
import {
|
|
5
3
|
GeoAutocompleteDataSchema,
|
|
6
4
|
type GeoAutocompleteInput,
|
|
@@ -26,28 +24,10 @@ import { cutGeoWindow, decodePolyline6, locateOnGeoLine, simplifyGeoLine } from
|
|
|
26
24
|
import { createGeoRouteService } from "./route-service";
|
|
27
25
|
import { type GeoConfig, geoPlan, readGeoConfig } from "./settings";
|
|
28
26
|
|
|
29
|
-
export type
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export const geoIdentity = (context: GeoContext): { actor: RequestActor; accessSubject: AccessSubject } => {
|
|
34
|
-
const { actor, accessSubject } = context;
|
|
35
|
-
if (
|
|
36
|
-
!actor ||
|
|
37
|
-
!accessSubject ||
|
|
38
|
-
!(actor.kind === "user" ? actor.user?.id : actor.kind === "service_account" ? actor.serviceAccount?.id : undefined) ||
|
|
39
|
-
!(accessSubject.type === "user" ? accessSubject.userId : accessSubject.serviceAccountId)
|
|
40
|
-
)
|
|
41
|
-
throw new GeoError("GEO_FORBIDDEN");
|
|
42
|
-
return { actor, accessSubject };
|
|
43
|
-
};
|
|
44
|
-
const geoCallIdentity = (context: GeoCallContext): { actor: RequestActor | AppPwaActor; accessSubject: AccessSubject } => {
|
|
45
|
-
if (context.actor?.kind === "device" || context.actor?.kind === "visit") {
|
|
46
|
-
if (!context.actor.ownerUserId) throw new GeoError("GEO_FORBIDDEN");
|
|
47
|
-
return { actor: context.actor, accessSubject: { type: "user", userId: context.actor.ownerUserId } };
|
|
48
|
-
}
|
|
49
|
-
return geoIdentity({ actor: context.actor, accessSubject: "accessSubject" in context ? context.accessSubject : undefined });
|
|
50
|
-
};
|
|
27
|
+
export { type AppPwaGeoContext, type GeoContext, geoIdentity } from "./identity";
|
|
28
|
+
|
|
29
|
+
import { type AppPwaGeoContext, type GeoCallContext, type GeoContext, geoCallIdentity, geoIdentity } from "./identity";
|
|
30
|
+
|
|
51
31
|
const bound = (name: string, value: number, min: number, max: number, integer = false) => {
|
|
52
32
|
if (!Number.isFinite(value) || value > max || value < min || (integer && !Number.isInteger(value)))
|
|
53
33
|
limitExceeded(name, value < min ? min : max, value);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { HTTPException } from "hono/http-exception";
|
|
3
2
|
import {
|
|
4
3
|
DBIP_ATTRIBUTION,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
type GeoIpAdminStatus,
|
|
9
8
|
type GeoIpKind,
|
|
10
9
|
} from "../../contracts/geoip";
|
|
10
|
+
import { sql } from "../../runtime/clients";
|
|
11
11
|
import { createGeoIp, type GeoIp, type GeoIpDatabaseInfo, getGeoIp } from "../../server/request-context/geoip";
|
|
12
12
|
import { audit } from "../audit";
|
|
13
13
|
import { GeoIpError, parseGeoIpUpload, readGeoIpUpload } from "./upload";
|
|
@@ -79,11 +79,11 @@ const serialized = <T>(write: () => Promise<T>): Promise<T> => {
|
|
|
79
79
|
return run;
|
|
80
80
|
};
|
|
81
81
|
export const createGeoIpService = (options: { db?: typeof sql; reader?: GeoIp; bundled?: () => ReturnType<GeoIp["status"]> } = {}) => {
|
|
82
|
-
const db = options.db ?? sql;
|
|
82
|
+
const db = () => options.db ?? sql;
|
|
83
83
|
const reader = () => options.reader ?? getGeoIp();
|
|
84
84
|
const status = async (actor: Actor, config: { now?: () => number } = {}): Promise<GeoIpAdminStatus> => {
|
|
85
85
|
requireAdmin(actor);
|
|
86
|
-
const rows = await db<GeoIpUploadInfo[]>`
|
|
86
|
+
const rows = await db()<GeoIpUploadInfo[]>`
|
|
87
87
|
SELECT d.kind, d.sha256, (d.metadata->>'size')::int AS size, d.database_type, d.build_epoch,
|
|
88
88
|
d.metadata->'description'->>'en' AS description, d.uploaded_at, d.uploaded_by,
|
|
89
89
|
COALESCE(NULLIF(u.display_name, ''), u.uid) AS uploader_name
|
|
@@ -103,7 +103,7 @@ export const createGeoIpService = (options: { db?: typeof sql; reader?: GeoIp; b
|
|
|
103
103
|
database.info.sha256 = sha256;
|
|
104
104
|
config.signal?.throwIfAborted();
|
|
105
105
|
await serialized(async () => {
|
|
106
|
-
await db.begin(async (tx) => {
|
|
106
|
+
await db().begin(async (tx) => {
|
|
107
107
|
await tx`
|
|
108
108
|
INSERT INTO geoip.databases (kind, data, sha256, database_type, build_epoch, metadata, uploaded_by)
|
|
109
109
|
VALUES (${kind}, ${bytes}, ${sha256}, ${metadata.databaseType}, ${metadata.buildEpoch}::timestamptz,
|
|
@@ -132,7 +132,7 @@ export const createGeoIpService = (options: { db?: typeof sql; reader?: GeoIp; b
|
|
|
132
132
|
async reset(actor: Actor, kind: GeoIpKind) {
|
|
133
133
|
requireAdmin(actor);
|
|
134
134
|
const changed = await serialized(async () => {
|
|
135
|
-
const deleted = await db.begin(async (tx) => {
|
|
135
|
+
const deleted = await db().begin(async (tx) => {
|
|
136
136
|
const [row] = await tx<{ sha256: string; database_type: string; build_epoch: Date; size: number }[]>`
|
|
137
137
|
DELETE FROM geoip.databases WHERE kind = ${kind}
|
|
138
138
|
RETURNING sha256, database_type, build_epoch, octet_length(data) AS size`;
|
|
@@ -1,30 +1,33 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { GEOIP_MAX_DATABASE_BYTES, type GeoIpKind } from "../../contracts/geoip";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import type { GeoIpUploads } from "../../server/request-context/geoip";
|
|
4
4
|
|
|
5
5
|
const SLICE_BYTES = 8 * 1024 * 1024;
|
|
6
|
-
export const createGeoIpUploads = (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
export const createGeoIpUploads = (injectedDb?: typeof sql): GeoIpUploads => {
|
|
7
|
+
const db = () => injectedDb ?? sql;
|
|
8
|
+
return {
|
|
9
|
+
async versions() {
|
|
10
|
+
const rows = await db()<{ kind: GeoIpKind; sha256: string }[]>`SELECT kind, sha256 FROM geoip.databases`;
|
|
11
|
+
return Object.fromEntries(rows.map((row) => [row.kind, row.sha256]));
|
|
12
|
+
},
|
|
13
|
+
async read(kind, sha256) {
|
|
14
|
+
const [row] = await db()<{ size: number }[]>`
|
|
13
15
|
SELECT octet_length(data) AS size FROM geoip.databases WHERE kind = ${kind} AND sha256 = ${sha256}`;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if (!row) return null;
|
|
17
|
+
if (row.size < 1 || row.size > GEOIP_MAX_DATABASE_BYTES) throw new Error("Invalid GeoIP database size");
|
|
18
|
+
const bytes = Buffer.allocUnsafe(row.size);
|
|
19
|
+
for (let offset = 0; offset < bytes.length; offset += SLICE_BYTES) {
|
|
20
|
+
const length = Math.min(SLICE_BYTES, bytes.length - offset);
|
|
21
|
+
const [slice] = await db()<{ data: Uint8Array }[]>`
|
|
20
22
|
SELECT substring(data FROM ${offset + 1} FOR ${length}) AS data
|
|
21
23
|
FROM geoip.databases WHERE kind = ${kind} AND sha256 = ${sha256}`;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
24
|
+
if (!slice) return null;
|
|
25
|
+
if (slice.data.byteLength !== length) throw new Error("Incomplete GeoIP database slice");
|
|
26
|
+
bytes.set(slice.data, offset);
|
|
27
|
+
}
|
|
28
|
+
return bytes;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
29
32
|
|
|
30
33
|
export const geoIpUploads = createGeoIpUploads();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import { appAudienceRoles, hasAnyAppRole } from "../../_internal/app-roles";
|
|
3
3
|
import { getApp, listApps } from "../../_internal/registry";
|
|
4
4
|
import type { AppRegistryEntry } from "../../contracts/registry";
|
|
5
5
|
import type { RequestActor } from "../../contracts/shared";
|
|
6
|
+
import { sql } from "../../runtime/clients";
|
|
6
7
|
import type { RequestAuthority } from "../../server/middleware/auth";
|
|
7
8
|
import { resolveAppPresentations } from "../../shared/app-presentation";
|
|
8
9
|
import { type HelpArticleLink, helpLocaleChain, parseHelpReference } from "../../shared/help";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import { APP_REGISTRY_TTL_MS } from "../../_internal/registry";
|
|
3
|
+
import { sql } from "../../runtime/clients";
|
|
3
4
|
import type { HelpAccessLevel, HelpKind } from "../../shared/help";
|
|
4
5
|
import { ensureSchema, toPgTextArray } from "../postgres";
|
|
5
6
|
import type { HelpCorpus, HelpMetadata } from "./types";
|