@k2b/cloud 0.47.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/_internal/app-access-manager-sql.ts +10 -0
- package/src/_internal/app-access-resources.ts +2 -1
- package/src/_internal/app-capabilities.ts +73 -0
- package/src/_internal/app-event-catalog.ts +1 -1
- package/src/_internal/app-queries.ts +1 -1
- package/src/_internal/application-package.ts +2 -1
- package/src/_internal/bounded-json.ts +7 -1
- package/src/_internal/capabilities.ts +25 -2
- package/src/_internal/define-app.ts +33 -2
- package/src/_internal/heartbeat.ts +2 -0
- package/src/_internal/nats-connection.ts +10 -9
- package/src/_internal/process-sync.ts +6 -6
- package/src/_internal/public-surface.ts +76 -0
- package/src/_internal/purpose-name.ts +7 -0
- package/src/_internal/registry-validation.ts +1 -0
- package/src/_internal/registry.ts +49 -3
- package/src/_internal/workload-credential.ts +2 -1
- package/src/ai/app-skill-store.ts +2 -1
- package/src/ai/app-skills.ts +2 -1
- package/src/ai/approvals.ts +2 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/credentials.ts +2 -1
- package/src/ai/dictation-runtime.ts +1 -1
- package/src/ai/dictations.ts +1 -1
- package/src/ai/files-store.ts +2 -1
- package/src/ai/inference-calls.ts +2 -1
- package/src/ai/map-store.ts +1 -1
- package/src/ai/map-tool.ts +1 -1
- package/src/ai/memories.ts +1 -1
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +1 -1
- package/src/ai/memory-workflow-evidence.ts +1 -1
- package/src/ai/message-queue.ts +2 -1
- package/src/ai/migrate.ts +1 -1
- package/src/ai/model-access-migrate.ts +1 -1
- package/src/ai/model-access.ts +1 -1
- package/src/ai/model-catalog.ts +1 -1
- package/src/ai/model-delete.ts +1 -1
- package/src/ai/model-usage.ts +2 -1
- package/src/ai/open-questions.ts +1 -1
- package/src/ai/openai-api-slots.ts +2 -1
- package/src/ai/prefs.ts +2 -1
- package/src/ai/projects.ts +2 -1
- package/src/ai/provider-connections.ts +1 -1
- package/src/ai/quota-limits.ts +2 -1
- package/src/ai/quota-provider.ts +2 -1
- package/src/ai/quota-report.ts +2 -1
- package/src/ai/quotas-migrate.ts +2 -1
- package/src/ai/quotas.ts +2 -1
- package/src/ai/request-headers.ts +2 -1
- package/src/ai/review-content.ts +2 -1
- package/src/ai/skill-search.ts +2 -1
- package/src/ai/skills.ts +2 -1
- package/src/ai/store.ts +2 -1
- package/src/ai/structured-runs.ts +2 -1
- package/src/ai/tool-audit.ts +2 -1
- package/src/ai/turn-timing.ts +1 -1
- package/src/ai/usage.ts +2 -1
- package/src/ai/web-provenance.ts +2 -1
- package/src/api/admin-ai-quotas.ts +1 -1
- package/src/api/admin-ai.ts +1 -1
- package/src/api/admin-app-domains.ts +62 -0
- package/src/api/admin-app-keys.ts +47 -3
- package/src/api/admin-core-settings.ts +1 -1
- package/src/api/admin-outgoing-mail.ts +1 -1
- package/src/api/app-bootstrap.ts +153 -0
- package/src/api/app-events.ts +1 -1
- package/src/api/auth.ts +1 -1
- package/src/api/authenticate-app-request.ts +129 -0
- package/src/api/capabilities.ts +87 -6
- package/src/api/index.ts +7 -0
- package/src/api/me.ts +1 -1
- package/src/api/search.bench.ts +1 -1
- package/src/app-pwa/service.ts +2 -1
- package/src/app-pwa/transaction.ts +4 -2
- package/src/capabilities/claims.ts +2 -1
- package/src/capabilities/executions.ts +8 -4
- package/src/capabilities/geo-client.ts +1 -1
- package/src/capabilities/migrate.ts +3 -1
- package/src/capabilities/resource-readers.ts +2 -2
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/server-invoke.ts +73 -20
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/app-domains.ts +140 -0
- package/src/cli/admin/app-keys.ts +25 -3
- package/src/cli/admin/index.ts +107 -88
- package/src/config/env.ts +10 -4
- package/src/contracts/app-domains.ts +91 -0
- package/src/contracts/app-status.ts +104 -0
- package/src/contracts/capabilities.ts +37 -2
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-event-delivery.ts +1 -1
- package/src/events/live-delivery.ts +2 -1
- package/src/index.ts +1 -0
- package/src/runtime/bootstrap-client.ts +195 -0
- package/src/runtime/bootstrap-protocol.ts +67 -0
- package/src/runtime/clients.ts +6 -2
- package/src/runtime/connections.ts +9 -0
- package/src/server/app-key.ts +14 -0
- package/src/server/index.ts +2 -0
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access.ts +2 -1
- package/src/services/account-category-policy.ts +1 -1
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/account-lifecycle/index.ts +1 -1
- package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
- package/src/services/accounts/app.ts +1 -1
- package/src/services/accounts/avatar.ts +1 -1
- package/src/services/accounts/base-group.ts +1 -1
- package/src/services/accounts/directory-shares.ts +1 -1
- package/src/services/accounts/duplicate-emails.ts +1 -1
- package/src/services/accounts/entities.ts +2 -1
- package/src/services/accounts/group-sql.ts +2 -1
- package/src/services/accounts/groups.ts +1 -1
- package/src/services/accounts/identities.ts +13 -11
- package/src/services/accounts/ipa-data.ts +1 -1
- package/src/services/accounts/local-groups.ts +2 -1
- package/src/services/accounts/machine-group-management.ts +2 -1
- package/src/services/accounts/posix.ts +13 -12
- package/src/services/accounts/request-policy.ts +2 -1
- package/src/services/accounts/switching.ts +1 -1
- package/src/services/accounts/users.ts +2 -1
- package/src/services/announcements/index.ts +1 -1
- package/src/services/app-access-management.ts +3 -9
- package/src/services/app-access.ts +2 -1
- package/src/services/app-admin-facts.ts +160 -0
- package/src/services/app-approval.ts +34 -30
- package/src/services/app-bootstrap.ts +217 -0
- package/src/services/app-details.ts +3 -2
- package/src/services/app-domain-check.ts +152 -0
- package/src/services/app-domains.ts +172 -0
- package/src/services/app-events/accept.ts +5 -5
- package/src/services/app-events/admin.ts +15 -15
- package/src/services/app-events/declarations.ts +6 -3
- package/src/services/app-events/deliver.ts +6 -6
- package/src/services/app-events.ts +3 -2
- package/src/services/app-keys.ts +45 -11
- package/src/services/approvals/content.ts +1 -1
- package/src/services/approvals/index.ts +2 -1
- package/src/services/approvals/list.ts +1 -1
- package/src/services/approvals/migrate.ts +1 -1
- package/src/services/audit/index.ts +2 -1
- package/src/services/auth-flows/ipa.ts +1 -1
- package/src/services/auth-flows/magic-link.ts +1 -1
- package/src/services/auth-flows/password-reset.ts +1 -1
- package/src/services/auth-flows/proxy-return.ts +1 -1
- package/src/services/downstream-budget.ts +2 -2
- package/src/services/geo/admin.ts +13 -12
- package/src/services/geo/identity.ts +46 -0
- package/src/services/geo/ledger.ts +118 -106
- package/src/services/geo/migrate.ts +2 -1
- package/src/services/geo/route-chain.ts +3 -3
- package/src/services/geo/service.ts +4 -24
- package/src/services/geoip/index.ts +5 -5
- package/src/services/geoip/store.ts +24 -21
- package/src/services/help/index.ts +2 -1
- package/src/services/help/store.ts +2 -1
- package/src/services/identity/app-request.ts +44 -0
- package/src/services/identity/constants.ts +1 -0
- package/src/services/identity/index.ts +1 -0
- package/src/services/identity/invocation-actor.ts +2 -1
- package/src/services/identity/invocation-token.ts +112 -0
- package/src/services/identity/key-ring.ts +2 -2
- package/src/services/identity/resource-readers.ts +1 -1
- package/src/services/identity/routes.ts +11 -3
- package/src/services/identity/runtime-config.ts +14 -3
- package/src/services/identity/workload-auth.ts +2 -0
- package/src/services/identity/workload-runtime.ts +3 -3
- package/src/services/index.ts +4 -0
- package/src/services/ipa/groups.ts +1 -1
- package/src/services/ipa/profile.ts +1 -1
- package/src/services/ipa/search.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +1 -1
- package/src/services/known-browsers.ts +2 -1
- package/src/services/legal-consent.ts +1 -1
- package/src/services/links/index.ts +16 -0
- package/src/services/links/runtime.ts +44 -0
- package/src/services/links/store.ts +105 -0
- package/src/services/logging/index.ts +13 -8
- package/src/services/logging/redaction.ts +6 -1
- package/src/services/logging/trace.ts +9 -9
- package/src/services/logging/web-vitals.ts +1 -1
- package/src/services/mandates/index.ts +2 -1
- package/src/services/notifications/batches.ts +1 -1
- package/src/services/notifications/browser.ts +1 -1
- package/src/services/notifications/catalog.ts +1 -1
- package/src/services/notifications/dispatcher.ts +1 -1
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/observability.ts +2 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/quiet.ts +1 -1
- package/src/services/notifications/user.ts +1 -1
- package/src/services/oauth-tokens.ts +1 -1
- package/src/services/outbox.ts +15 -14
- package/src/services/outgoing-mail/admin.ts +1 -1
- package/src/services/outgoing-mail/dispatcher.ts +1 -1
- package/src/services/outgoing-mail/drain.ts +2 -1
- package/src/services/outgoing-mail/enqueue.ts +2 -1
- package/src/services/outgoing-mail/messages.ts +2 -1
- package/src/services/outgoing-mail/retention.ts +1 -1
- package/src/services/outgoing-mail/send.ts +1 -1
- package/src/services/outgoing-mail/store.ts +2 -1
- package/src/services/panel-context-consent.ts +32 -28
- package/src/services/providers/local/auth.ts +1 -1
- package/src/services/providers/local/users.ts +1 -1
- package/src/services/pwa-devices.ts +2 -1
- package/src/services/rail-preferences.ts +16 -12
- package/src/services/rail-shortcuts.ts +12 -10
- package/src/services/request-cache-redis.ts +3 -3
- package/src/services/service-account-credentials.ts +2 -1
- package/src/services/service-accounts.ts +2 -1
- package/src/services/session/index.ts +2 -1
- package/src/services/session/user.ts +2 -1
- package/src/services/settings/core-settings.ts +20 -0
- package/src/services/settings/crypto.ts +2 -2
- package/src/services/settings/index.ts +1 -1
- package/src/services/settings/store.ts +6 -1
- package/src/services/settings/versions.ts +2 -1
- package/src/services/weather/forecast.ts +2 -1
- package/src/services/weather/locations.ts +1 -1
- package/src/services/weather/migrate.ts +2 -1
- package/src/services/weather/short-id.ts +2 -1
- package/src/services/webauthn.ts +1 -1
- package/src/ssr/admin-navigation.ts +0 -1
- package/src/ssr/compact-shell.browser-harness.ts +2 -0
- package/src/ssr/platform-messages.ts +0 -2
- package/src/start.ts +35 -14
- package/src/workflows/ai/store.ts +2 -1
- package/src/workflows/store/budget.ts +2 -1
- package/src/workflows/store/definitions.ts +2 -1
- package/src/workflows/store/events.ts +2 -1
- package/src/workflows/store/observability.ts +2 -1
- package/src/workflows/store/retention.ts +2 -1
- package/src/workflows/store/runs.ts +2 -1
- package/src/workflows/store/transaction.ts +2 -1
- package/src/workflows/store/worker.ts +2 -1
|
@@ -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";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getAppWorkloadCredential } from "../../_internal/workload-credential";
|
|
2
|
+
import { parseAppKey } from "../../app-key";
|
|
3
|
+
import { env } from "../../config/env";
|
|
4
|
+
import { CapabilityAppIdSchema } from "../../contracts/capabilities";
|
|
5
|
+
import { signAppProof } from "../../runtime/bootstrap-protocol";
|
|
6
|
+
|
|
7
|
+
/** Send only to Core. Call for every transmission, including safe caller-owned retries. */
|
|
8
|
+
export const sendAppRequest = async (
|
|
9
|
+
request: Request,
|
|
10
|
+
options: {
|
|
11
|
+
appId: string;
|
|
12
|
+
transport?: (request: Request) => Promise<Response>;
|
|
13
|
+
/** Release N legacy authority; evaluated only when no app key is configured. */
|
|
14
|
+
bearer?: () => string | undefined;
|
|
15
|
+
},
|
|
16
|
+
): Promise<Response> => {
|
|
17
|
+
const appId = CapabilityAppIdSchema.parse(options.appId);
|
|
18
|
+
const headers = new Headers(request.headers);
|
|
19
|
+
const declaredApp = headers.get("X-Cloud-App-Id");
|
|
20
|
+
if (declaredApp !== null && declaredApp !== appId) throw new Error("App identity header does not match the calling application");
|
|
21
|
+
headers.set("X-Cloud-App-Id", appId);
|
|
22
|
+
const keyValue = env.CLOUD_APP_KEY;
|
|
23
|
+
if (keyValue) {
|
|
24
|
+
const key = parseAppKey(keyValue);
|
|
25
|
+
if (key.appId !== options.appId) throw new Error("App-key appId does not match the calling application");
|
|
26
|
+
const url = new URL(request.url);
|
|
27
|
+
const proof = await signAppProof(key, {
|
|
28
|
+
method: request.method,
|
|
29
|
+
path: url.pathname + url.search,
|
|
30
|
+
body: new Uint8Array(await request.clone().arrayBuffer()),
|
|
31
|
+
});
|
|
32
|
+
headers.set("authorization", `Bearer ${proof}`);
|
|
33
|
+
} else if (!headers.has("authorization")) {
|
|
34
|
+
const token = (options.bearer ?? getAppWorkloadCredential)();
|
|
35
|
+
if (!token) throw new Error("App workload authorization is unavailable");
|
|
36
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
37
|
+
}
|
|
38
|
+
// A proof is bound to this target; redirects must never carry it elsewhere.
|
|
39
|
+
return (options.transport ?? fetch)(new Request(request, { headers, redirect: "error" }));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Legacy workload tokens and the OAuth secret never contain JWT separators. */
|
|
43
|
+
export const isAppRequestProof = (authorization: string | undefined): boolean =>
|
|
44
|
+
authorization?.startsWith("Bearer ") === true && authorization.slice(7).includes(".");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const CLOUD_SESSION_JWKS_PATH = "/.well-known/cloud-session-jwks.json";
|
|
2
2
|
export const CLOUD_INVOCATION_JWKS_PATH = "/.well-known/cloud-invocation-jwks.json";
|
|
3
3
|
export const CLOUD_OAUTH_JWKS_PATH = "/.well-known/jwks.json";
|
|
4
|
+
export const CLOUD_INTERNAL_OAUTH_JWKS_PATH = "/api/_internal/identity/v1/oauth/jwks";
|
|
4
5
|
export const CLOUD_SESSION_TOKEN_TYPE = "cloud-session+jwt";
|
|
5
6
|
export const CLOUD_SESSION_AUDIENCE = "cloud";
|
|
6
7
|
export const CLOUD_INVOCATION_TOKEN_TYPE = "cloud-invocation+jwt";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { sql } from "
|
|
1
|
+
import { sql } from "../../runtime/clients";
|
|
2
|
+
|
|
2
3
|
import type { RequestActor } from "../../server/middleware/auth";
|
|
3
4
|
import type { AccessSubject } from "../../server/services/access";
|
|
4
5
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|