@colixsystems/widget-sdk 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -29
- package/dist/contract.cjs +137 -110
- package/dist/contract.js +147 -119
- package/dist/hooks.js +734 -571
- package/dist/index.d.ts +228 -65
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +75 -2
- package/dist/manifest.js +75 -2
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type WidgetCategory =
|
|
|
12
12
|
| "data"
|
|
13
13
|
| "media"
|
|
14
14
|
| "communication"
|
|
15
|
+
| "administration"
|
|
15
16
|
| "custom";
|
|
16
17
|
|
|
17
18
|
export type WidgetScope = string; // e.g. "datastore.read:orders"
|
|
@@ -126,6 +127,36 @@ export interface WidgetDatastoreTemplate {
|
|
|
126
127
|
tables: WidgetDatastoreTemplateTable[];
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/**
|
|
131
|
+
* REQ-WIDGET-ACTION: a server-side action the widget declares. Each runs in
|
|
132
|
+
* the shared isolated-vm action runner (cron- or record-triggered) — never in
|
|
133
|
+
* the rendered app, so it has no effect on Player ↔ export parity. Operators
|
|
134
|
+
* enable it per tenant from the Properties Panel; it materialises DISABLED
|
|
135
|
+
* until they bind an integration API key (and, for `record_*` triggers, a
|
|
136
|
+
* target table) in the Actions admin page. `triggerTableId` / `apiKeyId` are
|
|
137
|
+
* deliberately absent — those are tenant-local and bound after install.
|
|
138
|
+
*/
|
|
139
|
+
export interface WidgetManifestAction {
|
|
140
|
+
/** Stable, unique within the manifest — the idempotency key for materialise. */
|
|
141
|
+
key: string;
|
|
142
|
+
name: string;
|
|
143
|
+
description?: string;
|
|
144
|
+
triggerType:
|
|
145
|
+
| "schedule"
|
|
146
|
+
| "record_created"
|
|
147
|
+
| "record_updated"
|
|
148
|
+
| "record_deleted";
|
|
149
|
+
/** Required iff `triggerType === "schedule"`. node-cron syntax. */
|
|
150
|
+
scheduleCron?: string;
|
|
151
|
+
/** 100–300000. Defaults to 30000 on materialise. */
|
|
152
|
+
timeoutMs?: number;
|
|
153
|
+
/**
|
|
154
|
+
* Runs against `datastore`, `fetch`, `console`, `record`, `tenantId`,
|
|
155
|
+
* `triggerType`, `triggerTableId` — NOT the React/SDK surface. ≤ 200 KiB.
|
|
156
|
+
*/
|
|
157
|
+
scriptSource: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
129
160
|
export interface WidgetManifest {
|
|
130
161
|
id: string;
|
|
131
162
|
name: string;
|
|
@@ -147,6 +178,12 @@ export interface WidgetManifest {
|
|
|
147
178
|
* structural constraints enforced at submission time.
|
|
148
179
|
*/
|
|
149
180
|
datastoreTemplate?: WidgetDatastoreTemplate;
|
|
181
|
+
/**
|
|
182
|
+
* Optional server-side actions (REQ-WIDGET-ACTION). Operators enable them
|
|
183
|
+
* per tenant from the Properties Panel; each runs in the existing
|
|
184
|
+
* isolated-vm action runner. See `WidgetManifestAction`.
|
|
185
|
+
*/
|
|
186
|
+
actions?: WidgetManifestAction[];
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
export interface ThemeTokens {
|
|
@@ -168,47 +205,162 @@ export interface ThemeTokens {
|
|
|
168
205
|
};
|
|
169
206
|
}
|
|
170
207
|
|
|
208
|
+
// ----------------------------------------------------- injected data clients
|
|
209
|
+
//
|
|
210
|
+
// REQ-WSDK-DOMAIN-CLIENTS. @colixsystems/widget-sdk is CORE ONLY — manifest
|
|
211
|
+
// contract, primitives, rendering, hooks, events, theme/i18n. It owns NO HTTP
|
|
212
|
+
// and depends on NONE of the data SDK packages. The data layer is FOUR domain
|
|
213
|
+
// client packages, each instantiated by the host and injected into
|
|
214
|
+
// WidgetContext. **Widgets never import these packages** — they reach the data
|
|
215
|
+
// surface only through the SDK hooks, which read the injected instances. The
|
|
216
|
+
// shapes below are declared STRUCTURALLY here (widget-sdk must not import the
|
|
217
|
+
// data SDKs); the authoritative typings ship with each client package.
|
|
218
|
+
//
|
|
219
|
+
// Wire/casing: snake_case end to end. The clients send/return snake_case
|
|
220
|
+
// VERBATIM (e.g. `created_at`, `group_ids`, `can_read`, `amount_cents`,
|
|
221
|
+
// `data_type`, `is_active`). There is NO client-side case transform anywhere.
|
|
222
|
+
// Author-defined record column values are passed through verbatim. List
|
|
223
|
+
// methods return the `{ data, meta }` envelope.
|
|
224
|
+
|
|
225
|
+
/** A `{ data, meta }` list envelope, returned verbatim by every `list(...)`. */
|
|
226
|
+
export interface ListEnvelope<T = unknown> {
|
|
227
|
+
data: T[];
|
|
228
|
+
meta?: Record<string, unknown>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Structural shape of the injected `@colixsystems/datastore-client`
|
|
233
|
+
* (`ctx.datastore`). Backs `useDatastoreQuery` / `useDatastoreRecord` /
|
|
234
|
+
* `useDatastoreSchema` / `useDatastoreMutation` / `useRecordPermissions`.
|
|
235
|
+
* Rows and bodies are snake_case verbatim; `list` returns `{ data, meta }`.
|
|
236
|
+
*/
|
|
237
|
+
export interface DatastoreClient {
|
|
238
|
+
tables: {
|
|
239
|
+
list(): Promise<ListEnvelope>;
|
|
240
|
+
get(tableIdOrName: string): Promise<unknown>;
|
|
241
|
+
};
|
|
242
|
+
schema(tableId: string): Promise<unknown>;
|
|
243
|
+
records(tableId: string): {
|
|
244
|
+
list(query?: Query): Promise<ListEnvelope>;
|
|
245
|
+
get(recordId: string): Promise<unknown>;
|
|
246
|
+
create(values: Record<string, unknown>): Promise<unknown>;
|
|
247
|
+
/** PATCH semantics — only the supplied columns are mutated. */
|
|
248
|
+
update(recordId: string, values: Record<string, unknown>): Promise<unknown>;
|
|
249
|
+
delete(recordId: string): Promise<void>;
|
|
250
|
+
aggregate(spec: unknown): Promise<unknown>;
|
|
251
|
+
permissions(recordId: string): {
|
|
252
|
+
list(): Promise<ListEnvelope>;
|
|
253
|
+
grant(body: Record<string, unknown>): Promise<unknown>;
|
|
254
|
+
update(
|
|
255
|
+
permissionId: string,
|
|
256
|
+
patch: Record<string, unknown>,
|
|
257
|
+
): Promise<unknown>;
|
|
258
|
+
revoke(permissionId: string): Promise<void>;
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Structural shape of the injected `@colixsystems/directory-client`
|
|
265
|
+
* (`ctx.directory`). Backs `useDirectory` / `useUsers` (via `.users`) and
|
|
266
|
+
* `useGroups` (via `.groups`). Rows and bodies are snake_case verbatim;
|
|
267
|
+
* `list` returns `{ data, meta }`.
|
|
268
|
+
*/
|
|
269
|
+
export interface DirectoryClient {
|
|
270
|
+
me(): Promise<unknown>;
|
|
271
|
+
users: {
|
|
272
|
+
list(query?: DirectoryQuery): Promise<ListEnvelope>;
|
|
273
|
+
get(userId: string): Promise<unknown>;
|
|
274
|
+
invite(body: Record<string, unknown>): Promise<unknown>;
|
|
275
|
+
deactivate(userId: string): Promise<unknown>;
|
|
276
|
+
reactivate(userId: string): Promise<unknown>;
|
|
277
|
+
};
|
|
278
|
+
groups: {
|
|
279
|
+
list(query?: GroupsQuery): Promise<ListEnvelope>;
|
|
280
|
+
create(body: Record<string, unknown>): Promise<unknown>;
|
|
281
|
+
remove(groupId: string): Promise<void>;
|
|
282
|
+
addMember(groupId: string, userId: string): Promise<void>;
|
|
283
|
+
removeMember(groupId: string, userId: string): Promise<void>;
|
|
284
|
+
listMine(): Promise<ListEnvelope>;
|
|
285
|
+
};
|
|
286
|
+
invites: {
|
|
287
|
+
list(): Promise<ListEnvelope>;
|
|
288
|
+
revoke(inviteId: string): Promise<void>;
|
|
289
|
+
resend(inviteId: string): Promise<unknown>;
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Structural shape of the injected Asset-Manager files client (`ctx.files`).
|
|
295
|
+
* Slimmed to the three top-level ops the host injects — `get` / `list` /
|
|
296
|
+
* `upload`. Backs `useFile`, which calls `ctx.files.get(id)`. The returned
|
|
297
|
+
* file carries an absolute `url` safe to drop into `<Image source>`.
|
|
298
|
+
*/
|
|
299
|
+
export interface FilesClient {
|
|
300
|
+
get(fileId: string): Promise<unknown>;
|
|
301
|
+
list(query?: Record<string, unknown>): Promise<ListEnvelope>;
|
|
302
|
+
upload(formData: unknown): Promise<unknown>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Structural shape of the injected `@colixsystems/payments-client`
|
|
307
|
+
* (`ctx.payments`, REQ-BILL-07-WIDGETPAY). Backs `usePayments`.
|
|
308
|
+
*/
|
|
309
|
+
export interface PaymentsClient {
|
|
310
|
+
requestPayment(body: PaymentRequest): Promise<PaymentResult>;
|
|
311
|
+
getPayment(paymentId: string): Promise<PaymentResult>;
|
|
312
|
+
}
|
|
313
|
+
|
|
171
314
|
export interface WidgetContext<TProps = unknown> {
|
|
172
315
|
props: TProps;
|
|
173
316
|
widget: { id: string; instanceId: string; version: string };
|
|
317
|
+
/** Active end-user identity, snake_case verbatim. `id` is null when anonymous. */
|
|
174
318
|
user: {
|
|
175
319
|
id: string | null;
|
|
176
320
|
email: string | null;
|
|
177
|
-
|
|
321
|
+
display_name: string | null;
|
|
178
322
|
roles: string[];
|
|
179
|
-
|
|
323
|
+
group_ids: string[];
|
|
180
324
|
};
|
|
181
325
|
workspace: {
|
|
182
326
|
id: string;
|
|
183
|
-
|
|
327
|
+
slug: string;
|
|
184
328
|
locale: string;
|
|
185
329
|
theme: ThemeTokens;
|
|
186
330
|
};
|
|
187
331
|
navigation: {
|
|
188
332
|
goTo(pageId: string, params?: Record<string, string>): void;
|
|
189
333
|
goBack(): void;
|
|
334
|
+
push(pageId: string, params?: Record<string, unknown>): void;
|
|
335
|
+
replace(pageId: string, params?: Record<string, unknown>): void;
|
|
336
|
+
back(): void;
|
|
190
337
|
currentRoute: { pageId: string; params: Record<string, string> };
|
|
191
338
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
339
|
+
/** Injected @colixsystems/datastore-client. */
|
|
340
|
+
datastore: DatastoreClient;
|
|
341
|
+
/** Injected @colixsystems/directory-client (users + groups + invites). */
|
|
342
|
+
directory: DirectoryClient;
|
|
343
|
+
/** Injected Asset-Manager files client: { get, list, upload }. */
|
|
344
|
+
files: FilesClient;
|
|
345
|
+
/** Injected @colixsystems/payments-client. */
|
|
346
|
+
payments: PaymentsClient;
|
|
347
|
+
/** Host child-node renderer; backs WidgetTree / useChildRenderer. */
|
|
348
|
+
renderer: { renderNode(node: unknown): unknown };
|
|
196
349
|
events: { emit(eventName: string, payload?: unknown): void };
|
|
197
|
-
payments: {
|
|
198
|
-
requestPayment(args: PaymentRequest): Promise<PaymentResult>;
|
|
199
|
-
getPayment(paymentId: string): Promise<PaymentResult>;
|
|
200
|
-
};
|
|
201
350
|
i18n: {
|
|
202
351
|
locale: string;
|
|
203
|
-
t(key: string,
|
|
352
|
+
t(key: string, fallback?: string): string;
|
|
204
353
|
};
|
|
205
|
-
platform: "web" | "native";
|
|
206
354
|
logger: {
|
|
207
355
|
debug: (...args: unknown[]) => void;
|
|
208
356
|
info: (...args: unknown[]) => void;
|
|
209
357
|
warn: (...args: unknown[]) => void;
|
|
210
358
|
error: (...args: unknown[]) => void;
|
|
211
359
|
};
|
|
360
|
+
/** Optional host toast slot; backs useToast. */
|
|
361
|
+
toast?: {
|
|
362
|
+
showToast(args: { kind?: string; message: string }): void;
|
|
363
|
+
};
|
|
212
364
|
}
|
|
213
365
|
|
|
214
366
|
/**
|
|
@@ -303,8 +455,8 @@ export interface DirectoryQuery {
|
|
|
303
455
|
q?: string;
|
|
304
456
|
/** `"USER"` (default), `"INTEGRATION"`, or `"ALL"`. */
|
|
305
457
|
role?: "USER" | "INTEGRATION" | "ALL";
|
|
306
|
-
/** Filter by active state. */
|
|
307
|
-
|
|
458
|
+
/** Filter by active state (snake_case on the wire). */
|
|
459
|
+
is_active?: boolean;
|
|
308
460
|
limit?: number;
|
|
309
461
|
offset?: number;
|
|
310
462
|
}
|
|
@@ -317,7 +469,9 @@ export interface DirectoryResult {
|
|
|
317
469
|
}
|
|
318
470
|
|
|
319
471
|
/**
|
|
320
|
-
* Read-only user directory hook.
|
|
472
|
+
* Read-only user directory hook. Reads the injected directory-client at
|
|
473
|
+
* `ctx.directory.users.list(query)` (returns the `{ data, meta }` envelope;
|
|
474
|
+
* the hook unwraps `res.data`). Resolves the tenant's app users to snake_case
|
|
321
475
|
* `{ id, name, role }` rows for chat people-lists, @-mention pickers, or
|
|
322
476
|
* author-id → display-name resolution. Requires the
|
|
323
477
|
* `directory.read:users` scope in the widget manifest.
|
|
@@ -387,10 +541,11 @@ export function useDatastoreRecord(
|
|
|
387
541
|
* One column in a table's schema, as returned by `useDatastoreSchema`.
|
|
388
542
|
* Structural metadata only — never row data.
|
|
389
543
|
*/
|
|
544
|
+
// Returned by `ctx.datastore.schema(tableId)` — wire data, snake_case verbatim.
|
|
390
545
|
export interface DatastoreSchemaColumn {
|
|
391
546
|
id: string;
|
|
392
547
|
name: string;
|
|
393
|
-
|
|
548
|
+
data_type:
|
|
394
549
|
| "STRING"
|
|
395
550
|
| "TEXT"
|
|
396
551
|
| "NUMBER"
|
|
@@ -405,11 +560,11 @@ export interface DatastoreSchemaColumn {
|
|
|
405
560
|
| "USER_GROUP";
|
|
406
561
|
required: boolean;
|
|
407
562
|
/** For RELATION columns only. */
|
|
408
|
-
|
|
563
|
+
relation_type?: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY" | null;
|
|
409
564
|
/** For RELATION columns only — the id of the table this column points at. */
|
|
410
|
-
|
|
565
|
+
target_table_id?: string | null;
|
|
411
566
|
/** True when this column is the table's display/identification column. */
|
|
412
|
-
|
|
567
|
+
is_identification?: boolean;
|
|
413
568
|
}
|
|
414
569
|
|
|
415
570
|
export interface DatastoreSchema {
|
|
@@ -462,9 +617,9 @@ export function useFile(fileId: string | null | undefined): {
|
|
|
462
617
|
file: {
|
|
463
618
|
id: string;
|
|
464
619
|
url: string;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
620
|
+
stored_filename?: string;
|
|
621
|
+
mime_type?: string;
|
|
622
|
+
size_bytes?: number;
|
|
468
623
|
[k: string]: unknown;
|
|
469
624
|
} | null;
|
|
470
625
|
loading: boolean;
|
|
@@ -485,9 +640,9 @@ export function useI18n(): {
|
|
|
485
640
|
export function useUser(): {
|
|
486
641
|
id: string | null;
|
|
487
642
|
email: string | null;
|
|
488
|
-
|
|
643
|
+
display_name: string | null;
|
|
489
644
|
roles: string[];
|
|
490
|
-
|
|
645
|
+
group_ids: string[];
|
|
491
646
|
};
|
|
492
647
|
|
|
493
648
|
/**
|
|
@@ -567,13 +722,13 @@ export interface AppUserRow {
|
|
|
567
722
|
name: string;
|
|
568
723
|
email?: string;
|
|
569
724
|
role: "USER" | "INTEGRATION";
|
|
570
|
-
|
|
725
|
+
is_active: boolean;
|
|
571
726
|
}
|
|
572
727
|
|
|
573
728
|
export interface UsersQuery {
|
|
574
729
|
q?: string;
|
|
575
730
|
role?: "USER" | "INTEGRATION" | "ALL";
|
|
576
|
-
|
|
731
|
+
is_active?: boolean;
|
|
577
732
|
limit?: number;
|
|
578
733
|
offset?: number;
|
|
579
734
|
}
|
|
@@ -581,15 +736,15 @@ export interface UsersQuery {
|
|
|
581
736
|
export interface InviteArgs {
|
|
582
737
|
email: string;
|
|
583
738
|
name?: string;
|
|
584
|
-
|
|
739
|
+
group_ids?: string[];
|
|
585
740
|
}
|
|
586
741
|
|
|
587
742
|
export interface AppUserInviteRow {
|
|
588
743
|
id: string;
|
|
589
744
|
email: string;
|
|
590
745
|
status: string;
|
|
591
|
-
|
|
592
|
-
|
|
746
|
+
invited_at?: string;
|
|
747
|
+
expires_at?: string;
|
|
593
748
|
}
|
|
594
749
|
|
|
595
750
|
export interface UsersApi {
|
|
@@ -604,10 +759,12 @@ export interface UsersApi {
|
|
|
604
759
|
}
|
|
605
760
|
|
|
606
761
|
/**
|
|
607
|
-
* AppUser administration. Reads
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
762
|
+
* AppUser administration. Reads through the injected directory-client at
|
|
763
|
+
* `ctx.directory.users.{list,get,invite,deactivate,reactivate}`. Reads
|
|
764
|
+
* require the `users.read:*` scope in the manifest; mutations additionally
|
|
765
|
+
* require `users.write:*`. Widgets that declare the scopes but whose calling
|
|
766
|
+
* APP_USER lacks the corresponding SystemAcl `users.*` capability grant get a
|
|
767
|
+
* `FORBIDDEN` DirectoryError.
|
|
611
768
|
*/
|
|
612
769
|
export function useUsers(query?: UsersQuery): UsersApi;
|
|
613
770
|
|
|
@@ -616,7 +773,7 @@ export function useUsers(query?: UsersQuery): UsersApi;
|
|
|
616
773
|
export interface AppUserGroupRow {
|
|
617
774
|
id: string;
|
|
618
775
|
name: string;
|
|
619
|
-
|
|
776
|
+
member_count?: number;
|
|
620
777
|
}
|
|
621
778
|
|
|
622
779
|
export interface GroupsQuery {
|
|
@@ -637,47 +794,50 @@ export interface GroupsApi {
|
|
|
637
794
|
}
|
|
638
795
|
|
|
639
796
|
/**
|
|
640
|
-
* AppUserGroup administration. Reads
|
|
641
|
-
*
|
|
797
|
+
* AppUserGroup administration. Reads through the injected directory-client at
|
|
798
|
+
* `ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}`.
|
|
799
|
+
* Reads require `groups.read:*`; mutations require `groups.write:*`. Same
|
|
800
|
+
* SystemAcl gating as `useUsers`.
|
|
642
801
|
*/
|
|
643
802
|
export function useGroups(query?: GroupsQuery): GroupsApi;
|
|
644
803
|
|
|
645
804
|
// ----------------------------------------------------- useRecordPermissions
|
|
646
805
|
//
|
|
647
806
|
// REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
648
|
-
// management for a single record.
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
// both
|
|
652
|
-
// returned to widgets so the call site is portable.
|
|
807
|
+
// management for a single record. Reads the injected datastore-client at
|
|
808
|
+
// `ctx.datastore.records(tableId).permissions(recordId)`. Rows and bodies are
|
|
809
|
+
// snake_case VERBATIM — the SDK does NOT transform them. A row carries
|
|
810
|
+
// `user_id` OR `group_id` (both null = a public grant) plus the `can_*` flags.
|
|
653
811
|
|
|
654
812
|
export interface RecordPermission {
|
|
655
813
|
id: string;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
814
|
+
/** Set when the grant targets a user; null otherwise. */
|
|
815
|
+
user_id: string | null;
|
|
816
|
+
/** Set when the grant targets a group; null otherwise. Both null = public. */
|
|
817
|
+
group_id: string | null;
|
|
818
|
+
can_read: boolean;
|
|
819
|
+
can_write: boolean;
|
|
820
|
+
can_delete: boolean;
|
|
821
|
+
can_grant: boolean;
|
|
822
|
+
[k: string]: unknown;
|
|
663
823
|
}
|
|
664
824
|
|
|
665
825
|
export interface RecordPermissionGrantInput {
|
|
666
|
-
/**
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
826
|
+
/** Target a user (omit / null `group_id`). */
|
|
827
|
+
user_id?: string | null;
|
|
828
|
+
/** Target a group (omit / null `user_id`). Both omitted = a public grant. */
|
|
829
|
+
group_id?: string | null;
|
|
830
|
+
can_read?: boolean;
|
|
831
|
+
can_write?: boolean;
|
|
832
|
+
can_delete?: boolean;
|
|
833
|
+
can_grant?: boolean;
|
|
674
834
|
}
|
|
675
835
|
|
|
676
836
|
export interface RecordPermissionUpdateInput {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
837
|
+
can_read?: boolean;
|
|
838
|
+
can_write?: boolean;
|
|
839
|
+
can_delete?: boolean;
|
|
840
|
+
can_grant?: boolean;
|
|
681
841
|
}
|
|
682
842
|
|
|
683
843
|
export interface RecordPermissionsResult {
|
|
@@ -720,10 +880,13 @@ export class PermissionError extends Error {
|
|
|
720
880
|
|
|
721
881
|
/**
|
|
722
882
|
* REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
723
|
-
* management.
|
|
724
|
-
* `
|
|
725
|
-
*
|
|
726
|
-
*
|
|
883
|
+
* management. Reads the injected datastore-client at
|
|
884
|
+
* `ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}`.
|
|
885
|
+
* Requires `acl.write:records` in the manifest's `requestedScopes`. The
|
|
886
|
+
* backend gates the call on `can_grant` for the target record; a widget that
|
|
887
|
+
* declares the scope but whose caller lacks the grant receives
|
|
888
|
+
* `PermissionError { code: "FORBIDDEN" }`. Rows and bodies are snake_case
|
|
889
|
+
* verbatim.
|
|
727
890
|
*
|
|
728
891
|
* When `tableId` OR `recordId` is null / empty, the hook collapses to a
|
|
729
892
|
* stable empty result without a network round-trip; mutation methods
|
package/dist/linter.cjs
CHANGED
|
@@ -287,6 +287,61 @@ function _scopeRules(source, manifest) {
|
|
|
287
287
|
return findings;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// REQ-WIDGET-ACTION — validate manifest-declared server actions. These run in
|
|
291
|
+
// the isolated-vm action runner, NOT in the rendered component, so they are
|
|
292
|
+
// validated structurally here (shape + caps) rather than scanned as component
|
|
293
|
+
// source. Mirrors the validator in manifest.cjs; emits `error` findings so a
|
|
294
|
+
// malformed / oversized / mis-triggered action blocks publish.
|
|
295
|
+
function _manifestActionRules(manifest) {
|
|
296
|
+
const findings = [];
|
|
297
|
+
if (!manifest || manifest.actions === undefined) return findings;
|
|
298
|
+
const validTriggers = new Set(CONTRACT.actionTriggerTypes);
|
|
299
|
+
const maxBytes = CONTRACT.actionScriptMaxBytes;
|
|
300
|
+
const push = (label) =>
|
|
301
|
+
findings.push({ rule: "manifest-action", severity: "error", label, line: 0, snippet: "" });
|
|
302
|
+
if (!Array.isArray(manifest.actions)) {
|
|
303
|
+
push("manifest.actions must be an array (omit it or use [] for none)");
|
|
304
|
+
return findings;
|
|
305
|
+
}
|
|
306
|
+
const seenKeys = new Set();
|
|
307
|
+
for (const a of manifest.actions) {
|
|
308
|
+
if (a === null || typeof a !== "object") {
|
|
309
|
+
push("manifest.actions entries must be objects");
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
if (typeof a.key !== "string" || a.key.length === 0) {
|
|
313
|
+
push("manifest.actions[].key must be a non-empty string");
|
|
314
|
+
} else if (seenKeys.has(a.key)) {
|
|
315
|
+
push(`manifest.actions[].key "${a.key}" is duplicated`);
|
|
316
|
+
} else {
|
|
317
|
+
seenKeys.add(a.key);
|
|
318
|
+
}
|
|
319
|
+
if (typeof a.name !== "string" || a.name.length === 0) {
|
|
320
|
+
push("manifest.actions[].name must be a non-empty string");
|
|
321
|
+
}
|
|
322
|
+
if (!validTriggers.has(a.triggerType)) {
|
|
323
|
+
push(`manifest.actions[].triggerType must be one of ${[...validTriggers].join(", ")}`);
|
|
324
|
+
} else if (a.triggerType === "schedule" && (typeof a.scheduleCron !== "string" || !a.scheduleCron)) {
|
|
325
|
+
push("manifest.actions[].scheduleCron is required when triggerType is 'schedule'");
|
|
326
|
+
}
|
|
327
|
+
if (typeof a.scriptSource !== "string" || a.scriptSource.length === 0) {
|
|
328
|
+
push("manifest.actions[].scriptSource must be a non-empty string");
|
|
329
|
+
} else if (new TextEncoder().encode(a.scriptSource).length > maxBytes) {
|
|
330
|
+
push("manifest.actions[].scriptSource exceeds 200 KiB");
|
|
331
|
+
}
|
|
332
|
+
if (a.timeoutMs !== undefined) {
|
|
333
|
+
const t = Number(a.timeoutMs);
|
|
334
|
+
if (!Number.isFinite(t) || t < 100 || t > 300000) {
|
|
335
|
+
push("manifest.actions[].timeoutMs must be between 100 and 300000");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
339
|
+
push("manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return findings;
|
|
343
|
+
}
|
|
344
|
+
|
|
290
345
|
function lintSource(source, options) {
|
|
291
346
|
if (typeof source !== "string") {
|
|
292
347
|
return {
|
|
@@ -331,6 +386,7 @@ function lintSource(source, options) {
|
|
|
331
386
|
severity: f.severity || "error",
|
|
332
387
|
})),
|
|
333
388
|
);
|
|
389
|
+
findings.push(..._manifestActionRules(options && options.manifest));
|
|
334
390
|
const hasErrors = findings.some((f) => f.severity !== "warning");
|
|
335
391
|
return { ok: !hasErrors, findings };
|
|
336
392
|
}
|
package/dist/linter.js
CHANGED
|
@@ -361,6 +361,61 @@ function _scopeRules(source, manifest) {
|
|
|
361
361
|
* @param {{ manifest?: { requestedScopes?: string[], supportedPlatforms?: string[] } }} [options]
|
|
362
362
|
* @returns {{ ok: boolean, findings: Array<{ rule: string, severity?: "error" | "warning", label: string, line: number, snippet: string }> }}
|
|
363
363
|
*/
|
|
364
|
+
// REQ-WIDGET-ACTION — validate manifest-declared server actions. These run in
|
|
365
|
+
// the isolated-vm action runner, NOT in the rendered component, so they are
|
|
366
|
+
// validated structurally here (shape + caps) rather than scanned as component
|
|
367
|
+
// source. Mirrors the validator in manifest.js; emits `error` findings so a
|
|
368
|
+
// malformed / oversized / mis-triggered action blocks publish.
|
|
369
|
+
function _manifestActionRules(manifest) {
|
|
370
|
+
const findings = [];
|
|
371
|
+
if (!manifest || manifest.actions === undefined) return findings;
|
|
372
|
+
const validTriggers = new Set(CONTRACT.actionTriggerTypes);
|
|
373
|
+
const maxBytes = CONTRACT.actionScriptMaxBytes;
|
|
374
|
+
const push = (label) =>
|
|
375
|
+
findings.push({ rule: "manifest-action", severity: "error", label, line: 0, snippet: "" });
|
|
376
|
+
if (!Array.isArray(manifest.actions)) {
|
|
377
|
+
push("manifest.actions must be an array (omit it or use [] for none)");
|
|
378
|
+
return findings;
|
|
379
|
+
}
|
|
380
|
+
const seenKeys = new Set();
|
|
381
|
+
for (const a of manifest.actions) {
|
|
382
|
+
if (a === null || typeof a !== "object") {
|
|
383
|
+
push("manifest.actions entries must be objects");
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
if (typeof a.key !== "string" || a.key.length === 0) {
|
|
387
|
+
push("manifest.actions[].key must be a non-empty string");
|
|
388
|
+
} else if (seenKeys.has(a.key)) {
|
|
389
|
+
push(`manifest.actions[].key "${a.key}" is duplicated`);
|
|
390
|
+
} else {
|
|
391
|
+
seenKeys.add(a.key);
|
|
392
|
+
}
|
|
393
|
+
if (typeof a.name !== "string" || a.name.length === 0) {
|
|
394
|
+
push("manifest.actions[].name must be a non-empty string");
|
|
395
|
+
}
|
|
396
|
+
if (!validTriggers.has(a.triggerType)) {
|
|
397
|
+
push(`manifest.actions[].triggerType must be one of ${[...validTriggers].join(", ")}`);
|
|
398
|
+
} else if (a.triggerType === "schedule" && (typeof a.scheduleCron !== "string" || !a.scheduleCron)) {
|
|
399
|
+
push("manifest.actions[].scheduleCron is required when triggerType is 'schedule'");
|
|
400
|
+
}
|
|
401
|
+
if (typeof a.scriptSource !== "string" || a.scriptSource.length === 0) {
|
|
402
|
+
push("manifest.actions[].scriptSource must be a non-empty string");
|
|
403
|
+
} else if (new TextEncoder().encode(a.scriptSource).length > maxBytes) {
|
|
404
|
+
push("manifest.actions[].scriptSource exceeds 200 KiB");
|
|
405
|
+
}
|
|
406
|
+
if (a.timeoutMs !== undefined) {
|
|
407
|
+
const t = Number(a.timeoutMs);
|
|
408
|
+
if (!Number.isFinite(t) || t < 100 || t > 300000) {
|
|
409
|
+
push("manifest.actions[].timeoutMs must be between 100 and 300000");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
413
|
+
push("manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return findings;
|
|
417
|
+
}
|
|
418
|
+
|
|
364
419
|
export function lintSource(source, options) {
|
|
365
420
|
if (typeof source !== "string") {
|
|
366
421
|
return {
|
|
@@ -410,6 +465,8 @@ export function lintSource(source, options) {
|
|
|
410
465
|
severity: f.severity || "error",
|
|
411
466
|
})),
|
|
412
467
|
);
|
|
468
|
+
// REQ-WIDGET-ACTION — structural validation of manifest-declared actions.
|
|
469
|
+
findings.push(..._manifestActionRules(options && options.manifest));
|
|
413
470
|
const hasErrors = findings.some((f) => f.severity !== "warning");
|
|
414
471
|
return { ok: !hasErrors, findings };
|
|
415
472
|
}
|