@k2b/cloud 0.44.0 → 0.46.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 +9 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +22 -4
- package/src/ai/index.ts +1 -0
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/protocol.ts +3 -1
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +32 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/shared.ts +2 -1
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +56 -17
- package/src/services/accounts/entities.ts +36 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +26 -17
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +141 -27
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/accounts/users.ts +38 -8
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/styles/global.css +2 -0
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
|
@@ -13,6 +13,7 @@ import type { RequestAuthority } from "../server/middleware/auth";
|
|
|
13
13
|
import type { AppEventDefinition, AppEventSubscriptionDefinition } from "./app-events";
|
|
14
14
|
import { CommandPathSchema } from "./commands";
|
|
15
15
|
import type { AccessSubject, RequestActor, User } from "./shared";
|
|
16
|
+
import { AuthenticatedPrincipalSchema } from "./shared";
|
|
16
17
|
|
|
17
18
|
export const CAPABILITY_PROTOCOL_VERSION = 2 as const;
|
|
18
19
|
export const CAPABILITY_MAX_REQUEST_BYTES = 256 * 1024;
|
|
@@ -121,6 +122,38 @@ export const CloudResourceReferenceSchema = CloudResourceRefSchema.extend({
|
|
|
121
122
|
|
|
122
123
|
export type CloudResourceReference = z.infer<typeof CloudResourceReferenceSchema>;
|
|
123
124
|
|
|
125
|
+
/** Authorized presentation returned as `data.view` by a canonical resource reader. */
|
|
126
|
+
export const CloudResourceReaderViewSchema = z
|
|
127
|
+
.object({
|
|
128
|
+
title: z.string().min(1).max(500),
|
|
129
|
+
context: CloudResourceReferenceSchema.shape.context,
|
|
130
|
+
status: CloudResourceReferenceSchema.shape.status,
|
|
131
|
+
done: z.boolean().optional(),
|
|
132
|
+
permission: z.enum(["read", "write", "admin"]),
|
|
133
|
+
container: z
|
|
134
|
+
.object({ ref: CloudResourceRefSchema, title: z.string().min(1).max(500) })
|
|
135
|
+
.strict()
|
|
136
|
+
.optional(),
|
|
137
|
+
})
|
|
138
|
+
.strict();
|
|
139
|
+
export type CloudResourceReaderView = z.infer<typeof CloudResourceReaderViewSchema>;
|
|
140
|
+
|
|
141
|
+
/** Concrete accounts, reusing the grant principal identities; never a group or wildcard. */
|
|
142
|
+
export const ResourcePrincipalSchema = z.discriminatedUnion("type", [
|
|
143
|
+
AuthenticatedPrincipalSchema.options[0].strict(),
|
|
144
|
+
AuthenticatedPrincipalSchema.options[2].strict(),
|
|
145
|
+
]);
|
|
146
|
+
export type ResourcePrincipal = z.infer<typeof ResourcePrincipalSchema>;
|
|
147
|
+
export const ResourceReadableInputSchema = z
|
|
148
|
+
.object({
|
|
149
|
+
id: CloudResourceRefSchema.shape.id,
|
|
150
|
+
container: CloudResourceRefSchema.optional().describe("Expected current container; required for a contained type."),
|
|
151
|
+
principals: z.array(ResourcePrincipalSchema).max(500).describe("Concrete accounts whose current resource readability is checked."),
|
|
152
|
+
})
|
|
153
|
+
.strict();
|
|
154
|
+
export type ResourceReadableInput = z.output<typeof ResourceReadableInputSchema>;
|
|
155
|
+
export const ResourceReadableDataSchema = z.object({ readable: z.array(ResourcePrincipalSchema).max(500) }).strict();
|
|
156
|
+
|
|
124
157
|
// Consumer permalinks may encode all 512 UTF-16 units of an id (up to 4,608 characters).
|
|
125
158
|
export const CloudResourceHrefSchema = z.string().min(1).max(8192).regex(CLOUD_PATH_PATTERN);
|
|
126
159
|
|
|
@@ -832,6 +865,15 @@ export type CapabilityResourceTypeDefinition = {
|
|
|
832
865
|
/** One colour per kind of resource, so a card or list shows at a glance what it stands for. */
|
|
833
866
|
color?: CapabilityResourceColor;
|
|
834
867
|
reader?: string;
|
|
868
|
+
/** Local type in the same application. Describes containment, never inherited authority. */
|
|
869
|
+
container?: string;
|
|
870
|
+
access?: {
|
|
871
|
+
/** Return only readable input principals, using current grants and account status, never credential scopes. */
|
|
872
|
+
readable?: (
|
|
873
|
+
input: ResourceReadableInput,
|
|
874
|
+
context: CapabilityAppExecutionContext,
|
|
875
|
+
) => readonly ResourcePrincipal[] | Promise<readonly ResourcePrincipal[]>;
|
|
876
|
+
};
|
|
835
877
|
/** Root-relative pages recognized when a person pastes a Cloud link. Requires a reader. */
|
|
836
878
|
pages?: readonly string[];
|
|
837
879
|
};
|
|
@@ -911,7 +953,23 @@ export type CapabilityPresentationCatalog = {
|
|
|
911
953
|
translations: Readonly<Record<string, CapabilityPresentationTranslation>>;
|
|
912
954
|
};
|
|
913
955
|
|
|
956
|
+
/** App authority has no grant-bearing subject, user, nested exchange, or interactive credential. */
|
|
957
|
+
export type CapabilityAppExecutionContext = {
|
|
958
|
+
actor: { kind: "app"; appId: string };
|
|
959
|
+
accessSubject: { type: "none" };
|
|
960
|
+
user: null;
|
|
961
|
+
authority?: never;
|
|
962
|
+
query?: never;
|
|
963
|
+
idempotencyKey?: never;
|
|
964
|
+
locale: string;
|
|
965
|
+
requestId: string;
|
|
966
|
+
origin: CapabilityOrigin;
|
|
967
|
+
via?: CapabilityVia;
|
|
968
|
+
signal: AbortSignal;
|
|
969
|
+
};
|
|
970
|
+
|
|
914
971
|
export type CapabilityQueryDefinition<Input extends z.ZodType = z.ZodType<any>, Data extends z.ZodType = z.ZodType<any>> = {
|
|
972
|
+
appOnly?: never;
|
|
915
973
|
title: string;
|
|
916
974
|
description: string;
|
|
917
975
|
input: Input;
|
|
@@ -929,6 +987,7 @@ export type CapabilityIdempotencyPolicy = "none" | "required";
|
|
|
929
987
|
export type CapabilityActionApprovalPolicy = "rememberable" | "none";
|
|
930
988
|
|
|
931
989
|
export type CapabilityActionDefinition<Input extends z.ZodType = z.ZodType<any>, Data extends z.ZodType = z.ZodType<any>> = {
|
|
990
|
+
appOnly?: never;
|
|
932
991
|
title: string;
|
|
933
992
|
description: string;
|
|
934
993
|
input: Input;
|
|
@@ -980,6 +1039,8 @@ export type CapabilityFileProviderDeclaration = { list: string; read: string; sa
|
|
|
980
1039
|
|
|
981
1040
|
export type CapabilityCalendarSourceDeclaration = { calendars: string; events: string; update?: string; move?: string; delete?: string };
|
|
982
1041
|
|
|
1042
|
+
export type CapabilityDiscussionsDeclaration = { item: string; containers: string; activity?: string };
|
|
1043
|
+
|
|
983
1044
|
export type CapabilityDefinitions = {
|
|
984
1045
|
protocolVersion: typeof CAPABILITY_PROTOCOL_VERSION;
|
|
985
1046
|
presentation?: CapabilityPresentationCatalog;
|
|
@@ -989,6 +1050,7 @@ export type CapabilityDefinitions = {
|
|
|
989
1050
|
commands?: CapabilityDefinitionCatalog<CapabilityCommandDefinition>;
|
|
990
1051
|
fileProvider?: CapabilityFileProviderDeclaration;
|
|
991
1052
|
calendarSource?: CapabilityCalendarSourceDeclaration;
|
|
1053
|
+
discussions?: CapabilityDiscussionsDeclaration;
|
|
992
1054
|
events?: CapabilityDefinitionCatalog<AppEventDefinition>;
|
|
993
1055
|
subscribes?: CapabilityDefinitionCatalog<AppEventSubscriptionDefinition>;
|
|
994
1056
|
};
|
|
@@ -1044,6 +1106,7 @@ const CapabilityOperationManifestBaseSchema = z
|
|
|
1044
1106
|
.strict();
|
|
1045
1107
|
|
|
1046
1108
|
export const CapabilityQueryManifestSchema = CapabilityOperationManifestBaseSchema.extend({
|
|
1109
|
+
appOnly: z.literal(true).optional(),
|
|
1047
1110
|
openWorld: z.boolean(),
|
|
1048
1111
|
universalSearch: z
|
|
1049
1112
|
.object({ tags: z.array(CapabilitySearchTagManifestSchema).max(100), scopeTypes: z.array(CapabilityLocalIdSchema).max(100).optional() })
|
|
@@ -1116,6 +1179,24 @@ export const CapabilityResourcePageManifestSchema = z
|
|
|
1116
1179
|
})
|
|
1117
1180
|
.strict();
|
|
1118
1181
|
|
|
1182
|
+
export const CapabilityDiscussionsManifestSchema = z
|
|
1183
|
+
.object({
|
|
1184
|
+
item: CapabilityLocalIdSchema,
|
|
1185
|
+
containers: CapabilityLocalIdSchema,
|
|
1186
|
+
activity: CapabilityLocalIdSchema.optional(),
|
|
1187
|
+
})
|
|
1188
|
+
.strict();
|
|
1189
|
+
export const CapabilityResourceTypeModelSchema = z
|
|
1190
|
+
.object({
|
|
1191
|
+
type: CapabilityLocalIdSchema,
|
|
1192
|
+
container: CapabilityLocalIdSchema.optional(),
|
|
1193
|
+
view: z.literal(true).optional(),
|
|
1194
|
+
/** Availability claim, retained in person catalogs even though the generated query is hidden. */
|
|
1195
|
+
readable: z.literal(true).optional(),
|
|
1196
|
+
})
|
|
1197
|
+
.strict();
|
|
1198
|
+
export type CapabilityResourceTypeModel = z.infer<typeof CapabilityResourceTypeModelSchema>;
|
|
1199
|
+
|
|
1119
1200
|
/** Keeps the sent entries this release reads completely. */
|
|
1120
1201
|
const readEntries = <T extends z.ZodType>(values: unknown, entry: T): z.output<T>[] | null => {
|
|
1121
1202
|
// Not a bounded list: leave the value to the manifest schema, which rejects it.
|
|
@@ -1146,7 +1227,7 @@ const readEntries = <T extends z.ZodType>(values: unknown, entry: T): z.output<T
|
|
|
1146
1227
|
* pages and pages without a readable Type/reader are omitted; live prefix ownership is checked separately.
|
|
1147
1228
|
* Every other entry stays available.
|
|
1148
1229
|
*/
|
|
1149
|
-
const readableManifest = (value: unknown): unknown => {
|
|
1230
|
+
const readableManifest = (value: unknown, personCatalog = false): unknown => {
|
|
1150
1231
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1151
1232
|
const sent = value as Record<string, unknown>;
|
|
1152
1233
|
const types = readEntries(sent.types, CapabilityResourceTypeManifestSchema);
|
|
@@ -1180,7 +1261,54 @@ const readableManifest = (value: unknown): unknown => {
|
|
|
1180
1261
|
policies[action.localId] = policy;
|
|
1181
1262
|
return true;
|
|
1182
1263
|
});
|
|
1183
|
-
const {
|
|
1264
|
+
const {
|
|
1265
|
+
calendarSource: sentCalendarSource,
|
|
1266
|
+
fileProvider: sentProvider,
|
|
1267
|
+
policies: _policies,
|
|
1268
|
+
resourcePages: sentPages,
|
|
1269
|
+
resourceTypes: sentModels,
|
|
1270
|
+
discussions: sentDiscussions,
|
|
1271
|
+
...rest
|
|
1272
|
+
} = sent;
|
|
1273
|
+
const models = readEntries(sentModels ?? [], CapabilityResourceTypeModelSchema) ?? [];
|
|
1274
|
+
const resourceTypes = models.flatMap((model) => {
|
|
1275
|
+
const type = types.find((type) => type.localId === model.type);
|
|
1276
|
+
if (!type || (model.container && !typeIds.has(model.container))) return [];
|
|
1277
|
+
const seen = new Set([model.type]);
|
|
1278
|
+
let parent = model.container;
|
|
1279
|
+
while (parent) {
|
|
1280
|
+
if (seen.has(parent)) return [];
|
|
1281
|
+
seen.add(parent);
|
|
1282
|
+
parent = models.find((candidate) => candidate.type === parent)?.container;
|
|
1283
|
+
}
|
|
1284
|
+
if (models.filter((candidate) => candidate.type === model.type).length !== 1) return [];
|
|
1285
|
+
const { view, readable, ...rest } = model;
|
|
1286
|
+
return [
|
|
1287
|
+
{
|
|
1288
|
+
...rest,
|
|
1289
|
+
...(readable &&
|
|
1290
|
+
(readableQueries.some((query) => query.localId === `${model.type}.readable` && query.appOnly === true) ||
|
|
1291
|
+
// Core validates the full manifest before hiding app-only queries from person catalogs.
|
|
1292
|
+
(personCatalog &&
|
|
1293
|
+
Array.isArray(sent.queries) &&
|
|
1294
|
+
!sent.queries.some(
|
|
1295
|
+
(query: unknown) =>
|
|
1296
|
+
typeof query === "object" && query !== null && "localId" in query && query.localId === `${model.type}.readable`,
|
|
1297
|
+
)))
|
|
1298
|
+
? { readable }
|
|
1299
|
+
: {}),
|
|
1300
|
+
...(view && type.reader && queryIds.has(type.reader) ? { view } : {}),
|
|
1301
|
+
},
|
|
1302
|
+
];
|
|
1303
|
+
});
|
|
1304
|
+
const role = CapabilityDiscussionsManifestSchema.safeParse(sentDiscussions);
|
|
1305
|
+
const discussions =
|
|
1306
|
+
role.success &&
|
|
1307
|
+
typeIds.has(role.data.item) &&
|
|
1308
|
+
queryIds.has(role.data.containers) &&
|
|
1309
|
+
(!role.data.activity || queryIds.has(role.data.activity))
|
|
1310
|
+
? role.data
|
|
1311
|
+
: undefined;
|
|
1184
1312
|
const provider = CapabilityFileProviderManifestSchema.safeParse(sentProvider);
|
|
1185
1313
|
const calendar = CapabilityCalendarSourceManifestSchema.safeParse(sentCalendarSource);
|
|
1186
1314
|
const pageCounts = new Map<string, number>();
|
|
@@ -1198,6 +1326,8 @@ const readableManifest = (value: unknown): unknown => {
|
|
|
1198
1326
|
: [];
|
|
1199
1327
|
return {
|
|
1200
1328
|
...rest,
|
|
1329
|
+
...(sentModels !== undefined ? { resourceTypes } : {}),
|
|
1330
|
+
...(discussions ? { discussions } : {}),
|
|
1201
1331
|
...(sentPages !== undefined ? { resourcePages } : {}),
|
|
1202
1332
|
...(provider.success ? { fileProvider: provider.data } : {}),
|
|
1203
1333
|
...(calendar.success ? { calendarSource: calendar.data } : {}),
|
|
@@ -1228,7 +1358,7 @@ const readableManifest = (value: unknown): unknown => {
|
|
|
1228
1358
|
* Producers stay strict: `app.start()` registers exactly the manifest its release defines.
|
|
1229
1359
|
*/
|
|
1230
1360
|
export const CapabilityManifestSchema = z.preprocess(
|
|
1231
|
-
readableManifest,
|
|
1361
|
+
(value) => readableManifest(value),
|
|
1232
1362
|
z.object({
|
|
1233
1363
|
protocolVersion: z.literal(CAPABILITY_PROTOCOL_VERSION),
|
|
1234
1364
|
appId: CapabilityAppIdSchema,
|
|
@@ -1240,6 +1370,8 @@ export const CapabilityManifestSchema = z.preprocess(
|
|
|
1240
1370
|
// Optional without a default: a manifest without a provider keeps the shape and hash of earlier releases.
|
|
1241
1371
|
fileProvider: CapabilityFileProviderManifestSchema.optional(),
|
|
1242
1372
|
calendarSource: CapabilityCalendarSourceManifestSchema.optional(),
|
|
1373
|
+
discussions: CapabilityDiscussionsManifestSchema.optional(),
|
|
1374
|
+
resourceTypes: z.array(CapabilityResourceTypeModelSchema).max(MAX_MANIFEST_ENTRIES).optional(),
|
|
1243
1375
|
policies: z.record(CapabilityLocalIdSchema, CapabilityActionPolicySchema).optional(),
|
|
1244
1376
|
events: z.array(CapabilityEventManifestSchema).max(MAX_MANIFEST_ENTRIES).optional(),
|
|
1245
1377
|
subscribes: z.array(CapabilitySubscriptionManifestSchema).max(MAX_MANIFEST_ENTRIES).optional(),
|
|
@@ -1297,7 +1429,8 @@ export const CapabilityCatalogAppSchema = z.object({
|
|
|
1297
1429
|
appName: z.string().min(1).max(200),
|
|
1298
1430
|
appIcon: z.string().min(1).max(120),
|
|
1299
1431
|
appDescription: z.string().max(1000),
|
|
1300
|
-
|
|
1432
|
+
// This is Core's person projection, whose app-only queries have already been hidden.
|
|
1433
|
+
manifest: z.preprocess((value) => readableManifest(value, true), CapabilityManifestSchema.out),
|
|
1301
1434
|
events: z.array(CapabilityEventCatalogEntrySchema).max(MAX_MANIFEST_ENTRIES).optional(),
|
|
1302
1435
|
});
|
|
1303
1436
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CapabilityManifest, CapabilitySemanticLinkSchema, CloudResourceRefSchema } from "./capabilities";
|
|
3
|
+
import { type CapabilityContractIssue, capabilityContractIssues } from "./capability-compatibility";
|
|
4
|
+
import { typeViewIssues } from "./resource-types";
|
|
5
|
+
|
|
6
|
+
/** Same page bound as resource lists and owner activity pages. */
|
|
7
|
+
export const DISCUSSION_PAGE_LIMIT = 100;
|
|
8
|
+
const cursor = z.string().min(1).max(2048).describe("Opaque continuation cursor.");
|
|
9
|
+
export const DiscussionContainersInputSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
query: z.string().max(500).optional().describe("Optional container search text."),
|
|
12
|
+
cursor: cursor.optional(),
|
|
13
|
+
limit: z.number().int().min(1).max(DISCUSSION_PAGE_LIMIT).default(50).describe("Maximum containers in this page."),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
export const DiscussionContainerSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
ref: CloudResourceRefSchema,
|
|
19
|
+
title: z.string().min(1).max(500),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
export const DiscussionContainersDataSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
items: z.array(DiscussionContainerSchema).max(DISCUSSION_PAGE_LIMIT),
|
|
25
|
+
next: cursor.nullable(),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
/** Shared owner activity, independent of the application or discussion consumer. */
|
|
30
|
+
export const ResourceActivityEventSchema = z.object({
|
|
31
|
+
id: z.string().max(100),
|
|
32
|
+
at: z.string().datetime({ offset: true }),
|
|
33
|
+
ref: CloudResourceRefSchema.strip().nullable(),
|
|
34
|
+
title: z.string().max(500).nullable().optional(),
|
|
35
|
+
actor: z.object({ type: z.enum(["user", "service_account", "system"]), id: z.uuid().nullable(), name: z.string().max(1000) }),
|
|
36
|
+
icon: z
|
|
37
|
+
.string()
|
|
38
|
+
.regex(/^ti ti-[a-z0-9-]+$/)
|
|
39
|
+
.max(80),
|
|
40
|
+
sentence: z.string().max(4000),
|
|
41
|
+
href: CapabilitySemanticLinkSchema.shape.href.optional(),
|
|
42
|
+
count: z.number().int().min(1),
|
|
43
|
+
});
|
|
44
|
+
export type ResourceActivityEvent = z.infer<typeof ResourceActivityEventSchema>;
|
|
45
|
+
export const DiscussionActivityInputSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
container: CloudResourceRefSchema.shape.id.describe("Container whose authorized activity is requested."),
|
|
48
|
+
item: CloudResourceRefSchema.shape.id.optional().describe("Optional item inside that container."),
|
|
49
|
+
timeZone: z.string().min(1).max(200).optional().describe("IANA display time zone."),
|
|
50
|
+
before: z.string().datetime({ offset: true }).optional().describe("Exclusive upper timestamp bound."),
|
|
51
|
+
after: z.string().datetime({ offset: true }).optional().describe("Exclusive lower timestamp bound."),
|
|
52
|
+
cursor: cursor.optional(),
|
|
53
|
+
limit: z.number().int().min(1).max(DISCUSSION_PAGE_LIMIT).default(50).describe("Maximum events in this page."),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
export const DiscussionActivityDataSchema = z.array(ResourceActivityEventSchema).max(DISCUSSION_PAGE_LIMIT);
|
|
57
|
+
// The wire contract accepts additive fields; the public consumer schema strips them.
|
|
58
|
+
// Zod's output JSON Schema is closed even for stripping objects, so use an explicit
|
|
59
|
+
// open acceptance shape here rather than treating stripped fields as forbidden.
|
|
60
|
+
const DiscussionActivityWireSchema = z
|
|
61
|
+
.array(
|
|
62
|
+
ResourceActivityEventSchema.loose().extend({
|
|
63
|
+
actor: ResourceActivityEventSchema.shape.actor.loose(),
|
|
64
|
+
ref: CloudResourceRefSchema.loose().nullable(),
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
.max(DISCUSSION_PAGE_LIMIT);
|
|
68
|
+
export type DiscussionContainer = z.infer<typeof DiscussionContainerSchema>;
|
|
69
|
+
|
|
70
|
+
/** Startup/registry check: discovery is never permission to read content. */
|
|
71
|
+
export const discussionsIssues = (manifest: CapabilityManifest): CapabilityContractIssue[] => {
|
|
72
|
+
const role = manifest.discussions;
|
|
73
|
+
if (!role) return [];
|
|
74
|
+
const model = manifest.resourceTypes?.find((model) => model.type === role.item);
|
|
75
|
+
const issues: CapabilityContractIssue[] = [];
|
|
76
|
+
if (!model?.container) issues.push({ code: "kind", path: "$.item", message: "Discussion item must have a container type" });
|
|
77
|
+
for (const localId of [role.item, ...(model?.container ? [model.container] : [])]) issues.push(...typeViewIssues(manifest, localId));
|
|
78
|
+
for (const [localId, input, data] of [
|
|
79
|
+
[role.containers, DiscussionContainersInputSchema, DiscussionContainersDataSchema],
|
|
80
|
+
...(role.activity ? [[role.activity, DiscussionActivityInputSchema, DiscussionActivityWireSchema] as const] : []),
|
|
81
|
+
] as const) {
|
|
82
|
+
const query = manifest.queries.find((query) => query.localId === localId);
|
|
83
|
+
if (!query) issues.push({ code: "kind", path: "$", message: `Missing discussion Query ${localId}` });
|
|
84
|
+
else {
|
|
85
|
+
issues.push(...capabilityContractIssues({ kind: "query", input, data }, { kind: "query", operation: query }));
|
|
86
|
+
if (query.appOnly || query.openWorld)
|
|
87
|
+
issues.push({ code: "kind", path: "$", message: `Discussion Query ${localId} must be closed-world and person-readable` });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return issues;
|
|
91
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { GeoPlaceSchema, GeoPointSchema, GeoProvenanceSchema } from "./geo";
|
|
3
|
+
import { GeoRouteSchema } from "./geo-route";
|
|
4
|
+
|
|
5
|
+
// One place.search result fits a saved map. Routes carry their own bounded stops.
|
|
6
|
+
export const GEO_DOCUMENT_MAX_PINS = 20;
|
|
7
|
+
const shortId = z.string().regex(/^[A-Za-z0-9]{6}$/);
|
|
8
|
+
const label = z.string().trim().min(1).max(300);
|
|
9
|
+
export const GeoDocumentLinkSchema = z.discriminatedUnion("type", [
|
|
10
|
+
z.object({ type: z.literal("url"), url: z.url({ protocol: /^https?$/ }).max(2000), label }).strict(),
|
|
11
|
+
z.object({ type: z.literal("note"), noteId: shortId, label }).strict(),
|
|
12
|
+
z.object({ type: z.literal("heading"), noteId: shortId, heading: z.string().min(1).max(300), label }).strict(),
|
|
13
|
+
]);
|
|
14
|
+
export const GeoDocumentPinSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
id: GeoPlaceSchema.shape.id,
|
|
17
|
+
label,
|
|
18
|
+
point: GeoPointSchema,
|
|
19
|
+
provenance: GeoProvenanceSchema,
|
|
20
|
+
link: GeoDocumentLinkSchema.optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
export const GeoDocumentSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
version: z.literal(1),
|
|
26
|
+
title: label,
|
|
27
|
+
savedAt: z.iso.datetime().optional(),
|
|
28
|
+
pins: z.array(GeoDocumentPinSchema).max(GEO_DOCUMENT_MAX_PINS),
|
|
29
|
+
route: GeoRouteSchema.optional(),
|
|
30
|
+
})
|
|
31
|
+
.strict()
|
|
32
|
+
.superRefine((document, context) => {
|
|
33
|
+
// Each coordinate consists of up to ten continuation bytes and one terminator.
|
|
34
|
+
// Complete latitude/longitude pairs keep malformed authored geometry out of map renderers.
|
|
35
|
+
if (document.route && !/^(?:[_-~]{0,10}[?-^][_-~]{0,10}[?-^])+$/.test(document.route.polyline6)) {
|
|
36
|
+
context.addIssue({
|
|
37
|
+
code: "custom",
|
|
38
|
+
path: ["route", "polyline6"],
|
|
39
|
+
message: "Route geometry must contain complete polyline6 coordinate pairs.",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const ids = new Set<string>();
|
|
43
|
+
document.pins.forEach((pin, index) => {
|
|
44
|
+
if (ids.has(pin.id)) context.addIssue({ code: "custom", path: ["pins", index, "id"], message: "Pin IDs must be unique." });
|
|
45
|
+
ids.add(pin.id);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
export type GeoDocument = z.infer<typeof GeoDocumentSchema>;
|
|
49
|
+
export type GeoDocumentPin = z.infer<typeof GeoDocumentPinSchema>;
|
|
50
|
+
export type GeoDocumentLink = z.infer<typeof GeoDocumentLinkSchema>;
|
|
51
|
+
|
|
52
|
+
/** A portable Notebook block. Escaped JSON cannot close the directive or create HTML. */
|
|
53
|
+
export function serializeGeoDocument(input: GeoDocument): string {
|
|
54
|
+
const document = GeoDocumentSchema.parse(input);
|
|
55
|
+
const json = JSON.stringify(document).replace(
|
|
56
|
+
/[<>&\u2028\u2029]/g,
|
|
57
|
+
(character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
|
|
58
|
+
);
|
|
59
|
+
return `:::map\n${json}\n:::`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Matches the geo result JSON envelope and the existing print decoder budgets.
|
|
4
|
+
export const GEO_SNAPSHOT_MAX_METADATA_BYTES = 256 * 1024;
|
|
5
|
+
export const GEO_SNAPSHOT_MAX_PIXELS = 16 * 1024 * 1024;
|
|
6
|
+
export const GEO_SNAPSHOT_MAX_BYTES = 32 * 1024 * 1024;
|
|
7
|
+
export const GeoMapSnapshotIdSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(100)
|
|
11
|
+
.regex(/^[A-Za-z0-9_-]+$/);
|
|
12
|
+
export const GeoMapSnapshotImageSchema = z
|
|
13
|
+
.strictObject({
|
|
14
|
+
id: GeoMapSnapshotIdSchema,
|
|
15
|
+
fingerprint: z.string().regex(/^[a-f0-9]{64}$/),
|
|
16
|
+
width: z.number().int().min(1).max(16383),
|
|
17
|
+
height: z.number().int().min(1).max(16383),
|
|
18
|
+
})
|
|
19
|
+
.refine((image) => image.width * image.height <= GEO_SNAPSHOT_MAX_PIXELS, "Map image exceeds decoded pixel budget");
|
|
20
|
+
export const GeoMapSnapshotManifestSchema = z
|
|
21
|
+
.strictObject({
|
|
22
|
+
version: z.literal(1),
|
|
23
|
+
revision: z.string().min(1).max(300),
|
|
24
|
+
// Even IDs with one character need a 64-byte SHA-256: this bounds schema work independently of input parsing.
|
|
25
|
+
images: z.array(GeoMapSnapshotImageSchema).max(GEO_SNAPSHOT_MAX_METADATA_BYTES / 64),
|
|
26
|
+
})
|
|
27
|
+
.refine((value) => new Set(value.images.map((image) => image.id)).size === value.images.length, "Duplicate map ID");
|
|
28
|
+
export type GeoMapSnapshotManifest = z.infer<typeof GeoMapSnapshotManifestSchema>;
|
|
29
|
+
export const geoMapSnapshotPart = (id: string) => `map-snapshot-${GeoMapSnapshotIdSchema.parse(id)}`;
|
|
30
|
+
|
|
31
|
+
/** Hash the validated saved GeoDocument, not a lossy geometry projection. Same algorithm in browser and server. */
|
|
32
|
+
export async function geoMapSnapshotFingerprint(value: unknown): Promise<string> {
|
|
33
|
+
// First enforce genuine JSON (and bound traversal) without invoking caller-defined toJSON/accessors.
|
|
34
|
+
let remaining = GEO_SNAPSHOT_MAX_METADATA_BYTES;
|
|
35
|
+
const ancestors = new Set<object>();
|
|
36
|
+
const canonical = (input: unknown): string => {
|
|
37
|
+
if (--remaining < 0) throw new RangeError("Map snapshot input exceeds geo JSON budget");
|
|
38
|
+
if (
|
|
39
|
+
input === null ||
|
|
40
|
+
typeof input === "boolean" ||
|
|
41
|
+
typeof input === "string" ||
|
|
42
|
+
(typeof input === "number" && Number.isFinite(input))
|
|
43
|
+
) {
|
|
44
|
+
const result = JSON.stringify(input);
|
|
45
|
+
remaining -= result.length;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
if (typeof input !== "object" || !input || ancestors.has(input)) throw new TypeError("Map snapshot input must be JSON");
|
|
49
|
+
if (!Array.isArray(input) && Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
|
|
50
|
+
throw new TypeError("Map snapshot input must be plain JSON");
|
|
51
|
+
ancestors.add(input);
|
|
52
|
+
try {
|
|
53
|
+
if (Array.isArray(input)) {
|
|
54
|
+
if (input.length > remaining) throw new RangeError("Map snapshot input exceeds geo JSON budget");
|
|
55
|
+
const items: string[] = [];
|
|
56
|
+
for (let index = 0; index < input.length; index++) {
|
|
57
|
+
const property = Object.getOwnPropertyDescriptor(input, String(index));
|
|
58
|
+
if (!property || !("value" in property)) throw new TypeError("Map snapshot input must be plain JSON");
|
|
59
|
+
items.push(canonical(property.value));
|
|
60
|
+
}
|
|
61
|
+
return `[${items.join(",")}]`;
|
|
62
|
+
}
|
|
63
|
+
return `{${Object.keys(input)
|
|
64
|
+
.sort()
|
|
65
|
+
.map((key) => {
|
|
66
|
+
const property = Object.getOwnPropertyDescriptor(input, key)!;
|
|
67
|
+
if (!("value" in property)) throw new TypeError("Map snapshot input must not contain accessors");
|
|
68
|
+
remaining -= key.length;
|
|
69
|
+
return `${JSON.stringify(key)}:${canonical(property.value)}`;
|
|
70
|
+
})
|
|
71
|
+
.join(",")}}`;
|
|
72
|
+
} finally {
|
|
73
|
+
ancestors.delete(input);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
let serialized: string;
|
|
77
|
+
try {
|
|
78
|
+
serialized = canonical(value);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof RangeError) throw new RangeError("Map snapshot input exceeds geo JSON budget");
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const bytes = new TextEncoder().encode(serialized);
|
|
84
|
+
if (bytes.byteLength > GEO_SNAPSHOT_MAX_METADATA_BYTES) throw new RangeError("Map snapshot input exceeds geo JSON budget");
|
|
85
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
86
|
+
}
|
package/src/contracts/geo.ts
CHANGED
|
@@ -30,6 +30,12 @@ export const GeoAddressSchema = z
|
|
|
30
30
|
point: GeoPointSchema.describe("Resolved geographic point."),
|
|
31
31
|
formatted: z.string().max(600).describe("Formatted provider address."),
|
|
32
32
|
confidence: z.number().min(0).max(1).nullable().describe("Provider match confidence, or null when unavailable."),
|
|
33
|
+
buildingConfidence: z
|
|
34
|
+
.number()
|
|
35
|
+
.min(0)
|
|
36
|
+
.max(1)
|
|
37
|
+
.nullable()
|
|
38
|
+
.describe("Provider certainty that this point is the requested building; null without a house-number match."),
|
|
33
39
|
provenance: GeoProvenanceSchema.describe("Provider source and required attribution."),
|
|
34
40
|
})
|
|
35
41
|
.strict();
|
|
@@ -128,6 +134,13 @@ export const GEO_MAX_POLYLINE_CHARS = 102_400;
|
|
|
128
134
|
export const GeoPlaceSearchInputSchema = z
|
|
129
135
|
.object({
|
|
130
136
|
categories: z.array(GeoCategorySchema).describe("1–5 Cloud categories."),
|
|
137
|
+
name: z
|
|
138
|
+
.string()
|
|
139
|
+
.trim()
|
|
140
|
+
.min(1)
|
|
141
|
+
.max(300)
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Optional provider place-name filter, 1–300 characters after trimming; categories still apply."),
|
|
131
144
|
near: GeoPointSchema.optional().describe("Rank by distance to this point; choose near, area, or along."),
|
|
132
145
|
area: GeoAreaSchema.optional().describe("Optional geographic filter."),
|
|
133
146
|
along: z
|
|
@@ -249,6 +262,7 @@ export type GeoAutocompleteInput = z.infer<typeof GeoAutocompleteInputSchema>;
|
|
|
249
262
|
export type GeoGeocodeInput = z.infer<typeof GeoGeocodeInputSchema>;
|
|
250
263
|
export type GeoReverseInput = z.infer<typeof GeoReverseInputSchema>;
|
|
251
264
|
export type GeoPlaceSearchInput = z.infer<typeof GeoPlaceSearchInputSchema>;
|
|
265
|
+
export type GeoAvailabilityData = z.infer<typeof GeoAvailabilityDataSchema>;
|
|
252
266
|
export type GeoErrorCode = z.infer<typeof GeoErrorDetailsSchema>["code"];
|
|
253
267
|
export type GeoFunction = "autocomplete" | "geocode" | "reverse" | "places" | "route";
|
|
254
268
|
export const geoContract = {
|
package/src/contracts/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./capabilities";
|
|
|
10
10
|
export * from "./capability-compatibility";
|
|
11
11
|
export * from "./commands";
|
|
12
12
|
export * from "./contact-directory";
|
|
13
|
+
export * from "./discussions";
|
|
13
14
|
export * from "./file-provider";
|
|
14
15
|
export * from "./geo";
|
|
15
16
|
export * from "./geo-admin";
|
|
@@ -29,6 +30,7 @@ export type { RailShortcut } from "./rail-preferences";
|
|
|
29
30
|
export * from "./registry";
|
|
30
31
|
export * from "./resource-clipboard";
|
|
31
32
|
export { type ResolvedCloudResourceUrl, ResolvedCloudResourceUrlSchema } from "./resource-resolution";
|
|
33
|
+
export * from "./resource-types";
|
|
32
34
|
export * from "./seen-hints";
|
|
33
35
|
export * from "./shared";
|
|
34
36
|
export * from "./user-notifications";
|
|
@@ -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", "mail:read"]);
|
|
5
|
+
export const PlatformPermissionSchema = z.enum(["mail:send", "mail:read", "resources:readers"]);
|
|
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> & {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CapabilityManifest, CloudResourceReaderViewSchema } from "./capabilities";
|
|
3
|
+
import { type CapabilityContractIssue, capabilityContractIssues } from "./capability-compatibility";
|
|
4
|
+
|
|
5
|
+
/** Check the reader's guaranteed `data.view`; the type model supplies containment. */
|
|
6
|
+
export const typeViewIssues = (
|
|
7
|
+
manifest: Pick<CapabilityManifest, "types" | "queries" | "resourceTypes">,
|
|
8
|
+
localId: string,
|
|
9
|
+
): CapabilityContractIssue[] => {
|
|
10
|
+
const type = manifest.types.find((type) => type.localId === localId);
|
|
11
|
+
const reader = manifest.queries.find((query) => query.localId === type?.reader);
|
|
12
|
+
if (!type || !reader) return [{ code: "kind", path: "$", message: `Type ${localId} needs a reader` }];
|
|
13
|
+
const container = manifest.resourceTypes?.find((model) => model.type === localId)?.container;
|
|
14
|
+
const view = container ? CloudResourceReaderViewSchema.required({ container: true }) : CloudResourceReaderViewSchema;
|
|
15
|
+
const issues = capabilityContractIssues(
|
|
16
|
+
{ kind: "query", input: z.object({}).strict(), data: z.looseObject({ view }) },
|
|
17
|
+
{ kind: "query", operation: reader },
|
|
18
|
+
).filter((issue) => issue.code !== "input");
|
|
19
|
+
if (reader.appOnly) issues.push({ code: "kind", path: "$", message: "A resource reader must accept person authority" });
|
|
20
|
+
return issues;
|
|
21
|
+
};
|
package/src/contracts/shared.ts
CHANGED
|
@@ -146,9 +146,10 @@ export const EntityListItemSchema = z.discriminatedUnion("kind", [
|
|
|
146
146
|
export type EntityListItem = z.infer<typeof EntityListItemSchema>;
|
|
147
147
|
|
|
148
148
|
export const GroupMemberSchema = z.object({
|
|
149
|
-
type: z.enum(["user", "group"]),
|
|
149
|
+
type: z.enum(["user", "group", "service_account"]),
|
|
150
150
|
id: z.string(),
|
|
151
151
|
displayName: z.string().nullable(),
|
|
152
|
+
serviceAccountKind: ServiceAccountKindSchema.optional(),
|
|
152
153
|
});
|
|
153
154
|
export type GroupMember = z.infer<typeof GroupMemberSchema>;
|
|
154
155
|
|