@k2b/cloud 0.29.0 → 0.30.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 +5 -3
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +111 -3
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +10 -1
- package/src/_internal/registry-validation.ts +5 -0
- package/src/access/PermissionEditor.tsx +43 -24
- package/src/ai/app-skill-store.ts +298 -0
- package/src/ai/app-skills.ts +105 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +68 -9
- package/src/ai/chart-block.ts +380 -0
- package/src/ai/chat/action-sentences.ts +49 -0
- package/src/ai/chat/blocks.tsx +203 -185
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +5 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +12 -26
- package/src/ai/chat/tool-disclosure.tsx +21 -14
- package/src/ai/chat/tool-groups.ts +64 -37
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +4 -4
- package/src/ai/chat/turn-view.tsx +141 -69
- package/src/ai/code-runtime-tools.ts +59 -44
- package/src/ai/default-tool-contracts.ts +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +19 -21
- package/src/ai/index.ts +9 -1
- package/src/ai/migrate.ts +59 -12
- package/src/ai/protocol.ts +9 -4
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/skill-content.ts +54 -0
- package/src/ai/skills-routes.ts +22 -13
- package/src/ai/skills.ts +177 -231
- package/src/ai/store.ts +20 -13
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/types.ts +10 -2
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/admin-outgoing-mail.ts +18 -0
- package/src/api/fanout.ts +128 -0
- package/src/api/help.ts +8 -3
- package/src/api/mcp.ts +4 -1
- package/src/api/search.ts +31 -106
- package/src/api/widgets.ts +259 -100
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/ndjson.ts +34 -0
- package/src/browser/reload.ts +46 -0
- package/src/browser/search-stream.ts +4 -26
- package/src/browser/widgets.ts +28 -0
- package/src/capabilities/server.ts +20 -2
- package/src/cli/access.ts +48 -14
- package/src/cli/admin/ai-skills.ts +59 -39
- package/src/cli/admin/outgoing-mail.ts +30 -2
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/capabilities.ts +24 -0
- package/src/contracts/outgoing-mail.ts +56 -11
- package/src/contracts/pwa-paths.ts +7 -2
- package/src/contracts/pwa.ts +2 -7
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +20 -0
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/services/accounts/users.ts +15 -0
- package/src/services/help/index.ts +18 -5
- package/src/services/help/types.ts +3 -2
- package/src/services/identity/constants.ts +2 -0
- package/src/services/identity/key-ring.ts +123 -42
- package/src/services/identity/runtime-config.ts +13 -2
- package/src/services/outbox.ts +24 -9
- package/src/services/outgoing-mail/index.ts +33 -5
- package/src/services/outgoing-mail/messages.ts +60 -19
- package/src/services/outgoing-mail/store.ts +63 -4
- package/src/shared/network-address.ts +1 -0
- package/src/ssr/AppLaunchpadPanel.tsx +12 -3
- package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
- package/src/ssr/GlobalSearchDialog.tsx +1 -43
- package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
- package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/app-navigation.ts +5 -7
- package/src/ssr/global-search-host.tsx +55 -0
- package/src/ssr/help.ts +3 -3
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +142 -0
- package/src/styles/utilities-markdown-table.css +5 -1
- package/src/ai/code-mode-skill.ts +0 -114
- package/src/ai/data-analysis-skill.ts +0 -10
- package/src/ai/grids-skill.ts +0 -158
- package/src/ai/skill-seeds.ts +0 -560
package/src/cli/access.ts
CHANGED
|
@@ -28,10 +28,24 @@ export type AccessCommandAdapter<TResource extends AccessResource> = {
|
|
|
28
28
|
resourceArgDescription?: string;
|
|
29
29
|
resolveResource: AccessCommandResourceResolver<TResource>;
|
|
30
30
|
list: (ctx: CloudCliContext, resource: TResource) => Promise<AccessEntry[]>;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/** `scope` is one of `scopes`, or undefined when the command line names none. */
|
|
32
|
+
grant: (
|
|
33
|
+
ctx: CloudCliContext,
|
|
34
|
+
resource: TResource,
|
|
35
|
+
principal: Principal,
|
|
36
|
+
permission: AccessPermission,
|
|
37
|
+
scope?: string,
|
|
38
|
+
) => Promise<AccessEntry>;
|
|
39
|
+
/** `scope` is one of `scopes`, or undefined to keep the grant's scope. */
|
|
40
|
+
update: (ctx: CloudCliContext, resource: TResource, accessId: string, permission: AccessPermission, scope?: string) => Promise<void>;
|
|
33
41
|
revoke: (ctx: CloudCliContext, resource: TResource, accessId: string) => Promise<void>;
|
|
34
42
|
allowedPermissions?: readonly AccessPermission[];
|
|
43
|
+
/**
|
|
44
|
+
* Parts of the resource a grant can cover, as the resource's access API names them in `scope`:
|
|
45
|
+
* the whole resource first (shown for entries without a scope), then narrower parts. Adds
|
|
46
|
+
* `--scope` to `grant` and `set` and a SCOPE column to `list`.
|
|
47
|
+
*/
|
|
48
|
+
scopes?: readonly [string, ...string[]];
|
|
35
49
|
allowPublic?: boolean;
|
|
36
50
|
allowAuthenticated?: boolean;
|
|
37
51
|
allowServiceAccounts?: boolean;
|
|
@@ -243,7 +257,7 @@ const entryTypeLabel = (entry: AccessEntry): string => {
|
|
|
243
257
|
};
|
|
244
258
|
|
|
245
259
|
// Resource-bound service accounts back resource API keys, which are managed with those keys.
|
|
246
|
-
const accessRows = (entries: AccessEntry[], options: { includeServiceAccounts?: boolean } = {}) =>
|
|
260
|
+
const accessRows = (entries: AccessEntry[], options: { includeServiceAccounts?: boolean; scopes?: readonly string[] } = {}) =>
|
|
247
261
|
entries
|
|
248
262
|
.filter((entry) => options.includeServiceAccounts || entry.serviceAccountKind !== "resource_bound")
|
|
249
263
|
.sort((a, b) => {
|
|
@@ -256,6 +270,7 @@ const accessRows = (entries: AccessEntry[], options: { includeServiceAccounts?:
|
|
|
256
270
|
principal: entryDisplayName(entry),
|
|
257
271
|
type: entryTypeLabel(entry),
|
|
258
272
|
permission: entry.permission,
|
|
273
|
+
...(options.scopes ? { scope: entry.scope ?? options.scopes[0] } : {}),
|
|
259
274
|
createdAt: entry.createdAt,
|
|
260
275
|
}));
|
|
261
276
|
|
|
@@ -269,7 +284,7 @@ const printStructured = (ctx: CloudCliContext, value: unknown): boolean => {
|
|
|
269
284
|
export const printAccessEntries = (
|
|
270
285
|
ctx: CloudCliContext,
|
|
271
286
|
entries: AccessEntry[],
|
|
272
|
-
options: { includeServiceAccounts?: boolean; jsonValue?: unknown } = {},
|
|
287
|
+
options: { includeServiceAccounts?: boolean; jsonValue?: unknown; scopes?: readonly string[] } = {},
|
|
273
288
|
) => {
|
|
274
289
|
if (printStructured(ctx, options.jsonValue ?? entries)) return;
|
|
275
290
|
const rows = accessRows(entries, options);
|
|
@@ -281,6 +296,7 @@ export const printAccessEntries = (
|
|
|
281
296
|
{ key: "principal", label: "PRINCIPAL" },
|
|
282
297
|
{ key: "type", label: "TYPE" },
|
|
283
298
|
{ key: "permission", label: "PERMISSION" },
|
|
299
|
+
...(options.scopes ? [{ key: "scope" as const, label: "SCOPE" }] : []),
|
|
284
300
|
{ key: "accessId", label: "ACCESS ID" },
|
|
285
301
|
]);
|
|
286
302
|
};
|
|
@@ -349,6 +365,19 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
349
365
|
includeServiceAccountFlag: adapter.allowServiceAccounts === true,
|
|
350
366
|
};
|
|
351
367
|
const resolve = (ctx: CloudCliContext, args: string[]) => adapter.resolveResource(ctx, args);
|
|
368
|
+
const scopeFlag: { scope?: ReturnType<typeof flag.string> } = adapter.scopes
|
|
369
|
+
? {
|
|
370
|
+
scope: flag.string({
|
|
371
|
+
description: `Part of the ${adapter.resourceLabel} the grant covers. Allowed: ${adapter.scopes.join(", ")}`,
|
|
372
|
+
}),
|
|
373
|
+
}
|
|
374
|
+
: {};
|
|
375
|
+
const scopeSuffix = (scope: string | undefined): string => (scope && scope !== adapter.scopes?.[0] ? ` (${scope})` : "");
|
|
376
|
+
const scopeOf = (flags: { scope?: string }): string | undefined => {
|
|
377
|
+
if (flags.scope === undefined) return undefined;
|
|
378
|
+
if (!adapter.scopes?.includes(flags.scope)) throw new Error(`Scope must be one of: ${adapter.scopes?.join(", ") ?? "none"}.`);
|
|
379
|
+
return flags.scope;
|
|
380
|
+
};
|
|
352
381
|
const resourceArgs = {
|
|
353
382
|
args: arg.rest({
|
|
354
383
|
valueLabel: adapter.resourceArgLabel ?? adapter.resourceLabel,
|
|
@@ -374,6 +403,7 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
374
403
|
printAccessEntries(ctx, entries, {
|
|
375
404
|
includeServiceAccounts: flags.includeServiceAccounts,
|
|
376
405
|
jsonValue: { resource, entries },
|
|
406
|
+
scopes: adapter.scopes,
|
|
377
407
|
});
|
|
378
408
|
},
|
|
379
409
|
});
|
|
@@ -385,16 +415,18 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
385
415
|
flags: {
|
|
386
416
|
...principalFlags(principalFlagOptions),
|
|
387
417
|
permission: flag.enum(allowed, { required: true, description: `Permission to grant. Allowed: ${allowed.join(", ")}` }),
|
|
418
|
+
...scopeFlag,
|
|
388
419
|
},
|
|
389
420
|
examples: adapter.examples?.grant,
|
|
390
421
|
async run({ ctx, args, flags }) {
|
|
391
422
|
const resource = await resolve(ctx, args.args);
|
|
392
423
|
const permission = flags.permission as AccessPermission;
|
|
393
424
|
assertAllowedPermission(permission, allowed);
|
|
425
|
+
const scope = scopeOf(flags);
|
|
394
426
|
const principal = await resolveAccessPrincipal(ctx, flags as PrincipalFlags, adapter);
|
|
395
|
-
const entry = await adapter.grant(ctx, resource, principal, permission);
|
|
427
|
+
const entry = await adapter.grant(ctx, resource, principal, permission, scope);
|
|
396
428
|
if (!printStructured(ctx, { resource, entry }))
|
|
397
|
-
ctx.print(`Granted ${permission} on ${resource.label} to ${entryDisplayName(entry)}.`);
|
|
429
|
+
ctx.print(`Granted ${permission}${scopeSuffix(scope)} on ${resource.label} to ${entryDisplayName(entry)}.`);
|
|
398
430
|
},
|
|
399
431
|
});
|
|
400
432
|
|
|
@@ -407,17 +439,19 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
407
439
|
...principalFlags(principalFlagOptions),
|
|
408
440
|
...accessIdFlag,
|
|
409
441
|
permission: flag.enum(allowed, { required: true, description: `Permission to set. Allowed: ${allowed.join(", ")}` }),
|
|
442
|
+
...scopeFlag,
|
|
410
443
|
},
|
|
411
444
|
examples: adapter.examples?.set,
|
|
412
445
|
async run({ ctx, args, flags }) {
|
|
413
446
|
const resource = await resolve(ctx, args.args);
|
|
414
447
|
const permission = flags.permission as AccessPermission;
|
|
415
448
|
assertAllowedPermission(permission, allowed);
|
|
449
|
+
const scope = scopeOf(flags);
|
|
416
450
|
|
|
417
451
|
if (flags.accessId) {
|
|
418
|
-
await adapter.update(ctx, resource, flags.accessId, permission);
|
|
419
|
-
if (!printStructured(ctx, { resource, accessId: flags.accessId, permission, action: "updated" })) {
|
|
420
|
-
ctx.print(`Updated ${flags.accessId} to ${permission} on ${resource.label}.`);
|
|
452
|
+
await adapter.update(ctx, resource, flags.accessId, permission, scope);
|
|
453
|
+
if (!printStructured(ctx, { resource, accessId: flags.accessId, permission, scope, action: "updated" })) {
|
|
454
|
+
ctx.print(`Updated ${flags.accessId} to ${permission}${scopeSuffix(scope)} on ${resource.label}.`);
|
|
421
455
|
}
|
|
422
456
|
return;
|
|
423
457
|
}
|
|
@@ -426,15 +460,15 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
426
460
|
const entries = await adapter.list(ctx, resource);
|
|
427
461
|
const existing = entries.find((entry) => principalKey(entry.principal) === principalKey(principal));
|
|
428
462
|
if (existing) {
|
|
429
|
-
await adapter.update(ctx, resource, existing.id, permission);
|
|
430
|
-
if (!printStructured(ctx, { resource, accessId: existing.id, permission, action: "updated" })) {
|
|
431
|
-
ctx.print(`Updated ${entryDisplayName(existing)} to ${permission} on ${resource.label}.`);
|
|
463
|
+
await adapter.update(ctx, resource, existing.id, permission, scope);
|
|
464
|
+
if (!printStructured(ctx, { resource, accessId: existing.id, permission, scope, action: "updated" })) {
|
|
465
|
+
ctx.print(`Updated ${entryDisplayName(existing)} to ${permission}${scopeSuffix(scope)} on ${resource.label}.`);
|
|
432
466
|
}
|
|
433
467
|
return;
|
|
434
468
|
}
|
|
435
|
-
const entry = await adapter.grant(ctx, resource, principal, permission);
|
|
469
|
+
const entry = await adapter.grant(ctx, resource, principal, permission, scope);
|
|
436
470
|
if (!printStructured(ctx, { resource, entry, action: "created" })) {
|
|
437
|
-
ctx.print(`Granted ${permission} on ${resource.label} to ${entryDisplayName(entry)}.`);
|
|
471
|
+
ctx.print(`Granted ${permission}${scopeSuffix(scope)} on ${resource.label} to ${entryDisplayName(entry)}.`);
|
|
438
472
|
}
|
|
439
473
|
},
|
|
440
474
|
});
|
|
@@ -4,7 +4,7 @@ import { apiGet, apiJson, queryString } from "./shared";
|
|
|
4
4
|
|
|
5
5
|
export const aiSkillCommands = [
|
|
6
6
|
command("ai skills list", {
|
|
7
|
-
summary: "List all Skills with
|
|
7
|
+
summary: "List all Skills with source app, availability, status, and revision (platform admin)",
|
|
8
8
|
flags: {
|
|
9
9
|
search: flag.string(),
|
|
10
10
|
page: flag.int({ min: 1, default: 1 }),
|
|
@@ -15,49 +15,69 @@ export const aiSkillCommands = [
|
|
|
15
15
|
ctx,
|
|
16
16
|
`/api/admin/core/ai-skills${queryString(flags)}`,
|
|
17
17
|
);
|
|
18
|
-
printRows(
|
|
19
|
-
{ key: "shortId" },
|
|
20
|
-
{ key: "name" },
|
|
21
|
-
{ key: "revision" },
|
|
22
|
-
{ key: "templateId" },
|
|
23
|
-
{ key: "templateVersion" },
|
|
24
|
-
{ key: "currentTemplateVersion" },
|
|
25
|
-
{ key: "templateStatus" },
|
|
26
|
-
]);
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
command("ai skills templates", {
|
|
30
|
-
summary: "List current trusted Skill template IDs and versions",
|
|
31
|
-
run: async ({ ctx }) => {
|
|
32
|
-
const result = await apiGet<{ templates: { templateId: string; name: string; version: number }[] }>(
|
|
18
|
+
printRows(
|
|
33
19
|
ctx,
|
|
34
|
-
|
|
20
|
+
result,
|
|
21
|
+
result.items.map((skill) => ({
|
|
22
|
+
...skill,
|
|
23
|
+
app: skill.source?.appName ?? "",
|
|
24
|
+
status: skill.source?.status ?? "",
|
|
25
|
+
available: skill.source?.available ?? "",
|
|
26
|
+
appVersion: skill.source?.appVersion ?? "",
|
|
27
|
+
})),
|
|
28
|
+
[
|
|
29
|
+
{ key: "shortId" },
|
|
30
|
+
{ key: "name" },
|
|
31
|
+
{ key: "revision" },
|
|
32
|
+
{ key: "app" },
|
|
33
|
+
{ key: "status" },
|
|
34
|
+
{ key: "available" },
|
|
35
|
+
{ key: "appVersion" },
|
|
36
|
+
],
|
|
35
37
|
);
|
|
36
|
-
printRows(ctx, result, result.templates, [{ key: "templateId" }, { key: "name" }, { key: "version" }]);
|
|
37
38
|
},
|
|
38
39
|
}),
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
command("ai skills reset", {
|
|
41
|
+
summary: "Replace Skill content and references with the owning app's current version",
|
|
42
|
+
args: { skillId: arg.required() },
|
|
43
|
+
flags: {
|
|
44
|
+
expectedRevision: flag.int({ name: "revision", required: true, min: 1 }),
|
|
45
|
+
expectedAppVersion: flag.string({ name: "app-version", required: true }),
|
|
46
|
+
yes: confirmFlag("Confirm resetting this Skill; export customized content first"),
|
|
47
|
+
},
|
|
48
|
+
run: async ({ ctx, args, flags }) => {
|
|
49
|
+
if (!flags.yes) throw new Error("Review the Skill first, then confirm with --yes.");
|
|
50
|
+
const result = await apiJson(ctx, "POST", `/api/admin/core/ai-skills/${encodeURIComponent(args.skillId)}/reset`, {
|
|
51
|
+
expectedRevision: flags.expectedRevision,
|
|
52
|
+
expectedAppVersion: flags.expectedAppVersion,
|
|
53
|
+
confirmed: true,
|
|
54
|
+
});
|
|
55
|
+
if (!printStructured(ctx, result)) ctx.print("Skill reset to the app version.");
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
...(
|
|
59
|
+
[
|
|
60
|
+
["restore", "Install a deleted app Skill again from the app's version, when its name is free", "App Skill installed."],
|
|
61
|
+
[
|
|
62
|
+
"adopt",
|
|
63
|
+
"Link the existing Skill that holds an app Skill's name to that app, keeping its content",
|
|
64
|
+
"Existing Skill linked to the app.",
|
|
65
|
+
],
|
|
66
|
+
] as const
|
|
67
|
+
).map(([action, summary, done]) =>
|
|
68
|
+
command(`ai skills ${action}`, {
|
|
69
|
+
summary,
|
|
70
|
+
args: { appId: arg.required(), name: arg.required() },
|
|
71
|
+
flags: { yes: confirmFlag(`Confirm the app and Skill name to ${action}`) },
|
|
52
72
|
run: async ({ ctx, args, flags }) => {
|
|
53
|
-
if (!flags.yes) throw new Error("Review the Skill
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
confirmed: true,
|
|
59
|
-
|
|
60
|
-
if (!printStructured(ctx, result)) ctx.print(
|
|
73
|
+
if (!flags.yes) throw new Error("Review the app Skill first, then confirm with --yes.");
|
|
74
|
+
const result = await apiJson(
|
|
75
|
+
ctx,
|
|
76
|
+
"POST",
|
|
77
|
+
`/api/admin/core/ai-skills/apps/${encodeURIComponent(args.appId)}/skills/${encodeURIComponent(args.name)}/${action}`,
|
|
78
|
+
{ confirmed: true },
|
|
79
|
+
);
|
|
80
|
+
if (!printStructured(ctx, result)) ctx.print(done);
|
|
61
81
|
},
|
|
62
82
|
}),
|
|
63
83
|
),
|
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
type AdminMailApp,
|
|
3
3
|
type AdminMailProfile,
|
|
4
4
|
type AdminMailRecord,
|
|
5
|
+
MailLogAccessSchema,
|
|
6
|
+
MailLogAppIdSchema,
|
|
5
7
|
type MailPage,
|
|
6
8
|
MailProfileInputSchema,
|
|
7
9
|
type MailRetention,
|
|
@@ -181,8 +183,16 @@ export const outgoingMailCommands = [
|
|
|
181
183
|
printJsonOrTable(
|
|
182
184
|
ctx,
|
|
183
185
|
result,
|
|
184
|
-
result.items.map((app) => ({ ...app, profiles: app.profiles.join(",") })),
|
|
185
|
-
[
|
|
186
|
+
result.items.map((app) => ({ ...app, reads: app.readDeclared, profiles: app.profiles.join(","), logApps: app.logApps.join(",") })),
|
|
187
|
+
[
|
|
188
|
+
{ key: "appId" },
|
|
189
|
+
{ key: "name" },
|
|
190
|
+
{ key: "declared" },
|
|
191
|
+
{ key: "reads" },
|
|
192
|
+
{ key: "mode" },
|
|
193
|
+
{ key: "profiles" },
|
|
194
|
+
{ key: "logApps" },
|
|
195
|
+
],
|
|
186
196
|
);
|
|
187
197
|
},
|
|
188
198
|
}),
|
|
@@ -212,4 +222,22 @@ export const outgoingMailCommands = [
|
|
|
212
222
|
);
|
|
213
223
|
},
|
|
214
224
|
}),
|
|
225
|
+
command("outgoing-mail apps set-log-access", {
|
|
226
|
+
summary: "Choose which other applications' mail logs an app may read",
|
|
227
|
+
args: { app: arg.required({ valueLabel: "app" }) },
|
|
228
|
+
flags: {
|
|
229
|
+
apps: flag.string({ description: "Comma-separated source application IDs" }),
|
|
230
|
+
ownOnly: flag.boolean({ description: "Remove all other application log grants" }),
|
|
231
|
+
yes: confirmFlag("Confirm application mail log access change"),
|
|
232
|
+
},
|
|
233
|
+
async run({ ctx, args, flags }) {
|
|
234
|
+
confirmed(flags.yes);
|
|
235
|
+
if ((flags.apps !== undefined) === !!flags.ownOnly) throw new Error("Choose exactly one of --apps or --own-only.");
|
|
236
|
+
const reader = MailLogAppIdSchema.safeParse(args.app);
|
|
237
|
+
const value = MailLogAccessSchema.safeParse({ apps: flags.apps?.split(",").map((app) => app.trim()) ?? [] });
|
|
238
|
+
if (!reader.success || args.app === "core" || !value.success || value.data.apps.includes(args.app))
|
|
239
|
+
throw new Error("Choose a valid reader and distinct source apps other than Core or the reader itself.");
|
|
240
|
+
print(ctx, await apiJson(ctx, "PUT", `${root}/apps/${encodeURIComponent(args.app)}/log-access`, value.data));
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
215
243
|
];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The announcements cookie, without schemas: the shell reads and writes it on every page and must not load the
|
|
3
|
+
* announcement validators for that. `AnnouncementCookieStateSchema` in `./announcements` describes the same shape.
|
|
4
|
+
*/
|
|
5
|
+
export const ANNOUNCEMENTS_COOKIE = "cloud_announcements";
|
|
6
|
+
export const ANNOUNCEMENTS_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
|
7
|
+
export const MAX_DISMISSED_BANNER_VERSIONS = 50;
|
|
8
|
+
|
|
9
|
+
export type AnnouncementCookieState = {
|
|
10
|
+
seenAnnouncementVersion: number;
|
|
11
|
+
dismissedBannerVersions: number[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_ANNOUNCEMENT_COOKIE_STATE: AnnouncementCookieState = {
|
|
15
|
+
seenAnnouncementVersion: 0,
|
|
16
|
+
dismissedBannerVersions: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isVersion = (value: unknown, minimum: number): value is number =>
|
|
20
|
+
typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
21
|
+
|
|
22
|
+
/** Any invalid field resets the whole state, as a failed schema parse did. */
|
|
23
|
+
const normalizeCookieState = (value: unknown): AnnouncementCookieState => {
|
|
24
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
25
|
+
const { seenAnnouncementVersion = 0, dismissedBannerVersions = [] } = value as Partial<Record<keyof AnnouncementCookieState, unknown>>;
|
|
26
|
+
if (!isVersion(seenAnnouncementVersion, 0)) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
27
|
+
if (!Array.isArray(dismissedBannerVersions) || !dismissedBannerVersions.every((version) => isVersion(version, 1)))
|
|
28
|
+
return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
29
|
+
const dismissed = [...new Set(dismissedBannerVersions)].sort((a, b) => b - a).slice(0, MAX_DISMISSED_BANNER_VERSIONS);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
seenAnnouncementVersion: Math.max(0, seenAnnouncementVersion),
|
|
33
|
+
dismissedBannerVersions: dismissed,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const parseAnnouncementCookieValue = (value: string | null | undefined): AnnouncementCookieState => {
|
|
38
|
+
if (!value) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
39
|
+
try {
|
|
40
|
+
return normalizeCookieState(JSON.parse(decodeURIComponent(value)));
|
|
41
|
+
} catch {
|
|
42
|
+
return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const parseAnnouncementCookieHeader = (cookieHeader: string | null | undefined): AnnouncementCookieState => {
|
|
47
|
+
if (!cookieHeader) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
48
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${ANNOUNCEMENTS_COOKIE}=([^;]+)`));
|
|
49
|
+
return parseAnnouncementCookieValue(match?.[1]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const serializeAnnouncementCookieState = (state: AnnouncementCookieState): string =>
|
|
53
|
+
encodeURIComponent(JSON.stringify(normalizeCookieState(state)));
|
|
54
|
+
|
|
55
|
+
export const mergeAnnouncementCookieState = (
|
|
56
|
+
current: AnnouncementCookieState,
|
|
57
|
+
patch: Partial<AnnouncementCookieState>,
|
|
58
|
+
): AnnouncementCookieState =>
|
|
59
|
+
normalizeCookieState({
|
|
60
|
+
seenAnnouncementVersion: Math.max(current.seenAnnouncementVersion, patch.seenAnnouncementVersion ?? 0),
|
|
61
|
+
dismissedBannerVersions: [...current.dismissedBannerVersions, ...(patch.dismissedBannerVersions ?? [])],
|
|
62
|
+
});
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
2
|
+
import type { AnnouncementCookieState } from "./announcement-cookie";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
ANNOUNCEMENTS_COOKIE,
|
|
6
|
+
ANNOUNCEMENTS_COOKIE_MAX_AGE_SECONDS,
|
|
7
|
+
type AnnouncementCookieState,
|
|
8
|
+
DEFAULT_ANNOUNCEMENT_COOKIE_STATE,
|
|
9
|
+
MAX_DISMISSED_BANNER_VERSIONS,
|
|
10
|
+
mergeAnnouncementCookieState,
|
|
11
|
+
parseAnnouncementCookieHeader,
|
|
12
|
+
parseAnnouncementCookieValue,
|
|
13
|
+
serializeAnnouncementCookieState,
|
|
14
|
+
} from "./announcement-cookie";
|
|
6
15
|
|
|
7
16
|
export const AnnouncementKindSchema = z.enum(["announcement", "banner"]);
|
|
8
17
|
export type AnnouncementKind = z.infer<typeof AnnouncementKindSchema>;
|
|
@@ -13,13 +22,7 @@ export type AnnouncementTone = z.infer<typeof AnnouncementToneSchema>;
|
|
|
13
22
|
export const AnnouncementCookieStateSchema = z.object({
|
|
14
23
|
seenAnnouncementVersion: z.number().int().nonnegative().default(0),
|
|
15
24
|
dismissedBannerVersions: z.array(z.number().int().positive()).default([]),
|
|
16
|
-
})
|
|
17
|
-
export type AnnouncementCookieState = z.infer<typeof AnnouncementCookieStateSchema>;
|
|
18
|
-
|
|
19
|
-
export const DEFAULT_ANNOUNCEMENT_COOKIE_STATE: AnnouncementCookieState = {
|
|
20
|
-
seenAnnouncementVersion: 0,
|
|
21
|
-
dismissedBannerVersions: [],
|
|
22
|
-
};
|
|
25
|
+
}) satisfies z.ZodType<AnnouncementCookieState>;
|
|
23
26
|
|
|
24
27
|
export const AnnouncementEntrySchema = z.object({
|
|
25
28
|
id: z.uuid(),
|
|
@@ -78,44 +81,3 @@ export const ActiveAnnouncementsResponseSchema = z.object({
|
|
|
78
81
|
latestAnnouncementVersion: z.number().int().nonnegative(),
|
|
79
82
|
});
|
|
80
83
|
export type ActiveAnnouncementsResponse = z.infer<typeof ActiveAnnouncementsResponseSchema>;
|
|
81
|
-
|
|
82
|
-
const normalizeCookieState = (value: unknown): AnnouncementCookieState => {
|
|
83
|
-
const parsed = AnnouncementCookieStateSchema.safeParse(value);
|
|
84
|
-
if (!parsed.success) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
85
|
-
const dismissed = [...new Set(parsed.data.dismissedBannerVersions)]
|
|
86
|
-
.filter((version) => Number.isInteger(version) && version > 0)
|
|
87
|
-
.sort((a, b) => b - a)
|
|
88
|
-
.slice(0, MAX_DISMISSED_BANNER_VERSIONS);
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
seenAnnouncementVersion: Math.max(0, parsed.data.seenAnnouncementVersion),
|
|
92
|
-
dismissedBannerVersions: dismissed,
|
|
93
|
-
};
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export const parseAnnouncementCookieValue = (value: string | null | undefined): AnnouncementCookieState => {
|
|
97
|
-
if (!value) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
98
|
-
try {
|
|
99
|
-
return normalizeCookieState(JSON.parse(decodeURIComponent(value)));
|
|
100
|
-
} catch {
|
|
101
|
-
return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
export const parseAnnouncementCookieHeader = (cookieHeader: string | null | undefined): AnnouncementCookieState => {
|
|
106
|
-
if (!cookieHeader) return DEFAULT_ANNOUNCEMENT_COOKIE_STATE;
|
|
107
|
-
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${ANNOUNCEMENTS_COOKIE}=([^;]+)`));
|
|
108
|
-
return parseAnnouncementCookieValue(match?.[1]);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export const serializeAnnouncementCookieState = (state: AnnouncementCookieState): string =>
|
|
112
|
-
encodeURIComponent(JSON.stringify(normalizeCookieState(state)));
|
|
113
|
-
|
|
114
|
-
export const mergeAnnouncementCookieState = (
|
|
115
|
-
current: AnnouncementCookieState,
|
|
116
|
-
patch: Partial<AnnouncementCookieState>,
|
|
117
|
-
): AnnouncementCookieState =>
|
|
118
|
-
normalizeCookieState({
|
|
119
|
-
seenAnnouncementVersion: Math.max(current.seenAnnouncementVersion, patch.seenAnnouncementVersion ?? 0),
|
|
120
|
-
dismissedBannerVersions: [...current.dismissedBannerVersions, ...(patch.dismissedBannerVersions ?? [])],
|
|
121
|
-
});
|
|
@@ -349,6 +349,26 @@ export type CapabilityUniversalSearchDefinition = {
|
|
|
349
349
|
|
|
350
350
|
export type CapabilitySchemaPresentation = Readonly<Record<string, string>>;
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* What one Action does, in words people read in approvals, receipts, and step rows. Each sentence is a
|
|
354
|
+
* template: `{input.to}` inserts a field of the validated call input by its dotted schema path, and
|
|
355
|
+
* `done` may also read the Action's result data with `{data.id}`. Cloud inserts values as escaped,
|
|
356
|
+
* bounded plain text, formats fields with a `date` or `date-time` schema format for the reader, and
|
|
357
|
+
* names an object by its email, address, or path before its display name. A sentence whose value is
|
|
358
|
+
* missing falls back to the generic wording, so every sentence stays optional. A `done` sentence
|
|
359
|
+
* replaces the result's runtime summary in the receipt.
|
|
360
|
+
*/
|
|
361
|
+
export type CapabilityActionSentences = {
|
|
362
|
+
/** What will happen, as a short instruction: "Send email to {input.to}". */
|
|
363
|
+
approval?: string;
|
|
364
|
+
/** What happened after the Action ran: "Sent email to {input.to}". */
|
|
365
|
+
done?: string;
|
|
366
|
+
/** What did not happen because the person rejected it: "Did not send email to {input.to}". */
|
|
367
|
+
rejected?: string;
|
|
368
|
+
/** What did not happen because the turn ended first: "Email to {input.to} not sent". */
|
|
369
|
+
notRun?: string;
|
|
370
|
+
};
|
|
371
|
+
|
|
352
372
|
export type CapabilityOperationPresentationTranslation = {
|
|
353
373
|
title?: string;
|
|
354
374
|
description?: string;
|
|
@@ -358,6 +378,8 @@ export type CapabilityOperationPresentationTranslation = {
|
|
|
358
378
|
data?: CapabilitySchemaPresentation;
|
|
359
379
|
/** Search-tag presentation keyed by the stable tag token. */
|
|
360
380
|
searchTags?: Readonly<Record<string, { title?: string; description?: string }>>;
|
|
381
|
+
/** Actions only: the complete set of sentences in this locale. */
|
|
382
|
+
sentences?: CapabilityActionSentences;
|
|
361
383
|
};
|
|
362
384
|
|
|
363
385
|
export type CapabilityPresentationTranslation = {
|
|
@@ -370,6 +392,8 @@ export type CapabilityPresentationTranslation = {
|
|
|
370
392
|
export type CapabilityPresentationCatalog = {
|
|
371
393
|
/** Locale of the complete Type, Query, Action, search-tag, and schema presentation. */
|
|
372
394
|
baseLocale: string;
|
|
395
|
+
/** Optional Action sentences in the base locale, keyed by Action local ID. */
|
|
396
|
+
sentences?: Readonly<Record<string, CapabilityActionSentences>>;
|
|
373
397
|
/** Partial presentation overlays with exact -> ancestor -> base fallback. */
|
|
374
398
|
translations: Readonly<Record<string, CapabilityPresentationTranslation>>;
|
|
375
399
|
};
|
|
@@ -2,7 +2,7 @@ import type { PageParams, Paginated, ServiceError } from "@k2b/stdlib";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { type RequestActor, ServiceAccountSchema, UserSchema } from "./shared";
|
|
4
4
|
|
|
5
|
-
export const PlatformPermissionSchema = z.enum(["mail:send"]);
|
|
5
|
+
export const PlatformPermissionSchema = z.enum(["mail:send", "mail:read"]);
|
|
6
6
|
export type PlatformPermission = z.infer<typeof PlatformPermissionSchema>;
|
|
7
7
|
export const MailRetentionSchema = z.object({ contentDays: z.int().min(1).max(36500), recordDays: z.int().min(1).max(36500) }).strict();
|
|
8
8
|
export type MailRetention = z.infer<typeof MailRetentionSchema>;
|
|
@@ -74,6 +74,7 @@ export type MailProfile = z.infer<typeof MailProfileSchema>;
|
|
|
74
74
|
export const MailErrorCodeSchema = z.enum([
|
|
75
75
|
"bad_input",
|
|
76
76
|
"profile_not_allowed",
|
|
77
|
+
"app_not_allowed",
|
|
77
78
|
"profile_required",
|
|
78
79
|
"quota_exceeded",
|
|
79
80
|
"backlog_full",
|
|
@@ -110,14 +111,31 @@ export const AdminMailProfileSchema = MailProfileInputSchema.omit({ smtpPassword
|
|
|
110
111
|
appCount: z.int(),
|
|
111
112
|
});
|
|
112
113
|
export type AdminMailProfile = z.infer<typeof AdminMailProfileSchema>;
|
|
113
|
-
export
|
|
114
|
-
appId: string
|
|
115
|
-
name: string
|
|
116
|
-
registered: boolean
|
|
117
|
-
declared: boolean
|
|
118
|
-
mode: "default"
|
|
119
|
-
profiles: string
|
|
120
|
-
|
|
114
|
+
export const AdminMailAppSchema = z.object({
|
|
115
|
+
appId: z.string(),
|
|
116
|
+
name: z.string(),
|
|
117
|
+
registered: z.boolean(),
|
|
118
|
+
declared: z.boolean(),
|
|
119
|
+
mode: z.enum(["default", "selected"]),
|
|
120
|
+
profiles: z.array(z.string()),
|
|
121
|
+
/** Whether the registered app declares `mail:read`; grants in `logApps` only apply with it. */
|
|
122
|
+
readDeclared: z.boolean(),
|
|
123
|
+
/** Other apps whose send log this app may read, besides its own. */
|
|
124
|
+
logApps: z.array(z.string()),
|
|
125
|
+
});
|
|
126
|
+
export type AdminMailApp = z.infer<typeof AdminMailAppSchema>;
|
|
127
|
+
/** Application IDs as registered, like sender access; Core's own mail is never readable by apps. */
|
|
128
|
+
export const MailLogAppIdSchema = z.string().min(1);
|
|
129
|
+
/** How many other apps' send logs an administrator can let one app read. */
|
|
130
|
+
const MAIL_LOG_GRANT_LIMIT = 100;
|
|
131
|
+
export const MailLogAccessSchema = z
|
|
132
|
+
.object({ apps: z.array(MailLogAppIdSchema).max(MAIL_LOG_GRANT_LIMIT) })
|
|
133
|
+
.strict()
|
|
134
|
+
.refine(
|
|
135
|
+
(value) => new Set(value.apps).size === value.apps.length && !value.apps.includes("core"),
|
|
136
|
+
"Choose distinct apps other than core",
|
|
137
|
+
);
|
|
138
|
+
export type MailLogAccess = z.infer<typeof MailLogAccessSchema>;
|
|
121
139
|
|
|
122
140
|
export const MailStatusSchema = z.enum(["queued", "sending", "sent", "failed", "bounced", "cancelled"]);
|
|
123
141
|
export type MailStatus = z.infer<typeof MailStatusSchema>;
|
|
@@ -218,11 +236,38 @@ export const MailFilterSchema = z
|
|
|
218
236
|
batchId: z.uuid().optional(),
|
|
219
237
|
status: z.array(MailStatusSchema).max(6).optional(),
|
|
220
238
|
since: z.iso.datetime({ offset: true }).optional(),
|
|
239
|
+
/** Exclusive upper bound on `createdAt`. */
|
|
240
|
+
until: z.iso.datetime({ offset: true }).optional(),
|
|
241
|
+
/**
|
|
242
|
+
* Apps whose records to read; omitted means only the calling app. Others need `mail:read` and an
|
|
243
|
+
* administrator grant. Holds everything `mail.readableApps()` returns: the caller and every grant.
|
|
244
|
+
*/
|
|
245
|
+
apps: z
|
|
246
|
+
.array(MailLogAppIdSchema)
|
|
247
|
+
.min(1)
|
|
248
|
+
.max(MAIL_LOG_GRANT_LIMIT + 1)
|
|
249
|
+
.refine((apps) => new Set(apps).size === apps.length, "Choose distinct apps")
|
|
250
|
+
.optional(),
|
|
251
|
+
/**
|
|
252
|
+
* Case-insensitive substring of a recipient address or the subject. Three letters or digits in a row
|
|
253
|
+
* give the trigram index something to look up; without them every record would be a candidate.
|
|
254
|
+
*/
|
|
255
|
+
q: z
|
|
256
|
+
.string()
|
|
257
|
+
.refine((value) => !/[\x00-\x1f\x7f]/.test(value), "Search must not contain control characters")
|
|
258
|
+
.trim()
|
|
259
|
+
.max(200)
|
|
260
|
+
.regex(/[\p{L}\p{N}]{3}/u, "Search needs three letters or digits in a row")
|
|
261
|
+
.optional(),
|
|
262
|
+
/** One recipient address, matched exactly but case-insensitively. */
|
|
263
|
+
recipient: z.string().trim().pipe(mailAddress).optional(),
|
|
221
264
|
})
|
|
222
265
|
.strict();
|
|
223
266
|
export type MailFilter = z.infer<typeof MailFilterSchema>;
|
|
224
267
|
export const MailRecordSchema = z.object({
|
|
225
268
|
id: z.uuid(),
|
|
269
|
+
/** The app that sent the record. */
|
|
270
|
+
appId: z.string(),
|
|
226
271
|
batchId: z.uuid().optional(),
|
|
227
272
|
profile: z.string(),
|
|
228
273
|
ref: ref.optional(),
|
|
@@ -241,7 +286,6 @@ export const MailRecordSchema = z.object({
|
|
|
241
286
|
});
|
|
242
287
|
export type MailRecord = z.infer<typeof MailRecordSchema>;
|
|
243
288
|
export const AdminMailRecordSchema = MailRecordSchema.omit({ text: true }).extend({
|
|
244
|
-
appId: z.string(),
|
|
245
289
|
errorCode: z.string().optional(),
|
|
246
290
|
response: z.string().optional(),
|
|
247
291
|
});
|
|
@@ -252,9 +296,10 @@ export type MailPage<T = MailRecord> = Paginated<T> & { nextCursor?: string };
|
|
|
252
296
|
export const MailPageParamsSchema = z
|
|
253
297
|
.object({ page: z.int().min(1).optional(), perPage: z.int().min(1).max(100).optional(), cursor: z.string().max(1024).optional() })
|
|
254
298
|
.strict();
|
|
255
|
-
export const AdminMailFilterSchema = MailFilterSchema.extend({
|
|
299
|
+
export const AdminMailFilterSchema = MailFilterSchema.omit({ apps: true }).extend({
|
|
256
300
|
app: z.string().min(1).optional(),
|
|
257
301
|
profile: MailProfileKeySchema.optional(),
|
|
302
|
+
/** Administrators search a recipient substring. */
|
|
258
303
|
recipient: z.string().max(320).optional(),
|
|
259
304
|
});
|
|
260
305
|
export type AdminMailFilter = z.infer<typeof AdminMailFilterSchema>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paths of the installable mobile app, without schemas, so browser code that runs on every page can test for
|
|
3
|
-
* app page without loading the contract's validators.
|
|
2
|
+
* Paths and ids of the installable mobile app, without schemas, so browser code that runs on every page can test for
|
|
3
|
+
* an app page without loading the contract's validators.
|
|
4
4
|
*/
|
|
5
5
|
export const PWA_SCOPE = "/pwa/";
|
|
6
6
|
export const PWA_AUTH_PATH = "/pwa/_auth";
|
|
7
|
+
export const PWA_SERVICE_WORKER_PATH = "/pwa/sw.js";
|
|
8
|
+
/** Ids that can never own a part: the shell itself and its own pages `/pwa/settings` and `/pwa/offline`. */
|
|
9
|
+
export const PWA_RESERVED_IDS = ["pwa", "settings", "offline"] as const;
|
|
10
|
+
/** Whether the app with this id may have a part at `/pwa/<id>`: a plain app id that the shell does not reserve. */
|
|
11
|
+
export const isPwaPartId = (id: string): boolean => /^[a-z][a-z0-9-]*$/.test(id) && !PWA_RESERVED_IDS.some((reserved) => reserved === id);
|
package/src/contracts/pwa.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { PWA_AUTH_PATH, PWA_SCOPE } from "./pwa-paths";
|
|
2
|
+
import { isPwaPartId, PWA_AUTH_PATH, PWA_RESERVED_IDS, PWA_SCOPE, PWA_SERVICE_WORKER_PATH } from "./pwa-paths";
|
|
3
3
|
|
|
4
|
-
export { PWA_AUTH_PATH, PWA_SCOPE };
|
|
4
|
+
export { isPwaPartId, PWA_AUTH_PATH, PWA_RESERVED_IDS, PWA_SCOPE, PWA_SERVICE_WORKER_PATH };
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The installable mobile app (preview). One shell application `pwa` owns the
|
|
@@ -11,11 +11,6 @@ export { PWA_AUTH_PATH, PWA_SCOPE };
|
|
|
11
11
|
export const PWA_SHELL_APP_ID = "pwa";
|
|
12
12
|
export const PWA_API_PATH = "/api/auth/pwa/v1";
|
|
13
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
14
|
/**
|
|
20
15
|
* The page canvas of the app (`--k2b-surface-canvas` of `@k2b/ui`) in light and dark. App documents paint it
|
|
21
16
|
* before any stylesheet loads and give it to the status bar.
|