@colixsystems/widget-sdk 0.18.0 → 0.21.1
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 +129 -29
- package/dist/contract.cjs +228 -113
- package/dist/contract.js +238 -122
- package/dist/hooks.js +781 -570
- package/dist/index.d.ts +257 -65
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +157 -2
- package/dist/manifest.js +157 -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,23 @@ 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[];
|
|
187
|
+
/**
|
|
188
|
+
* Optional translation strings the widget ships (REQ-L10N-WIDGET). Maps a
|
|
189
|
+
* relative key to its per-locale strings; `en` is required per key. At
|
|
190
|
+
* install the host merges these into the tenant's localization dictionary
|
|
191
|
+
* under a per-widget namespace (`widget.<id>.<key>`), so `useI18n().t(key)`
|
|
192
|
+
* resolves the namespaced key automatically — the author never types the
|
|
193
|
+
* prefix. Non-destructive (admin edits win; absent languages are not
|
|
194
|
+
* created) and persists across uninstalls. Caps: ≤100 keys, key matches
|
|
195
|
+
* /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/, value ≤1 KB.
|
|
196
|
+
*/
|
|
197
|
+
translations?: Record<string, { en: string } & Record<string, string>>;
|
|
150
198
|
}
|
|
151
199
|
|
|
152
200
|
export interface ThemeTokens {
|
|
@@ -168,47 +216,169 @@ export interface ThemeTokens {
|
|
|
168
216
|
};
|
|
169
217
|
}
|
|
170
218
|
|
|
219
|
+
// ----------------------------------------------------- injected data clients
|
|
220
|
+
//
|
|
221
|
+
// REQ-WSDK-DOMAIN-CLIENTS. @colixsystems/widget-sdk is CORE ONLY — manifest
|
|
222
|
+
// contract, primitives, rendering, hooks, events, theme/i18n. It owns NO HTTP
|
|
223
|
+
// and depends on NONE of the data SDK packages. The data layer is FOUR domain
|
|
224
|
+
// client packages, each instantiated by the host and injected into
|
|
225
|
+
// WidgetContext. **Widgets never import these packages** — they reach the data
|
|
226
|
+
// surface only through the SDK hooks, which read the injected instances. The
|
|
227
|
+
// shapes below are declared STRUCTURALLY here (widget-sdk must not import the
|
|
228
|
+
// data SDKs); the authoritative typings ship with each client package.
|
|
229
|
+
//
|
|
230
|
+
// Wire/casing: snake_case end to end. The clients send/return snake_case
|
|
231
|
+
// VERBATIM (e.g. `created_at`, `group_ids`, `can_read`, `amount_cents`,
|
|
232
|
+
// `data_type`, `is_active`). There is NO client-side case transform anywhere.
|
|
233
|
+
// Author-defined record column values are passed through verbatim. List
|
|
234
|
+
// methods return the `{ data, meta }` envelope.
|
|
235
|
+
|
|
236
|
+
/** A `{ data, meta }` list envelope, returned verbatim by every `list(...)`. */
|
|
237
|
+
export interface ListEnvelope<T = unknown> {
|
|
238
|
+
data: T[];
|
|
239
|
+
meta?: Record<string, unknown>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Structural shape of the injected `@colixsystems/datastore-client`
|
|
244
|
+
* (`ctx.datastore`). Backs `useDatastoreQuery` / `useDatastoreRecord` /
|
|
245
|
+
* `useDatastoreSchema` / `useDatastoreMutation` / `useRecordPermissions`.
|
|
246
|
+
* Rows and bodies are snake_case verbatim; `list` returns `{ data, meta }`.
|
|
247
|
+
*/
|
|
248
|
+
export interface DatastoreClient {
|
|
249
|
+
tables: {
|
|
250
|
+
list(): Promise<ListEnvelope>;
|
|
251
|
+
get(tableIdOrName: string): Promise<unknown>;
|
|
252
|
+
};
|
|
253
|
+
schema(tableId: string): Promise<unknown>;
|
|
254
|
+
records(tableId: string): {
|
|
255
|
+
list(query?: Query): Promise<ListEnvelope>;
|
|
256
|
+
get(recordId: string): Promise<unknown>;
|
|
257
|
+
create(values: Record<string, unknown>): Promise<unknown>;
|
|
258
|
+
/** PATCH semantics — only the supplied columns are mutated. */
|
|
259
|
+
update(recordId: string, values: Record<string, unknown>): Promise<unknown>;
|
|
260
|
+
delete(recordId: string): Promise<void>;
|
|
261
|
+
aggregate(spec: unknown): Promise<unknown>;
|
|
262
|
+
permissions(recordId: string): {
|
|
263
|
+
list(): Promise<ListEnvelope>;
|
|
264
|
+
grant(body: Record<string, unknown>): Promise<unknown>;
|
|
265
|
+
update(
|
|
266
|
+
permissionId: string,
|
|
267
|
+
patch: Record<string, unknown>,
|
|
268
|
+
): Promise<unknown>;
|
|
269
|
+
revoke(permissionId: string): Promise<void>;
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Structural shape of the injected `@colixsystems/directory-client`
|
|
276
|
+
* (`ctx.directory`). Backs `useDirectory` / `useUsers` (via `.users`) and
|
|
277
|
+
* `useGroups` (via `.groups`). Rows and bodies are snake_case verbatim;
|
|
278
|
+
* `list` returns `{ data, meta }`.
|
|
279
|
+
*/
|
|
280
|
+
export interface DirectoryClient {
|
|
281
|
+
me(): Promise<unknown>;
|
|
282
|
+
users: {
|
|
283
|
+
list(query?: DirectoryQuery): Promise<ListEnvelope>;
|
|
284
|
+
get(userId: string): Promise<unknown>;
|
|
285
|
+
invite(body: Record<string, unknown>): Promise<unknown>;
|
|
286
|
+
deactivate(userId: string): Promise<unknown>;
|
|
287
|
+
reactivate(userId: string): Promise<unknown>;
|
|
288
|
+
};
|
|
289
|
+
groups: {
|
|
290
|
+
list(query?: GroupsQuery): Promise<ListEnvelope>;
|
|
291
|
+
create(body: Record<string, unknown>): Promise<unknown>;
|
|
292
|
+
remove(groupId: string): Promise<void>;
|
|
293
|
+
addMember(groupId: string, userId: string): Promise<void>;
|
|
294
|
+
removeMember(groupId: string, userId: string): Promise<void>;
|
|
295
|
+
listMine(): Promise<ListEnvelope>;
|
|
296
|
+
};
|
|
297
|
+
invites: {
|
|
298
|
+
list(): Promise<ListEnvelope>;
|
|
299
|
+
revoke(inviteId: string): Promise<void>;
|
|
300
|
+
resend(inviteId: string): Promise<unknown>;
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Structural shape of the injected Asset-Manager files client (`ctx.files`).
|
|
306
|
+
* Slimmed to the three top-level ops the host injects — `get` / `list` /
|
|
307
|
+
* `upload`. Backs `useFile`, which calls `ctx.files.get(id)`. The returned
|
|
308
|
+
* file carries an absolute `url` safe to drop into `<Image source>`.
|
|
309
|
+
*/
|
|
310
|
+
export interface FilesClient {
|
|
311
|
+
get(fileId: string): Promise<unknown>;
|
|
312
|
+
list(query?: Record<string, unknown>): Promise<ListEnvelope>;
|
|
313
|
+
upload(formData: unknown): Promise<unknown>;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Structural shape of the injected `@colixsystems/payments-client`
|
|
318
|
+
* (`ctx.payments`, REQ-BILL-07-WIDGETPAY). Backs `usePayments`.
|
|
319
|
+
*/
|
|
320
|
+
export interface PaymentsClient {
|
|
321
|
+
requestPayment(body: PaymentRequest): Promise<PaymentResult>;
|
|
322
|
+
getPayment(paymentId: string): Promise<PaymentResult>;
|
|
323
|
+
}
|
|
324
|
+
|
|
171
325
|
export interface WidgetContext<TProps = unknown> {
|
|
172
326
|
props: TProps;
|
|
173
327
|
widget: { id: string; instanceId: string; version: string };
|
|
328
|
+
/**
|
|
329
|
+
* REQ-LAY-08 — optional host layout hint backing `useFill()`. `true` when
|
|
330
|
+
* the host sized this widget to fill its layout slot's available height (a
|
|
331
|
+
* page-grid tile set to "Fill tile height", or a default-fill widget type).
|
|
332
|
+
* Absent / `false` everywhere the host has not opted the widget into filling.
|
|
333
|
+
*/
|
|
334
|
+
fill?: boolean;
|
|
335
|
+
/** Active end-user identity, snake_case verbatim. `id` is null when anonymous. */
|
|
174
336
|
user: {
|
|
175
337
|
id: string | null;
|
|
176
338
|
email: string | null;
|
|
177
|
-
|
|
339
|
+
display_name: string | null;
|
|
178
340
|
roles: string[];
|
|
179
|
-
|
|
341
|
+
group_ids: string[];
|
|
180
342
|
};
|
|
181
343
|
workspace: {
|
|
182
344
|
id: string;
|
|
183
|
-
|
|
345
|
+
slug: string;
|
|
184
346
|
locale: string;
|
|
185
347
|
theme: ThemeTokens;
|
|
186
348
|
};
|
|
187
349
|
navigation: {
|
|
188
350
|
goTo(pageId: string, params?: Record<string, string>): void;
|
|
189
351
|
goBack(): void;
|
|
352
|
+
push(pageId: string, params?: Record<string, unknown>): void;
|
|
353
|
+
replace(pageId: string, params?: Record<string, unknown>): void;
|
|
354
|
+
back(): void;
|
|
190
355
|
currentRoute: { pageId: string; params: Record<string, string> };
|
|
191
356
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
357
|
+
/** Injected @colixsystems/datastore-client. */
|
|
358
|
+
datastore: DatastoreClient;
|
|
359
|
+
/** Injected @colixsystems/directory-client (users + groups + invites). */
|
|
360
|
+
directory: DirectoryClient;
|
|
361
|
+
/** Injected Asset-Manager files client: { get, list, upload }. */
|
|
362
|
+
files: FilesClient;
|
|
363
|
+
/** Injected @colixsystems/payments-client. */
|
|
364
|
+
payments: PaymentsClient;
|
|
365
|
+
/** Host child-node renderer; backs WidgetTree / useChildRenderer. */
|
|
366
|
+
renderer: { renderNode(node: unknown): unknown };
|
|
196
367
|
events: { emit(eventName: string, payload?: unknown): void };
|
|
197
|
-
payments: {
|
|
198
|
-
requestPayment(args: PaymentRequest): Promise<PaymentResult>;
|
|
199
|
-
getPayment(paymentId: string): Promise<PaymentResult>;
|
|
200
|
-
};
|
|
201
368
|
i18n: {
|
|
202
369
|
locale: string;
|
|
203
|
-
t(key: string,
|
|
370
|
+
t(key: string, fallback?: string): string;
|
|
204
371
|
};
|
|
205
|
-
platform: "web" | "native";
|
|
206
372
|
logger: {
|
|
207
373
|
debug: (...args: unknown[]) => void;
|
|
208
374
|
info: (...args: unknown[]) => void;
|
|
209
375
|
warn: (...args: unknown[]) => void;
|
|
210
376
|
error: (...args: unknown[]) => void;
|
|
211
377
|
};
|
|
378
|
+
/** Optional host toast slot; backs useToast. */
|
|
379
|
+
toast?: {
|
|
380
|
+
showToast(args: { kind?: string; message: string }): void;
|
|
381
|
+
};
|
|
212
382
|
}
|
|
213
383
|
|
|
214
384
|
/**
|
|
@@ -303,8 +473,8 @@ export interface DirectoryQuery {
|
|
|
303
473
|
q?: string;
|
|
304
474
|
/** `"USER"` (default), `"INTEGRATION"`, or `"ALL"`. */
|
|
305
475
|
role?: "USER" | "INTEGRATION" | "ALL";
|
|
306
|
-
/** Filter by active state. */
|
|
307
|
-
|
|
476
|
+
/** Filter by active state (snake_case on the wire). */
|
|
477
|
+
is_active?: boolean;
|
|
308
478
|
limit?: number;
|
|
309
479
|
offset?: number;
|
|
310
480
|
}
|
|
@@ -317,7 +487,9 @@ export interface DirectoryResult {
|
|
|
317
487
|
}
|
|
318
488
|
|
|
319
489
|
/**
|
|
320
|
-
* Read-only user directory hook.
|
|
490
|
+
* Read-only user directory hook. Reads the injected directory-client at
|
|
491
|
+
* `ctx.directory.users.list(query)` (returns the `{ data, meta }` envelope;
|
|
492
|
+
* the hook unwraps `res.data`). Resolves the tenant's app users to snake_case
|
|
321
493
|
* `{ id, name, role }` rows for chat people-lists, @-mention pickers, or
|
|
322
494
|
* author-id → display-name resolution. Requires the
|
|
323
495
|
* `directory.read:users` scope in the widget manifest.
|
|
@@ -387,10 +559,11 @@ export function useDatastoreRecord(
|
|
|
387
559
|
* One column in a table's schema, as returned by `useDatastoreSchema`.
|
|
388
560
|
* Structural metadata only — never row data.
|
|
389
561
|
*/
|
|
562
|
+
// Returned by `ctx.datastore.schema(tableId)` — wire data, snake_case verbatim.
|
|
390
563
|
export interface DatastoreSchemaColumn {
|
|
391
564
|
id: string;
|
|
392
565
|
name: string;
|
|
393
|
-
|
|
566
|
+
data_type:
|
|
394
567
|
| "STRING"
|
|
395
568
|
| "TEXT"
|
|
396
569
|
| "NUMBER"
|
|
@@ -405,11 +578,11 @@ export interface DatastoreSchemaColumn {
|
|
|
405
578
|
| "USER_GROUP";
|
|
406
579
|
required: boolean;
|
|
407
580
|
/** For RELATION columns only. */
|
|
408
|
-
|
|
581
|
+
relation_type?: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY" | null;
|
|
409
582
|
/** For RELATION columns only — the id of the table this column points at. */
|
|
410
|
-
|
|
583
|
+
target_table_id?: string | null;
|
|
411
584
|
/** True when this column is the table's display/identification column. */
|
|
412
|
-
|
|
585
|
+
is_identification?: boolean;
|
|
413
586
|
}
|
|
414
587
|
|
|
415
588
|
export interface DatastoreSchema {
|
|
@@ -462,9 +635,9 @@ export function useFile(fileId: string | null | undefined): {
|
|
|
462
635
|
file: {
|
|
463
636
|
id: string;
|
|
464
637
|
url: string;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
638
|
+
stored_filename?: string;
|
|
639
|
+
mime_type?: string;
|
|
640
|
+
size_bytes?: number;
|
|
468
641
|
[k: string]: unknown;
|
|
469
642
|
} | null;
|
|
470
643
|
loading: boolean;
|
|
@@ -485,11 +658,22 @@ export function useI18n(): {
|
|
|
485
658
|
export function useUser(): {
|
|
486
659
|
id: string | null;
|
|
487
660
|
email: string | null;
|
|
488
|
-
|
|
661
|
+
display_name: string | null;
|
|
489
662
|
roles: string[];
|
|
490
|
-
|
|
663
|
+
group_ids: string[];
|
|
491
664
|
};
|
|
492
665
|
|
|
666
|
+
/**
|
|
667
|
+
* REQ-LAY-08 — returns `true` when the host has sized this widget to fill its
|
|
668
|
+
* layout slot's available height (a page-grid tile set to "Fill tile height",
|
|
669
|
+
* or a default-fill widget type — containers + media). Widgets that can
|
|
670
|
+
* stretch (Image, Chart, Map, Video, …) should switch to a fill style
|
|
671
|
+
* (`flex: 1` / `height: "100%"`) when this is `true`; others may ignore it.
|
|
672
|
+
* Defaults to `false`, so calling it is always safe, and the same value is
|
|
673
|
+
* injected on web and native so fill behaviour is identical on both platforms.
|
|
674
|
+
*/
|
|
675
|
+
export function useFill(): boolean;
|
|
676
|
+
|
|
493
677
|
/**
|
|
494
678
|
* The host-provided navigation surface. `goTo(pageId, params?)` navigates
|
|
495
679
|
* to an internal app page; `goBack()` pops the stack. Missing methods
|
|
@@ -567,13 +751,13 @@ export interface AppUserRow {
|
|
|
567
751
|
name: string;
|
|
568
752
|
email?: string;
|
|
569
753
|
role: "USER" | "INTEGRATION";
|
|
570
|
-
|
|
754
|
+
is_active: boolean;
|
|
571
755
|
}
|
|
572
756
|
|
|
573
757
|
export interface UsersQuery {
|
|
574
758
|
q?: string;
|
|
575
759
|
role?: "USER" | "INTEGRATION" | "ALL";
|
|
576
|
-
|
|
760
|
+
is_active?: boolean;
|
|
577
761
|
limit?: number;
|
|
578
762
|
offset?: number;
|
|
579
763
|
}
|
|
@@ -581,15 +765,15 @@ export interface UsersQuery {
|
|
|
581
765
|
export interface InviteArgs {
|
|
582
766
|
email: string;
|
|
583
767
|
name?: string;
|
|
584
|
-
|
|
768
|
+
group_ids?: string[];
|
|
585
769
|
}
|
|
586
770
|
|
|
587
771
|
export interface AppUserInviteRow {
|
|
588
772
|
id: string;
|
|
589
773
|
email: string;
|
|
590
774
|
status: string;
|
|
591
|
-
|
|
592
|
-
|
|
775
|
+
invited_at?: string;
|
|
776
|
+
expires_at?: string;
|
|
593
777
|
}
|
|
594
778
|
|
|
595
779
|
export interface UsersApi {
|
|
@@ -604,10 +788,12 @@ export interface UsersApi {
|
|
|
604
788
|
}
|
|
605
789
|
|
|
606
790
|
/**
|
|
607
|
-
* AppUser administration. Reads
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
791
|
+
* AppUser administration. Reads through the injected directory-client at
|
|
792
|
+
* `ctx.directory.users.{list,get,invite,deactivate,reactivate}`. Reads
|
|
793
|
+
* require the `users.read:*` scope in the manifest; mutations additionally
|
|
794
|
+
* require `users.write:*`. Widgets that declare the scopes but whose calling
|
|
795
|
+
* APP_USER lacks the corresponding SystemAcl `users.*` capability grant get a
|
|
796
|
+
* `FORBIDDEN` DirectoryError.
|
|
611
797
|
*/
|
|
612
798
|
export function useUsers(query?: UsersQuery): UsersApi;
|
|
613
799
|
|
|
@@ -616,7 +802,7 @@ export function useUsers(query?: UsersQuery): UsersApi;
|
|
|
616
802
|
export interface AppUserGroupRow {
|
|
617
803
|
id: string;
|
|
618
804
|
name: string;
|
|
619
|
-
|
|
805
|
+
member_count?: number;
|
|
620
806
|
}
|
|
621
807
|
|
|
622
808
|
export interface GroupsQuery {
|
|
@@ -637,47 +823,50 @@ export interface GroupsApi {
|
|
|
637
823
|
}
|
|
638
824
|
|
|
639
825
|
/**
|
|
640
|
-
* AppUserGroup administration. Reads
|
|
641
|
-
*
|
|
826
|
+
* AppUserGroup administration. Reads through the injected directory-client at
|
|
827
|
+
* `ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}`.
|
|
828
|
+
* Reads require `groups.read:*`; mutations require `groups.write:*`. Same
|
|
829
|
+
* SystemAcl gating as `useUsers`.
|
|
642
830
|
*/
|
|
643
831
|
export function useGroups(query?: GroupsQuery): GroupsApi;
|
|
644
832
|
|
|
645
833
|
// ----------------------------------------------------- useRecordPermissions
|
|
646
834
|
//
|
|
647
835
|
// 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.
|
|
836
|
+
// management for a single record. Reads the injected datastore-client at
|
|
837
|
+
// `ctx.datastore.records(tableId).permissions(recordId)`. Rows and bodies are
|
|
838
|
+
// snake_case VERBATIM — the SDK does NOT transform them. A row carries
|
|
839
|
+
// `user_id` OR `group_id` (both null = a public grant) plus the `can_*` flags.
|
|
653
840
|
|
|
654
841
|
export interface RecordPermission {
|
|
655
842
|
id: string;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
843
|
+
/** Set when the grant targets a user; null otherwise. */
|
|
844
|
+
user_id: string | null;
|
|
845
|
+
/** Set when the grant targets a group; null otherwise. Both null = public. */
|
|
846
|
+
group_id: string | null;
|
|
847
|
+
can_read: boolean;
|
|
848
|
+
can_write: boolean;
|
|
849
|
+
can_delete: boolean;
|
|
850
|
+
can_grant: boolean;
|
|
851
|
+
[k: string]: unknown;
|
|
663
852
|
}
|
|
664
853
|
|
|
665
854
|
export interface RecordPermissionGrantInput {
|
|
666
|
-
/**
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
855
|
+
/** Target a user (omit / null `group_id`). */
|
|
856
|
+
user_id?: string | null;
|
|
857
|
+
/** Target a group (omit / null `user_id`). Both omitted = a public grant. */
|
|
858
|
+
group_id?: string | null;
|
|
859
|
+
can_read?: boolean;
|
|
860
|
+
can_write?: boolean;
|
|
861
|
+
can_delete?: boolean;
|
|
862
|
+
can_grant?: boolean;
|
|
674
863
|
}
|
|
675
864
|
|
|
676
865
|
export interface RecordPermissionUpdateInput {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
866
|
+
can_read?: boolean;
|
|
867
|
+
can_write?: boolean;
|
|
868
|
+
can_delete?: boolean;
|
|
869
|
+
can_grant?: boolean;
|
|
681
870
|
}
|
|
682
871
|
|
|
683
872
|
export interface RecordPermissionsResult {
|
|
@@ -720,10 +909,13 @@ export class PermissionError extends Error {
|
|
|
720
909
|
|
|
721
910
|
/**
|
|
722
911
|
* REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
723
|
-
* management.
|
|
724
|
-
* `
|
|
725
|
-
*
|
|
726
|
-
*
|
|
912
|
+
* management. Reads the injected datastore-client at
|
|
913
|
+
* `ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}`.
|
|
914
|
+
* Requires `acl.write:records` in the manifest's `requestedScopes`. The
|
|
915
|
+
* backend gates the call on `can_grant` for the target record; a widget that
|
|
916
|
+
* declares the scope but whose caller lacks the grant receives
|
|
917
|
+
* `PermissionError { code: "FORBIDDEN" }`. Rows and bodies are snake_case
|
|
918
|
+
* verbatim.
|
|
727
919
|
*
|
|
728
920
|
* When `tableId` OR `recordId` is null / empty, the hook collapses to a
|
|
729
921
|
* stable empty result without a network round-trip; mutation methods
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
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
|
}
|