@k2b/cloud 0.38.0 → 0.40.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/README.md +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { capabilityReviewView } from "../../_internal/capability-reviews";
|
|
1
2
|
import {
|
|
2
3
|
APP_APPROVAL_LIMITS,
|
|
3
4
|
type AppActionSummary,
|
|
@@ -5,7 +6,7 @@ import {
|
|
|
5
6
|
appActionDigest,
|
|
6
7
|
APP_ACTION_LIMITS as limits,
|
|
7
8
|
} from "../../contracts/app-approval";
|
|
8
|
-
import {
|
|
9
|
+
import { CapabilityActionReviewDataSchema } from "../../contracts/capabilities";
|
|
9
10
|
import { DEFAULT_LOCALE } from "../../server/locale";
|
|
10
11
|
import type { PendingApprovalRow } from "./list";
|
|
11
12
|
|
|
@@ -21,6 +22,8 @@ const cut = (value: string, limit: number): string => {
|
|
|
21
22
|
return points.length > limit ? `${points.slice(0, limit - 1).join("")}…` : value;
|
|
22
23
|
};
|
|
23
24
|
|
|
25
|
+
export const appApprovalText = (value: string, limit: number): string => cut(plain(value), limit);
|
|
26
|
+
|
|
24
27
|
/** A deterministic, plain-text snapshot; the digest binds the device's choice to everything it shows. */
|
|
25
28
|
export const appActionView = async (
|
|
26
29
|
row: PendingApprovalRow,
|
|
@@ -45,7 +48,7 @@ export const appActionView = async (
|
|
|
45
48
|
const first = lines[0] ?? plain(row.action);
|
|
46
49
|
const prefix = `${appName}: `;
|
|
47
50
|
const sentence = first.startsWith(prefix) && first.length > prefix.length ? first.slice(prefix.length) : first;
|
|
48
|
-
const review =
|
|
51
|
+
const review = CapabilityActionReviewDataSchema.safeParse(row.details);
|
|
49
52
|
let message: string | null;
|
|
50
53
|
const details: AppActionView["details"] = [];
|
|
51
54
|
const detail = (label: string, value: string, format?: "date" | "date-time") => {
|
|
@@ -60,11 +63,12 @@ export const appActionView = async (
|
|
|
60
63
|
const messages: string[] = [];
|
|
61
64
|
const reviewLines = new Set<string>();
|
|
62
65
|
if (review.success) {
|
|
63
|
-
const
|
|
66
|
+
const view = capabilityReviewView(review.data, { timeZone: "UTC" });
|
|
67
|
+
const cleaned = plain(view.message);
|
|
64
68
|
if (cleaned !== sentence) messages.push(cleaned);
|
|
65
69
|
reviewLines.add(cleaned);
|
|
66
|
-
for (const line of
|
|
67
|
-
for (const field of
|
|
70
|
+
for (const line of view.message.split(/\r?\n/)) reviewLines.add(plain(line));
|
|
71
|
+
for (const field of view.details ?? [])
|
|
68
72
|
detail(field.label, field.value, field.format === "date" || field.format === "date-time" ? field.format : undefined);
|
|
69
73
|
}
|
|
70
74
|
for (const line of lines.slice(1)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SQL, sql } from "bun";
|
|
2
2
|
import { capabilityRequestHash } from "../../capabilities/claims";
|
|
3
3
|
import { APPROVAL_ACP_CLIENT } from "../../contracts/approvals";
|
|
4
|
-
import type {
|
|
4
|
+
import type { CapabilityActionReviewData } from "../../contracts/capabilities";
|
|
5
5
|
import { isAppSessionActor } from "../../server/actor";
|
|
6
6
|
import type { RequestActor, RequestCredentialKind } from "../../server/middleware/auth";
|
|
7
7
|
import { audit } from "../audit";
|
|
@@ -23,7 +23,8 @@ export type ApprovalAssurance = "standard" | "confirm";
|
|
|
23
23
|
*/
|
|
24
24
|
export const approvalAssurance = (rememberable: boolean): ApprovalAssurance => (rememberable ? "standard" : "confirm");
|
|
25
25
|
export const approvalArgsDigest = (input: unknown): string => capabilityRequestHash(input);
|
|
26
|
-
export const approvalReviewDigest = (review:
|
|
26
|
+
export const approvalReviewDigest = (review: CapabilityActionReviewData | null | undefined): string =>
|
|
27
|
+
capabilityRequestHash(review ?? null);
|
|
27
28
|
export const approvalDecisionChannel = (input: {
|
|
28
29
|
credentialKind?: RequestCredentialKind;
|
|
29
30
|
actor?: RequestActor;
|
|
@@ -50,7 +51,7 @@ export type ApprovalRequestInput = {
|
|
|
50
51
|
approverUserId: string;
|
|
51
52
|
assurance: ApprovalAssurance;
|
|
52
53
|
sentence: string;
|
|
53
|
-
review?:
|
|
54
|
+
review?: CapabilityActionReviewData | null;
|
|
54
55
|
input: unknown;
|
|
55
56
|
expiresInMs: number;
|
|
56
57
|
};
|
|
@@ -67,7 +68,7 @@ export type ApprovalRequest = {
|
|
|
67
68
|
approverUserId: string;
|
|
68
69
|
assurance: ApprovalAssurance;
|
|
69
70
|
sentence: string;
|
|
70
|
-
details:
|
|
71
|
+
details: CapabilityActionReviewData | null;
|
|
71
72
|
argsDigest: string;
|
|
72
73
|
reviewDigest: string;
|
|
73
74
|
status: ApprovalStatus;
|
|
@@ -211,7 +212,7 @@ export type ApprovalConsumeResult =
|
|
|
211
212
|
| { ok: false; code: "NOT_FOUND" | "NOT_APPROVED" | "ALREADY_CONSUMED" | "INPUT_CHANGED" | "REVIEW_CHANGED" | "REVOKED" };
|
|
212
213
|
export const consume = async (
|
|
213
214
|
id: string,
|
|
214
|
-
input: { input: unknown; review:
|
|
215
|
+
input: { input: unknown; review: CapabilityActionReviewData | null; requester: { kind: ApprovalRequesterKind; ref: string } },
|
|
215
216
|
db?: SQL,
|
|
216
217
|
): Promise<ApprovalConsumeResult> => {
|
|
217
218
|
const argsDigest = approvalArgsDigest(input.input),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
|
+
import { type AiStoredActionReview, storedActionReview } from "../../ai/action-review";
|
|
2
3
|
import { AI_WEBSITE_APPROVAL_TOOL, aiTurnAllowsRememberedApprovals } from "../../ai/approvals";
|
|
3
4
|
import type { AiTurnRunConfig } from "../../ai/types";
|
|
4
5
|
import type { RuntimeAppMeta } from "../../contracts/app";
|
|
5
6
|
import { APPROVALS_PAGE_SIZE, type ApprovalCounts, type PendingApproval } from "../../contracts/approvals";
|
|
6
|
-
import {
|
|
7
|
+
import { type CapabilityActionPolicy, CapabilityActionReviewDataSchema } from "../../contracts/capabilities";
|
|
7
8
|
import type { ApprovalAssurance } from ".";
|
|
8
9
|
import { decodeApprovalCursor, encodeApprovalCursor } from "./cursor";
|
|
9
10
|
|
|
@@ -18,6 +19,7 @@ export type PendingApprovalRow = {
|
|
|
18
19
|
assurance: ApprovalAssurance;
|
|
19
20
|
sentence: string;
|
|
20
21
|
details: unknown;
|
|
22
|
+
storedReview: AiStoredActionReview | null;
|
|
21
23
|
createdAt: Date;
|
|
22
24
|
expiresAt: Date;
|
|
23
25
|
cursorCreatedAt: string;
|
|
@@ -61,10 +63,21 @@ export const listPending = async (input: {
|
|
|
61
63
|
to_char(r.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS "cursorCreatedAt",
|
|
62
64
|
question.short_id AS "conversationShortId", question.title AS "conversationTitle",
|
|
63
65
|
question.allow_chat AS "allowChat", question.allow_always AS "allowAlways",
|
|
64
|
-
question.remember_tool_name AS "rememberToolName", question.tool_name AS "toolName", question.run_config AS "runConfig"
|
|
66
|
+
question.remember_tool_name AS "rememberToolName", question.tool_name AS "toolName", question.run_config AS "runConfig",
|
|
67
|
+
question.stored_review AS "storedReview"
|
|
65
68
|
FROM open r JOIN LATERAL (
|
|
66
|
-
SELECT c.short_id, c.title, p.allow_chat, p.allow_always, p.remember_tool_name, p.tool_name, t.run_config
|
|
69
|
+
SELECT c.short_id, c.title, p.allow_chat, p.allow_always, p.remember_tool_name, p.tool_name, t.run_config,
|
|
70
|
+
(SELECT m.meta->'toolReviews'->review.call_id FROM (
|
|
71
|
+
SELECT meta, conversation_id, loop_id, role, seq FROM ai.messages
|
|
72
|
+
UNION ALL
|
|
73
|
+
SELECT meta, conversation_id, loop_id, role, seq FROM ai.task_messages
|
|
74
|
+
) m WHERE m.conversation_id=p.conversation_id AND m.loop_id=p.turn_id::text AND m.role='assistant'
|
|
75
|
+
AND m.meta->'toolReviews'->review.call_id IS NOT NULL
|
|
76
|
+
ORDER BY m.seq DESC LIMIT 1) AS stored_review
|
|
67
77
|
FROM ai.pending_actions p JOIN ai.conversations c ON c.id=p.conversation_id JOIN ai.turns t ON t.id=p.turn_id
|
|
78
|
+
-- A capability's own approval waits as "<call>-approval-<n>"; its review is stored under the call itself.
|
|
79
|
+
CROSS JOIN LATERAL (SELECT CASE WHEN p.kind = 'custom_approval'
|
|
80
|
+
THEN COALESCE(substring(p.call_id FROM '^(.*)-approval-[0-9]+$'), p.call_id) ELSE p.call_id END AS call_id) review
|
|
68
81
|
WHERE p.approval_id=r.id AND p.status='pending' ORDER BY p.created_at DESC, p.id DESC LIMIT 1
|
|
69
82
|
) question ON true
|
|
70
83
|
WHERE ${matches(input.appId, input.link)}
|
|
@@ -93,9 +106,17 @@ export const countPending = async (input: { approverUserId: string }): Promise<A
|
|
|
93
106
|
export const presentPendingApproval = (
|
|
94
107
|
row: PendingApprovalRow,
|
|
95
108
|
apps: readonly Pick<RuntimeAppMeta, "id" | "name" | "icon">[],
|
|
109
|
+
policy?: CapabilityActionPolicy,
|
|
96
110
|
): PendingApproval => {
|
|
97
111
|
const app = apps.find((entry) => entry.id === row.appId);
|
|
98
|
-
const review =
|
|
112
|
+
const review = CapabilityActionReviewDataSchema.nullable().safeParse(row.details);
|
|
113
|
+
const originalReview = review.success ? review.data : null;
|
|
114
|
+
const displayReview =
|
|
115
|
+
originalReview && "sentence" in originalReview
|
|
116
|
+
? row.storedReview && "sentence" in row.storedReview
|
|
117
|
+
? row.storedReview
|
|
118
|
+
: storedActionReview(originalReview, policy)
|
|
119
|
+
: originalReview;
|
|
99
120
|
const remember = row.assurance === "standard" && aiTurnAllowsRememberedApprovals(row.runConfig);
|
|
100
121
|
return {
|
|
101
122
|
id: row.id,
|
|
@@ -103,7 +124,7 @@ export const presentPendingApproval = (
|
|
|
103
124
|
action: row.action,
|
|
104
125
|
assurance: row.assurance,
|
|
105
126
|
sentence: row.sentence,
|
|
106
|
-
review:
|
|
127
|
+
review: displayReview && "legacy" in displayReview ? originalReview : displayReview,
|
|
107
128
|
requester: {
|
|
108
129
|
kind: "chat",
|
|
109
130
|
appId: row.requesterAppId,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { NewBrowserSignInNotifier } from "../known-browsers";
|
|
2
|
+
|
|
1
3
|
export type AuthNotificationDeliveryResult = {
|
|
2
4
|
id: string;
|
|
3
5
|
status: "queued" | "delivered" | "suppressed" | "error";
|
|
4
6
|
};
|
|
5
7
|
|
|
6
8
|
export type AuthNotificationSender = {
|
|
9
|
+
sendNewBrowserSignIn: NewBrowserSignInNotifier;
|
|
7
10
|
sendMagicLink: (input: { email: string; token: string; magicLink: string; locale?: string }) => Promise<AuthNotificationDeliveryResult>;
|
|
8
11
|
sendIpaLoginHint: (input: { email: string; loginUrl: string; locale?: string }) => Promise<AuthNotificationDeliveryResult>;
|
|
9
12
|
sendPasswordReset: (input: { email: string; resetLink: string; locale?: string }) => Promise<AuthNotificationDeliveryResult>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
import {
|
|
4
|
+
DBIP_ATTRIBUTION,
|
|
5
|
+
GEOIP_MAX_DATABASE_BYTES,
|
|
6
|
+
GEOIP_MAX_REQUEST_BYTES,
|
|
7
|
+
type GeoIpAdminDatabase,
|
|
8
|
+
type GeoIpAdminStatus,
|
|
9
|
+
type GeoIpKind,
|
|
10
|
+
} from "../../contracts/geoip";
|
|
11
|
+
import { createGeoIp, type GeoIp, type GeoIpDatabaseInfo, getGeoIp } from "../../server/request-context/geoip";
|
|
12
|
+
import { audit } from "../audit";
|
|
13
|
+
import { GeoIpError, parseGeoIpUpload, readGeoIpUpload } from "./upload";
|
|
14
|
+
|
|
15
|
+
export { GEOIP_MAX_DATABASE_BYTES, GEOIP_MAX_REQUEST_BYTES } from "../../contracts/geoip";
|
|
16
|
+
export { GeoIpError } from "./upload";
|
|
17
|
+
|
|
18
|
+
type Actor = { id: string; roles: readonly string[]; uid?: string; provider?: string };
|
|
19
|
+
export type GeoIpUploadInfo = {
|
|
20
|
+
kind: GeoIpKind;
|
|
21
|
+
sha256: string;
|
|
22
|
+
size: number;
|
|
23
|
+
database_type: string;
|
|
24
|
+
build_epoch: Date;
|
|
25
|
+
description: string | null;
|
|
26
|
+
uploaded_at: Date;
|
|
27
|
+
uploaded_by: string | null;
|
|
28
|
+
uploader_name: string | null;
|
|
29
|
+
};
|
|
30
|
+
const requireAdmin = (actor: Actor) => {
|
|
31
|
+
if (!actor.roles.includes("admin")) throw new HTTPException(403, { message: "Administrator access required." });
|
|
32
|
+
};
|
|
33
|
+
let bundledInfo: ReturnType<GeoIp["status"]> | undefined;
|
|
34
|
+
const getBundledInfo = () => (bundledInfo ??= createGeoIp().status());
|
|
35
|
+
const isDbIp = (value: string) => /db[\s-]?ip/i.test(value);
|
|
36
|
+
|
|
37
|
+
export const geoIpAdminStatus = (
|
|
38
|
+
rows: GeoIpUploadInfo[],
|
|
39
|
+
bundled: { city: GeoIpDatabaseInfo | null; asn: GeoIpDatabaseInfo | null },
|
|
40
|
+
now: number,
|
|
41
|
+
): GeoIpAdminStatus => {
|
|
42
|
+
const database = (kind: GeoIpKind): GeoIpAdminDatabase => {
|
|
43
|
+
const row = rows.find((row) => row.kind === kind);
|
|
44
|
+
const info = bundled[kind];
|
|
45
|
+
const buildDate = row?.build_epoch ?? info?.buildDate;
|
|
46
|
+
return {
|
|
47
|
+
kind,
|
|
48
|
+
source: row ? "uploaded" : (info?.source ?? null),
|
|
49
|
+
bundled: info !== null,
|
|
50
|
+
databaseType: row?.database_type ?? info?.databaseType ?? null,
|
|
51
|
+
buildDate: buildDate?.toISOString() ?? null,
|
|
52
|
+
stale: buildDate !== undefined && now - buildDate.getTime() > 183 * 24 * 60 * 60 * 1000,
|
|
53
|
+
dbIp: row ? isDbIp(`${row.database_type} ${row.description ?? ""}`) : info !== null,
|
|
54
|
+
upload: row
|
|
55
|
+
? {
|
|
56
|
+
sha256: row.sha256,
|
|
57
|
+
size: row.size,
|
|
58
|
+
uploadedAt: row.uploaded_at.toISOString(),
|
|
59
|
+
uploadedBy: row.uploaded_by ? { id: row.uploaded_by, name: row.uploader_name ?? row.uploaded_by } : null,
|
|
60
|
+
}
|
|
61
|
+
: null,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const databases: GeoIpAdminStatus["databases"] = [database("city"), database("asn")];
|
|
65
|
+
return {
|
|
66
|
+
databases,
|
|
67
|
+
attribution: databases.some((item) => item.dbIp) ? DBIP_ATTRIBUTION : null,
|
|
68
|
+
limits: { maxDatabaseBytes: GEOIP_MAX_DATABASE_BYTES, maxRequestBytes: GEOIP_MAX_REQUEST_BYTES },
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let uploading = false;
|
|
73
|
+
// Each write and its reader install run one after another in this process, so the local reader follows the
|
|
74
|
+
// commit order; other instances follow through their refresh.
|
|
75
|
+
let writes: Promise<unknown> = Promise.resolve();
|
|
76
|
+
const serialized = <T>(write: () => Promise<T>): Promise<T> => {
|
|
77
|
+
const run = writes.then(write, write);
|
|
78
|
+
writes = run.catch(() => {});
|
|
79
|
+
return run;
|
|
80
|
+
};
|
|
81
|
+
export const createGeoIpService = (options: { db?: typeof sql; reader?: GeoIp; bundled?: () => ReturnType<GeoIp["status"]> } = {}) => {
|
|
82
|
+
const db = options.db ?? sql;
|
|
83
|
+
const reader = () => options.reader ?? getGeoIp();
|
|
84
|
+
const status = async (actor: Actor, config: { now?: () => number } = {}): Promise<GeoIpAdminStatus> => {
|
|
85
|
+
requireAdmin(actor);
|
|
86
|
+
const rows = await db<GeoIpUploadInfo[]>`
|
|
87
|
+
SELECT d.kind, d.sha256, (d.metadata->>'size')::int AS size, d.database_type, d.build_epoch,
|
|
88
|
+
d.metadata->'description'->>'en' AS description, d.uploaded_at, d.uploaded_by,
|
|
89
|
+
COALESCE(NULLIF(u.display_name, ''), u.uid) AS uploader_name
|
|
90
|
+
FROM geoip.databases d LEFT JOIN auth.users u ON u.id = d.uploaded_by`;
|
|
91
|
+
return geoIpAdminStatus(rows, (options.bundled ?? getBundledInfo)(), (config.now ?? Date.now)());
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
status,
|
|
95
|
+
async upload(actor: Actor, body: ReadableStream<Uint8Array>, config: { contentLength?: number; signal?: AbortSignal }) {
|
|
96
|
+
requireAdmin(actor);
|
|
97
|
+
if (uploading) throw new GeoIpError("geoip_upload_busy");
|
|
98
|
+
uploading = true;
|
|
99
|
+
try {
|
|
100
|
+
const bytes = await readGeoIpUpload(body, config.contentLength);
|
|
101
|
+
const { kind, database, metadata } = parseGeoIpUpload(bytes);
|
|
102
|
+
const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
103
|
+
database.info.sha256 = sha256;
|
|
104
|
+
config.signal?.throwIfAborted();
|
|
105
|
+
await serialized(async () => {
|
|
106
|
+
await db.begin(async (tx) => {
|
|
107
|
+
await tx`
|
|
108
|
+
INSERT INTO geoip.databases (kind, data, sha256, database_type, build_epoch, metadata, uploaded_by)
|
|
109
|
+
VALUES (${kind}, ${bytes}, ${sha256}, ${metadata.databaseType}, ${metadata.buildEpoch}::timestamptz,
|
|
110
|
+
${JSON.stringify(metadata)}::jsonb, ${actor.id}::uuid)
|
|
111
|
+
ON CONFLICT (kind) DO UPDATE SET data = EXCLUDED.data, sha256 = EXCLUDED.sha256,
|
|
112
|
+
database_type = EXCLUDED.database_type, build_epoch = EXCLUDED.build_epoch, metadata = EXCLUDED.metadata,
|
|
113
|
+
uploaded_by = EXCLUDED.uploaded_by, uploaded_at = now()`;
|
|
114
|
+
await audit.record(
|
|
115
|
+
{
|
|
116
|
+
action: "core.geoip.upload",
|
|
117
|
+
outcome: "allowed",
|
|
118
|
+
actor: { ...actor, userId: actor.id },
|
|
119
|
+
metadata: { kind, databaseType: metadata.databaseType, buildDate: metadata.buildEpoch, sha256, size: bytes.length },
|
|
120
|
+
},
|
|
121
|
+
tx,
|
|
122
|
+
);
|
|
123
|
+
config.signal?.throwIfAborted();
|
|
124
|
+
});
|
|
125
|
+
reader().install(kind, database);
|
|
126
|
+
});
|
|
127
|
+
return { ...(await status(actor)), kind };
|
|
128
|
+
} finally {
|
|
129
|
+
uploading = false;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
async reset(actor: Actor, kind: GeoIpKind) {
|
|
133
|
+
requireAdmin(actor);
|
|
134
|
+
const changed = await serialized(async () => {
|
|
135
|
+
const deleted = await db.begin(async (tx) => {
|
|
136
|
+
const [row] = await tx<{ sha256: string; database_type: string; build_epoch: Date; size: number }[]>`
|
|
137
|
+
DELETE FROM geoip.databases WHERE kind = ${kind}
|
|
138
|
+
RETURNING sha256, database_type, build_epoch, octet_length(data) AS size`;
|
|
139
|
+
if (!row) return false;
|
|
140
|
+
await audit.record(
|
|
141
|
+
{
|
|
142
|
+
action: "core.geoip.reset",
|
|
143
|
+
outcome: "allowed",
|
|
144
|
+
actor: { ...actor, userId: actor.id },
|
|
145
|
+
metadata: {
|
|
146
|
+
kind,
|
|
147
|
+
databaseType: row.database_type,
|
|
148
|
+
buildDate: row.build_epoch.toISOString(),
|
|
149
|
+
sha256: row.sha256,
|
|
150
|
+
size: row.size,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
tx,
|
|
154
|
+
);
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
reader().install(kind, null);
|
|
158
|
+
return deleted;
|
|
159
|
+
});
|
|
160
|
+
return { ...(await status(actor)), changed };
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
export const geoip = createGeoIpService();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { GEOIP_MAX_DATABASE_BYTES, type GeoIpKind } from "../../contracts/geoip";
|
|
3
|
+
import type { GeoIpUploads } from "../../server/request-context/geoip";
|
|
4
|
+
|
|
5
|
+
const SLICE_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
export const createGeoIpUploads = (db: typeof sql = sql): GeoIpUploads => ({
|
|
7
|
+
async versions() {
|
|
8
|
+
const rows = await db<{ kind: GeoIpKind; sha256: string }[]>`SELECT kind, sha256 FROM geoip.databases`;
|
|
9
|
+
return Object.fromEntries(rows.map((row) => [row.kind, row.sha256]));
|
|
10
|
+
},
|
|
11
|
+
async read(kind, sha256) {
|
|
12
|
+
const [row] = await db<{ size: number }[]>`
|
|
13
|
+
SELECT octet_length(data) AS size FROM geoip.databases WHERE kind = ${kind} AND sha256 = ${sha256}`;
|
|
14
|
+
if (!row) return null;
|
|
15
|
+
if (row.size < 1 || row.size > GEOIP_MAX_DATABASE_BYTES) throw new Error("Invalid GeoIP database size");
|
|
16
|
+
const bytes = Buffer.allocUnsafe(row.size);
|
|
17
|
+
for (let offset = 0; offset < bytes.length; offset += SLICE_BYTES) {
|
|
18
|
+
const length = Math.min(SLICE_BYTES, bytes.length - offset);
|
|
19
|
+
const [slice] = await db<{ data: Uint8Array }[]>`
|
|
20
|
+
SELECT substring(data FROM ${offset + 1} FOR ${length}) AS data
|
|
21
|
+
FROM geoip.databases WHERE kind = ${kind} AND sha256 = ${sha256}`;
|
|
22
|
+
if (!slice) return null;
|
|
23
|
+
if (slice.data.byteLength !== length) throw new Error("Incomplete GeoIP database slice");
|
|
24
|
+
bytes.set(slice.data, offset);
|
|
25
|
+
}
|
|
26
|
+
return bytes;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const geoIpUploads = createGeoIpUploads();
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { createGunzip } from "node:zlib";
|
|
3
|
+
import { Reader } from "mmdb-lib";
|
|
4
|
+
import { GEOIP_MAX_DATABASE_BYTES, GEOIP_MAX_REQUEST_BYTES, type GeoIpErrorCode, type GeoIpKind } from "../../contracts/geoip";
|
|
5
|
+
import { hasBoundedMmdbMetadata, openGeoIpDatabase, truncateUtf8 } from "../../server/request-context/geoip";
|
|
6
|
+
|
|
7
|
+
const probeAddresses = [
|
|
8
|
+
...Array.from({ length: 512 }, (_, i) => [`${i >> 1}.${(i % 2) * 128}.0.0`, `${(0x2000 + i * 16).toString(16)}::`]).flat(),
|
|
9
|
+
"1.1.1.1",
|
|
10
|
+
"8.8.8.8",
|
|
11
|
+
"9.9.9.9",
|
|
12
|
+
"2001:4860:4860::8888",
|
|
13
|
+
"2606:4700:4700::1111",
|
|
14
|
+
"2a01:4f8::1",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const errors = {
|
|
18
|
+
geoip_request_too_large: { status: 413, message: "The request body exceeds 128 MiB. Upload the .mmdb.gz file." },
|
|
19
|
+
geoip_too_large: { status: 413, message: "The GeoIP database exceeds 192 MiB unpacked." },
|
|
20
|
+
geoip_empty: { status: 400, message: "The GeoIP upload is empty." },
|
|
21
|
+
geoip_invalid: { status: 422, message: "Upload a valid IPv6 MMDB database with a build date." },
|
|
22
|
+
geoip_unsupported_type: { status: 422, message: "Upload a City or ASN MMDB database." },
|
|
23
|
+
geoip_upload_busy: { status: 409, message: "Another GeoIP upload is being processed. Try again shortly." },
|
|
24
|
+
} as const;
|
|
25
|
+
export class GeoIpError extends Error {
|
|
26
|
+
readonly status;
|
|
27
|
+
constructor(public readonly code: GeoIpErrorCode) {
|
|
28
|
+
super(errors[code].message);
|
|
29
|
+
this.status = errors[code].status;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Backpressure reaches the request; both limits are enforced before retaining a chunk. */
|
|
34
|
+
export const readGeoIpUpload = async (body: ReadableStream<Uint8Array>, contentLength?: number): Promise<Buffer> => {
|
|
35
|
+
if (contentLength !== undefined && contentLength > GEOIP_MAX_REQUEST_BYTES) throw new GeoIpError("geoip_request_too_large");
|
|
36
|
+
const reader = body.getReader();
|
|
37
|
+
let requestBytes = 0;
|
|
38
|
+
async function* counted() {
|
|
39
|
+
while (true) {
|
|
40
|
+
const chunk = await reader.read();
|
|
41
|
+
if (chunk.done) return;
|
|
42
|
+
requestBytes += chunk.value.byteLength;
|
|
43
|
+
if (requestBytes > GEOIP_MAX_REQUEST_BYTES) throw new GeoIpError("geoip_request_too_large");
|
|
44
|
+
if (chunk.value.byteLength) yield chunk.value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
let source: Readable | undefined;
|
|
48
|
+
let gunzip: ReturnType<typeof createGunzip> | undefined;
|
|
49
|
+
try {
|
|
50
|
+
const chunks = counted();
|
|
51
|
+
const prefix: Uint8Array[] = [];
|
|
52
|
+
let prefixBytes = 0;
|
|
53
|
+
while (prefixBytes < 2) {
|
|
54
|
+
const next = await chunks.next();
|
|
55
|
+
if (next.done) break;
|
|
56
|
+
prefix.push(next.value);
|
|
57
|
+
prefixBytes += next.value.byteLength;
|
|
58
|
+
}
|
|
59
|
+
if (!prefixBytes) throw new GeoIpError("geoip_empty");
|
|
60
|
+
const first = prefix[0]!;
|
|
61
|
+
const gzip = first[0] === 0x1f && (first.byteLength > 1 ? first[1] : prefix[1]?.[0]) === 0x8b;
|
|
62
|
+
source = Readable.from(
|
|
63
|
+
(async function* () {
|
|
64
|
+
yield* prefix;
|
|
65
|
+
yield* chunks;
|
|
66
|
+
})(),
|
|
67
|
+
);
|
|
68
|
+
let output = source;
|
|
69
|
+
if (gzip) {
|
|
70
|
+
gunzip = createGunzip();
|
|
71
|
+
source.on("error", (error) => gunzip?.destroy(error));
|
|
72
|
+
output = source.pipe(gunzip);
|
|
73
|
+
}
|
|
74
|
+
// A resizable backing store avoids retaining all chunks plus a second full-size concatenation.
|
|
75
|
+
const storage = new ArrayBuffer(0, { maxByteLength: GEOIP_MAX_DATABASE_BYTES });
|
|
76
|
+
let size = 0;
|
|
77
|
+
for await (const chunk of output) {
|
|
78
|
+
if (!(chunk instanceof Uint8Array)) throw new GeoIpError("geoip_invalid");
|
|
79
|
+
if (size + chunk.byteLength > GEOIP_MAX_DATABASE_BYTES) throw new GeoIpError("geoip_too_large");
|
|
80
|
+
const needed = size + chunk.byteLength;
|
|
81
|
+
if (needed > storage.byteLength)
|
|
82
|
+
storage.resize(Math.min(GEOIP_MAX_DATABASE_BYTES, Math.ceil(needed / (8 * 1024 * 1024)) * 8 * 1024 * 1024));
|
|
83
|
+
new Uint8Array(storage).set(chunk, size);
|
|
84
|
+
size = needed;
|
|
85
|
+
}
|
|
86
|
+
if (!size) throw new GeoIpError("geoip_empty");
|
|
87
|
+
storage.resize(size);
|
|
88
|
+
return Buffer.from(storage);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof GeoIpError) throw error;
|
|
91
|
+
throw new GeoIpError("geoip_invalid");
|
|
92
|
+
} finally {
|
|
93
|
+
source?.destroy();
|
|
94
|
+
gunzip?.destroy();
|
|
95
|
+
await reader.cancel().catch(() => {});
|
|
96
|
+
reader.releaseLock();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const parseGeoIpUpload = (bytes: Uint8Array) => {
|
|
101
|
+
if (!hasBoundedMmdbMetadata(bytes)) throw new GeoIpError("geoip_invalid");
|
|
102
|
+
try {
|
|
103
|
+
const metadata = new Reader(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)).metadata;
|
|
104
|
+
const type = metadata.databaseType.toLowerCase();
|
|
105
|
+
const kind: GeoIpKind | null = type.includes("city") ? "city" : type.includes("asn") ? "asn" : null;
|
|
106
|
+
if (!kind) throw new GeoIpError("geoip_unsupported_type");
|
|
107
|
+
const database = openGeoIpDatabase(kind, bytes, "uploaded");
|
|
108
|
+
if (!database || Buffer.byteLength(metadata.databaseType) > 128) throw new GeoIpError("geoip_invalid");
|
|
109
|
+
const description: unknown = metadata.description;
|
|
110
|
+
const english =
|
|
111
|
+
typeof description === "object" && description !== null && "en" in description && typeof description.en === "string"
|
|
112
|
+
? description.en
|
|
113
|
+
: "";
|
|
114
|
+
const languages: unknown = metadata.languages;
|
|
115
|
+
const subset = {
|
|
116
|
+
databaseType: metadata.databaseType,
|
|
117
|
+
buildEpoch: metadata.buildEpoch.toISOString(),
|
|
118
|
+
ipVersion: metadata.ipVersion,
|
|
119
|
+
// `languages` is optional in MMDB metadata; mmdb-lib leaves it undefined despite its type.
|
|
120
|
+
languages: (Array.isArray(languages) ? languages : [])
|
|
121
|
+
.filter((language): language is string => typeof language === "string")
|
|
122
|
+
.slice(0, 16)
|
|
123
|
+
.map((language) => truncateUtf8(language, 32)),
|
|
124
|
+
description: { en: truncateUtf8(english, 256) },
|
|
125
|
+
nodeCount: metadata.nodeCount,
|
|
126
|
+
recordSize: metadata.recordSize,
|
|
127
|
+
size: bytes.byteLength,
|
|
128
|
+
};
|
|
129
|
+
if (
|
|
130
|
+
subset.databaseType.includes("\0") ||
|
|
131
|
+
subset.description.en.includes("\0") ||
|
|
132
|
+
subset.languages.some((language) => language.includes("\0"))
|
|
133
|
+
)
|
|
134
|
+
throw new GeoIpError("geoip_invalid");
|
|
135
|
+
// Pretty JSON is larger than PostgreSQL jsonb text, including its separator spaces.
|
|
136
|
+
if (Buffer.byteLength(JSON.stringify(subset, null, 1)) > 4096) throw new GeoIpError("geoip_invalid");
|
|
137
|
+
// A bounded probe catches gross corruption, not every bad record; request lookups still fail closed. Each
|
|
138
|
+
// address is one lookup that keeps nothing: at most 32 KiB of decode work and one decoded record in memory.
|
|
139
|
+
// A crafted file with records at that cap can block this Core process for about ten seconds; normal files take
|
|
140
|
+
// milliseconds.
|
|
141
|
+
for (const address of probeAddresses) {
|
|
142
|
+
const record = database.get(address);
|
|
143
|
+
if (record !== null && (typeof record !== "object" || Array.isArray(record))) throw new GeoIpError("geoip_invalid");
|
|
144
|
+
}
|
|
145
|
+
return { kind, database, metadata: subset };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof GeoIpError) throw error;
|
|
148
|
+
throw new GeoIpError("geoip_invalid");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
@@ -2,9 +2,13 @@ import { type Context, Hono } from "hono";
|
|
|
2
2
|
import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_SESSION_JWKS_PATH, IDENTITY_JWKS_MAX_AGE_SECONDS } from "./constants";
|
|
3
3
|
import { listIdentityJwks } from "./key-ring";
|
|
4
4
|
|
|
5
|
+
// If-None-Match uses weak comparison, including lists and the wildcard.
|
|
6
|
+
const matchesEtag = (header: string | undefined, etag: string): boolean =>
|
|
7
|
+
header?.trim() === "*" || (header?.match(/(?:W\/)?"[^"]*"/g) ?? []).some((tag) => tag.replace(/^W\//, "") === etag.replace(/^W\//, ""));
|
|
8
|
+
|
|
5
9
|
const serveJwks = (purpose: "session" | "invocation") => async (c: Context) => {
|
|
6
10
|
const jwks = await listIdentityJwks(purpose);
|
|
7
|
-
if (c.req.header("if-none-match")
|
|
11
|
+
if (matchesEtag(c.req.header("if-none-match"), jwks.etag)) {
|
|
8
12
|
c.header("ETag", jwks.etag);
|
|
9
13
|
c.header("Cache-Control", `public, max-age=${IDENTITY_JWKS_MAX_AGE_SECONDS}, must-revalidate`);
|
|
10
14
|
return c.body(null, 304);
|
package/src/services/index.ts
CHANGED
|
@@ -114,8 +114,8 @@ export type { AccountLifecycleNotificationSender } from "./account-lifecycle/not
|
|
|
114
114
|
export { lifecycleJobs } from "./account-lifecycle/scheduler";
|
|
115
115
|
|
|
116
116
|
export { settings } from "./settings/namespace";
|
|
117
|
-
export { loadCache, get, set, remove, getAll } from "./settings";
|
|
118
|
-
export type { SettingEntry } from "./settings";
|
|
117
|
+
export { loadCache, get, read, readStates, set, remove, getAll, SettingUnreadableError } from "./settings";
|
|
118
|
+
export type { SettingEntry, SettingRead, StoredSettingState } from "./settings";
|
|
119
119
|
export { SETTINGS, SETTINGS_MAP, registerSettings } from "./settings/defaults";
|
|
120
120
|
export { validateSettingValue, normalizeSettingValue, getSettingLabel } from "./settings/defaults";
|
|
121
121
|
export type { SettingDef, SettingKind, SettingOption } from "./settings/defaults";
|
|
@@ -164,6 +164,9 @@ export {
|
|
|
164
164
|
renderHtmlToPdfWithConfig,
|
|
165
165
|
renderTemplatePdfPreview,
|
|
166
166
|
testGotenberg,
|
|
167
|
+
preparePdfImage,
|
|
168
|
+
PDF_IMAGE_MAX_INPUT_BYTES,
|
|
169
|
+
PDF_IMAGE_MAX_BOXES,
|
|
167
170
|
} from "./pdf";
|
|
168
171
|
export type {
|
|
169
172
|
BuildPresetPdfHtmlInput,
|
|
@@ -184,6 +187,12 @@ export type {
|
|
|
184
187
|
TemplatePdfPreviewError,
|
|
185
188
|
TemplatePdfPreviewPhase,
|
|
186
189
|
TemplatePdfPreviewResult,
|
|
190
|
+
PdfImageBox,
|
|
191
|
+
PdfImageVariant,
|
|
192
|
+
PdfImageErrorCode,
|
|
193
|
+
PreparePdfImageInput,
|
|
194
|
+
PreparePdfImageOptions,
|
|
195
|
+
PreparePdfImageResult,
|
|
187
196
|
} from "./pdf";
|
|
188
197
|
|
|
189
198
|
export { createSyncOpsRoutes, SYNC_OPS_DEAD_LETTER_LIMIT } from "./sync-ops";
|
|
@@ -198,7 +207,16 @@ export type {
|
|
|
198
207
|
|
|
199
208
|
export { latestTopicCursor } from "./topic-cursor";
|
|
200
209
|
export { readAccountCategoryPolicy, isAccountCategoryAllowed } from "./account-category-policy";
|
|
201
|
-
export {
|
|
210
|
+
export {
|
|
211
|
+
AppApprovalError,
|
|
212
|
+
appApproval,
|
|
213
|
+
type AppDeviceAdministrator,
|
|
214
|
+
type AppDeviceEnrollmentNotice,
|
|
215
|
+
type AppApprovalConfig,
|
|
216
|
+
type AppTerminalLogins,
|
|
217
|
+
type AppTerminalLoginInput,
|
|
218
|
+
readAppApprovalConfig,
|
|
219
|
+
} from "./app-approval";
|
|
202
220
|
export { PwaError, pwaDevices, type PwaDeviceAdministrator, type PwaDevicePairedNotice } from "./pwa-devices";
|
|
203
221
|
export { legalConsent } from "./legal-consent";
|
|
204
222
|
|
|
@@ -228,3 +246,14 @@ export {
|
|
|
228
246
|
resolveAllowedNetworkAddresses,
|
|
229
247
|
} from "./network-security";
|
|
230
248
|
export type { NetworkLookup, PublicNetworkAddress } from "./network-security";
|
|
249
|
+
|
|
250
|
+
export { geoip } from "./geoip";
|
|
251
|
+
export {
|
|
252
|
+
knownBrowsers,
|
|
253
|
+
KNOWN_BROWSER_LIMITS,
|
|
254
|
+
SIGN_IN_METHODS,
|
|
255
|
+
type SignInMethod,
|
|
256
|
+
type NewBrowserSignInNotice,
|
|
257
|
+
type NewBrowserSignInNotifier,
|
|
258
|
+
} from "./known-browsers";
|
|
259
|
+
export { LASTING_ACCESS_SIGN_IN_SECONDS, requireConfirmedBrowserSignIn } from "./session/recent";
|