@k2b/cloud 0.22.0 → 0.23.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 +4 -3
- package/scripts/README.md +1 -1
- package/scripts/build-canvas-workers.ts +58 -0
- package/scripts/build.ts +4 -5
- package/src/_internal/canvas-worker.ts +24 -0
- package/src/_internal/define-app.ts +81 -7
- package/src/_internal/page-responses.ts +19 -3
- package/src/_internal/registry-validation.ts +6 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/static-assets.ts +18 -3
- package/src/access/ResourceApiKeys.tsx +30 -30
- package/src/ai/capability-execution.ts +5 -0
- package/src/ai/client/controller.ts +17 -3
- package/src/ai/client/transport.ts +47 -3
- package/src/ai/live-routes.ts +2 -2
- package/src/ai/pdf-render.ts +2 -22
- package/src/ai/routes.ts +5 -4
- package/src/ai/stream.ts +89 -65
- package/src/api/app-approval.ts +5 -1
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +19 -5
- package/src/api/pwa-phone.ts +161 -0
- package/src/api/pwa.ts +179 -0
- package/src/browser/app-session.ts +69 -0
- package/src/browser/live-connection.ts +211 -0
- package/src/browser/live-websocket.ts +31 -11
- package/src/browser/live.ts +2 -0
- package/src/contracts/app.ts +10 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/pwa-paths.ts +6 -0
- package/src/contracts/pwa.ts +156 -0
- package/src/contracts/registry.ts +7 -0
- package/src/contracts/shared.ts +7 -1
- package/src/events/index.ts +2 -0
- package/src/events/live-engine.ts +826 -0
- package/src/events/live-protocol.ts +31 -0
- package/src/events/live.ts +298 -0
- package/src/server/actor.ts +7 -0
- package/src/server/api-client.ts +23 -2
- package/src/server/index.ts +3 -1
- package/src/server/middleware/auth.ts +80 -8
- package/src/server/middleware/openapi.ts +2 -1
- package/src/server/services/access.ts +49 -0
- package/src/server/services/index.ts +1 -0
- package/src/services/app-approval.ts +6 -25
- package/src/services/audit/index.ts +2 -0
- package/src/services/branding/app-icon-source.ts +40 -0
- package/src/services/branding/app-icons.ts +112 -0
- package/src/services/branding/icon-render-worker.ts +69 -0
- package/src/services/branding/icon-svg.ts +22 -0
- package/src/services/identity/invocation-actor.ts +8 -5
- package/src/services/identity/invocation-authority.ts +1 -0
- package/src/services/identity/invocation-token.ts +6 -0
- package/src/services/index.ts +1 -0
- package/src/services/outbox.ts +150 -30
- package/src/services/pairing-secret.ts +16 -0
- package/src/services/pwa-devices.ts +574 -0
- package/src/services/session/index.ts +212 -57
- package/src/services/session/recent.ts +42 -0
- package/src/services/session/user.ts +15 -5
- package/src/shared/index.ts +1 -0
- package/src/{browser → shared}/locale-preference.ts +1 -1
- package/src/shared/markdown/extensions/info-blocks.ts +18 -47
- package/src/shared/markdown/index.ts +5 -14
- package/src/ssr/AppLaunchpad.island.tsx +8 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/Layout.tsx +10 -12
- package/src/ssr/LayoutHeader.tsx +14 -2
- package/src/ssr/LayoutHelp.tsx +2 -4
- package/src/ssr/LayoutRail.tsx +13 -4
- package/src/ssr/MinimalLayoutPreferences.island.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +19 -13
- package/src/ssr/PageError.tsx +23 -1
- package/src/ssr/ProfilePreferences.island.tsx +9 -1
- package/src/ssr/PwaLayout.tsx +69 -0
- package/src/ssr/PwaRuntime.island.tsx +105 -0
- package/src/ssr/TimezoneCookie.island.tsx +7 -1
- package/src/ssr/app-navigation.ts +25 -10
- package/src/ssr/index.ts +8 -1
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/preference-controller.ts +2 -2
- package/src/ssr/profile-actions.ts +3 -1
- package/src/ssr/profile-preferences-messages.ts +2 -0
- package/src/ssr/pwa-messages.ts +24 -0
- package/src/styles/global.css +6 -0
- package/src/styles/utilities-feedback.css +2 -2
- package/scripts/build-pdf-renderer.ts +0 -39
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PWA_AUTH_PATH, PWA_SCOPE } from "./pwa-paths";
|
|
3
|
+
|
|
4
|
+
export { PWA_AUTH_PATH, PWA_SCOPE };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The installable mobile app (preview). One shell application `pwa` owns the
|
|
8
|
+
* manifest scope `/pwa/`; Core owns the phone-side identity endpoints below
|
|
9
|
+
* `/pwa/_auth` and the web-side pairing API.
|
|
10
|
+
*/
|
|
11
|
+
export const PWA_SHELL_APP_ID = "pwa";
|
|
12
|
+
export const PWA_API_PATH = "/api/auth/pwa/v1";
|
|
13
|
+
export const PWA_MANIFEST_PATH = "/pwa/manifest.webmanifest";
|
|
14
|
+
export const PWA_SERVICE_WORKER_PATH = "/pwa/sw.js";
|
|
15
|
+
/** Ids that can never own a part: the shell itself and its own pages `/pwa/settings` and `/pwa/offline`. */
|
|
16
|
+
export const PWA_RESERVED_IDS = ["pwa", "settings", "offline"] as const;
|
|
17
|
+
/** Whether the app with this id may have a part at `/pwa/<id>`: a plain app id that the shell does not reserve. */
|
|
18
|
+
export const isPwaPartId = (id: string): boolean => /^[a-z][a-z0-9-]*$/.test(id) && !PWA_RESERVED_IDS.some((reserved) => reserved === id);
|
|
19
|
+
/**
|
|
20
|
+
* The page canvas of the app (`--k2b-surface-canvas` of `@k2b/ui`) in light and dark. App documents paint it
|
|
21
|
+
* before any stylesheet loads and give it to the status bar.
|
|
22
|
+
*/
|
|
23
|
+
export const PWA_CANVAS_COLORS = { light: "#fafafa", dark: "#090d12" } as const;
|
|
24
|
+
|
|
25
|
+
/** Cookie names. Only `pwa_session` reaches every application; the others use Core-only paths. */
|
|
26
|
+
export const PWA_COOKIES = {
|
|
27
|
+
session: "pwa_session",
|
|
28
|
+
device: "pwa_device",
|
|
29
|
+
pairing: "pwa_pairing",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export const PWA_LIMITS = {
|
|
33
|
+
/** Same as the Cloud Login API. */
|
|
34
|
+
bodyBytes: 8192,
|
|
35
|
+
/** A visible link can be claimed for five minutes. */
|
|
36
|
+
linkClaimSeconds: 300,
|
|
37
|
+
/** Claim, typing the code and completion. */
|
|
38
|
+
pairingSeconds: 600,
|
|
39
|
+
/** Starting a pairing needs a web sign-in from the last ten minutes. */
|
|
40
|
+
recentSessionSeconds: 600,
|
|
41
|
+
/** Wrong codes before the pairing is cancelled. */
|
|
42
|
+
confirmAttempts: 3,
|
|
43
|
+
devicesPerAccount: 20,
|
|
44
|
+
pendingPerAccount: 5,
|
|
45
|
+
/** A paired phone ends after this many days without use. */
|
|
46
|
+
idleDays: 150,
|
|
47
|
+
/** Lifetime of one app session. */
|
|
48
|
+
sessionSeconds: 86_400,
|
|
49
|
+
/** Renew when the app session has less than this left. */
|
|
50
|
+
renewWithinSeconds: 43_200,
|
|
51
|
+
/** Parallel renewals within this window do not re-issue. */
|
|
52
|
+
rotationGraceSeconds: 60,
|
|
53
|
+
pollSeconds: 5,
|
|
54
|
+
nameMaxLength: 80,
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
/** True while a live app with id "pwa" owns "/pwa". Pure; Core uses it for gating. */
|
|
58
|
+
export const isPwaShellAvailable = (apps: readonly { id: string; routes: readonly string[] }[]): boolean =>
|
|
59
|
+
apps.some((app) => app.id === PWA_SHELL_APP_ID && app.routes.includes("/pwa"));
|
|
60
|
+
|
|
61
|
+
const SECRET_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
62
|
+
|
|
63
|
+
/** The link the web shows as QR code and copy text. The secret lives only in the fragment. */
|
|
64
|
+
export const pairingLink = (origin: string, secret: string): string => {
|
|
65
|
+
if (!SECRET_PATTERN.test(secret)) throw new Error("Invalid pairing secret");
|
|
66
|
+
return `${new URL(origin).origin}${PWA_SCOPE}#pair=${secret}`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type PairingLinkResult = { ok: true; secret: string } | { ok: false; reason: "invalid" | "other-cloud" | "cloud-login" };
|
|
70
|
+
|
|
71
|
+
/** Strict parser for scanned or pasted text. Never fetches anything. */
|
|
72
|
+
export const parsePairingLink = (text: string, origin: string): PairingLinkResult => {
|
|
73
|
+
if (text.length > PWA_LIMITS.bodyBytes) return { ok: false, reason: "invalid" };
|
|
74
|
+
let url: URL;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(text.trim());
|
|
77
|
+
} catch {
|
|
78
|
+
return { ok: false, reason: "invalid" };
|
|
79
|
+
}
|
|
80
|
+
if (url.hash.startsWith("#pairing=")) return { ok: false, reason: "cloud-login" };
|
|
81
|
+
if (!url.hash.startsWith("#pair=") || url.pathname !== PWA_SCOPE || url.search || url.username || url.password) {
|
|
82
|
+
return { ok: false, reason: "invalid" };
|
|
83
|
+
}
|
|
84
|
+
if (url.origin !== new URL(origin).origin) return { ok: false, reason: "other-cloud" };
|
|
85
|
+
const secret = url.hash.slice("#pair=".length);
|
|
86
|
+
return SECRET_PATTERN.test(secret) ? { ok: true, secret } : { ok: false, reason: "invalid" };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const Id = z.string().uuid();
|
|
90
|
+
const DateTime = z.string().datetime();
|
|
91
|
+
const Code = z.string().regex(/^\d{6}$/);
|
|
92
|
+
|
|
93
|
+
export const PwaPlatformSchema = z.enum(["ios", "android", "other"]);
|
|
94
|
+
export type PwaPlatform = z.infer<typeof PwaPlatformSchema>;
|
|
95
|
+
export const PwaPairingStateSchema = z.enum(["pending", "claimed", "confirmed", "completed", "cancelled"]);
|
|
96
|
+
export type PwaPairingState = z.infer<typeof PwaPairingStateSchema>;
|
|
97
|
+
/** States of `/pwa/` the launch bounce leads to when it cannot renew. */
|
|
98
|
+
export const PwaLaunchStateSchema = z.enum(["new", "ended", "blocked", "unavailable"]);
|
|
99
|
+
export type PwaLaunchState = z.infer<typeof PwaLaunchStateSchema>;
|
|
100
|
+
|
|
101
|
+
export const PwaErrorCodeSchema = z.enum([
|
|
102
|
+
"UNAVAILABLE",
|
|
103
|
+
"REAUTHENTICATE",
|
|
104
|
+
"FORBIDDEN",
|
|
105
|
+
"ACCOUNT_BLOCKED",
|
|
106
|
+
"INVALID_REQUEST",
|
|
107
|
+
"NOT_FOUND",
|
|
108
|
+
"EXPIRED",
|
|
109
|
+
"ALREADY_USED",
|
|
110
|
+
"WRONG_CODE",
|
|
111
|
+
"CONFLICT",
|
|
112
|
+
"ALREADY_PAIRED",
|
|
113
|
+
"ACCOUNT_MISMATCH",
|
|
114
|
+
"LIMIT_REACHED",
|
|
115
|
+
"UNPAIRED",
|
|
116
|
+
]);
|
|
117
|
+
export type PwaErrorCode = z.infer<typeof PwaErrorCodeSchema>;
|
|
118
|
+
export const PwaErrorSchema = z.object({ code: PwaErrorCodeSchema, message: z.string(), attemptsLeft: z.number().int().optional() });
|
|
119
|
+
|
|
120
|
+
// Web side (`/api/auth/pwa/v1`).
|
|
121
|
+
export const PwaPairingStartResultSchema = z
|
|
122
|
+
.object({ id: Id, secret: z.string().regex(SECRET_PATTERN), claimUntil: DateTime, expiresAt: DateTime })
|
|
123
|
+
.strict();
|
|
124
|
+
export const PwaPairingStatusSchema = z
|
|
125
|
+
.object({
|
|
126
|
+
state: PwaPairingStateSchema,
|
|
127
|
+
claimUntil: DateTime,
|
|
128
|
+
expiresAt: DateTime,
|
|
129
|
+
device: z.object({ name: z.string(), platform: PwaPlatformSchema }).strict().nullable(),
|
|
130
|
+
attemptsLeft: z.number().int().min(0),
|
|
131
|
+
})
|
|
132
|
+
.strict();
|
|
133
|
+
export const PwaPairingConfirmSchema = z.object({ code: Code }).strict();
|
|
134
|
+
export const PwaDeviceViewSchema = z
|
|
135
|
+
.object({
|
|
136
|
+
id: Id,
|
|
137
|
+
name: z.string(),
|
|
138
|
+
platform: PwaPlatformSchema,
|
|
139
|
+
createdAt: DateTime,
|
|
140
|
+
lastUsedAt: DateTime,
|
|
141
|
+
current: z.boolean(),
|
|
142
|
+
})
|
|
143
|
+
.strict();
|
|
144
|
+
export type PwaDeviceView = z.infer<typeof PwaDeviceViewSchema>;
|
|
145
|
+
export const PwaDeviceListSchema = z.object({ items: z.array(PwaDeviceViewSchema) }).strict();
|
|
146
|
+
|
|
147
|
+
// Phone side (`/pwa/_auth`).
|
|
148
|
+
export const PwaClaimSchema = z.object({ secret: z.string().regex(SECRET_PATTERN), platform: PwaPlatformSchema }).strict();
|
|
149
|
+
const Account = z.object({ name: z.string() }).strict();
|
|
150
|
+
export const PwaClaimResultSchema = z.object({ code: Code, account: Account, expiresAt: DateTime }).strict();
|
|
151
|
+
export const PwaCompleteResultSchema = z.discriminatedUnion("state", [
|
|
152
|
+
z.object({ state: z.literal("paired") }).strict(),
|
|
153
|
+
z.object({ state: z.literal("waiting"), code: Code, account: Account, expiresAt: DateTime }).strict(),
|
|
154
|
+
]);
|
|
155
|
+
export const PwaRenewResultSchema = z.object({ renewed: z.boolean(), otherAccount: Account.optional() }).strict();
|
|
156
|
+
export const PwaRenameSchema = z.object({ name: z.string().trim().min(1).max(PWA_LIMITS.nameMaxLength) }).strict();
|
|
@@ -18,6 +18,11 @@ export type AppRegistryNav = {
|
|
|
18
18
|
adminHref?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
export type AppRegistryPwaPart = {
|
|
22
|
+
href: string;
|
|
23
|
+
requiresRoles?: Role[];
|
|
24
|
+
};
|
|
25
|
+
|
|
21
26
|
export type AppRegistryCapabilitySummary = {
|
|
22
27
|
protocolVersion: number;
|
|
23
28
|
manifestHash: string;
|
|
@@ -80,6 +85,8 @@ export type AppRegistryEntry = {
|
|
|
80
85
|
help?: AppRegistryHelpSummary;
|
|
81
86
|
legalLinks?: AppRegistryLegalLink[];
|
|
82
87
|
searchLinks?: readonly AppSearchLink[];
|
|
88
|
+
/** The app's part of the mobile app (preview); `href` is `/pwa/<id>`. Older readers ignore it. */
|
|
89
|
+
pwa?: AppRegistryPwaPart;
|
|
83
90
|
widgets?: AppRegistryWidget[];
|
|
84
91
|
/** Setting keys declared by this app. Used by admin tooling to avoid treating live app-owned settings as legacy. */
|
|
85
92
|
settingKeys?: readonly string[];
|
package/src/contracts/shared.ts
CHANGED
|
@@ -414,7 +414,13 @@ export type InvocationProvenance = {
|
|
|
414
414
|
workloadId?: string;
|
|
415
415
|
};
|
|
416
416
|
|
|
417
|
-
export type UserRequestActor = {
|
|
417
|
+
export type UserRequestActor = {
|
|
418
|
+
kind: "user";
|
|
419
|
+
user: User;
|
|
420
|
+
delegation?: InvocationProvenance;
|
|
421
|
+
/** `"app"` when the person acts through the mobile app's session; see the server's `UserRequestActor`. */
|
|
422
|
+
sessionKind?: "app";
|
|
423
|
+
};
|
|
418
424
|
|
|
419
425
|
export type ServiceAccountRequestActor = {
|
|
420
426
|
kind: "service_account";
|