@colixsystems/widget-sdk 0.92.0 → 0.94.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 +47 -0
- package/dist/contract.cjs +45 -14
- package/dist/contract.js +45 -14
- package/dist/hooks.js +112 -0
- package/dist/index.d.ts +49 -1
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +17 -0
- package/dist/linter.js +17 -0
- package/dist/manifest.cjs +18 -64
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
49
49
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
50
50
|
| **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (edits also `users.write:*`; `remove()` also `users.delete:*`) |
|
|
51
51
|
| **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
52
|
+
| **DIRECTORY** | `useInvites(query?)` | `{ invites, loading, error, refetch, resend, revoke }` | `directory.invites.*` — `users.write:*` + the SystemAcl `users.write` capability (the whole invite surface, list included). `query` is `{ status?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \| all` (endpoint default `all`). |
|
|
52
53
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
53
54
|
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId? })` | `{ exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.exportPdf` — `files.write:*`. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF server-side and saves it as a file (`application/pdf`); same server-side renderer on web + native. |
|
|
54
55
|
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. Rejects with `PaymentError { code, message, retryable }`; when `retryable` is `false` show `message` and drop the retry. Charges are accepted ONLY in the currency the workspace sells in — omit `currency` and the platform applies it (a disagreeing literal is a publish-blocking `payment-currency` finding). |
|
|
@@ -65,6 +66,16 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
65
66
|
|
|
66
67
|
`v0.91.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
67
68
|
|
|
69
|
+
### What's new in 0.93.0 (contract 1.66.0)
|
|
70
|
+
|
|
71
|
+
**BREAKING: a widget no longer declares server-side actions.** `manifest.actions` is removed from the contract and **refused** by `validateManifest` — an author who declares it now fails the publish with a message naming the replacement, rather than shipping a widget that quietly carries no automation.
|
|
72
|
+
|
|
73
|
+
- **Why.** `actions` made a widget two products in one package: a React component that renders, and a script that never renders at all — different runtime, different lifecycle, different review concerns, one manifest. Automation is now its own marketplace deliverable, an **Action** (`@colixsystems/action-sdk`), with its own manifest, starter kit, developer guide, submit button and platform-admin review. A workspace installs and configures it separately from any widget.
|
|
74
|
+
- **What to do instead.** Move the script into an Action manifest (`appstudio-action lint` / `pack`), publish it, and let the workspace install it. Its `propertySchema` is filled in by the **installing operator** rather than a page author, and the values reach the script as the `properties` global.
|
|
75
|
+
- **Removed from the contract**: the `actions` manifest field. `CONTRACT.actionTriggerTypes` / `actionScriptGlobals` / `actionScriptMaxBytes` remain exported for now but describe a surface no widget field uses — the action-sdk owns that vocabulary.
|
|
76
|
+
- **Existing installs are unaffected.** A tenant Action row materialised from a widget manifest keeps running, keeps its bindings and keeps its run history; the platform migrated those rows onto the Action that ships the automation. What is gone is the ability to declare a NEW one on a widget.
|
|
77
|
+
- **No parity impact.** Actions never ran in the rendered app, so nothing about the Player or the export changes.
|
|
78
|
+
|
|
68
79
|
### What's new in 0.92.0 (contract 1.65.0)
|
|
69
80
|
|
|
70
81
|
**New `useInterpretDraft(tableId)` hook — turn one sentence into DRAFT record values.** A new DATASTORE hook reading a new `interpret` method on the existing `ctx.datastore` slice (`@colixsystems/datastore-client` 0.13.0). Returns `{ interpret, interpreting, error, result, available }`. Call `interpret(text, { fields, timeZone })` **imperatively** from an event handler — never on mount or in a render loop — and it resolves to `{ values, unresolved }`, where `values` is keyed by column NAME (the same shape `useDatastoreMutation().create` takes) and `unresolved` names the fields the sentence did not state.
|
|
@@ -866,6 +877,42 @@ The matching manifest declares the scopes:
|
|
|
866
877
|
|
|
867
878
|
The host rejects calls whose scope is not declared in the manifest (the SDK linter catches this statically too). Declaring a write scope is also a consent prompt the Studio admin sees at install time — the wider the scope set, the more careful the admin is about granting the install.
|
|
868
879
|
|
|
880
|
+
## Listing pending invitations from a widget
|
|
881
|
+
|
|
882
|
+
`useInvites(query?)` lists the workspace's invites and resends / revokes them:
|
|
883
|
+
|
|
884
|
+
```jsx
|
|
885
|
+
import { useInvites, View, Text, Pressable } from '@colixsystems/widget-sdk';
|
|
886
|
+
|
|
887
|
+
// Manifest: requestedScopes: ['users.read:*', 'users.write:*']
|
|
888
|
+
function PendingInvites() {
|
|
889
|
+
const { invites, loading, error, resend, revoke } = useInvites({ status: 'pending' });
|
|
890
|
+
if (loading) return null;
|
|
891
|
+
if (error) return <Text>{error.message}</Text>; // code === 'FORBIDDEN' when the capability is missing
|
|
892
|
+
return (
|
|
893
|
+
<View>
|
|
894
|
+
{invites.map((i) => (
|
|
895
|
+
<View key={i.id}>
|
|
896
|
+
<Text>{i.email} · {i.status}</Text>
|
|
897
|
+
<Pressable onPress={() => resend(i.id).catch(() => {})}><Text>Resend</Text></Pressable>
|
|
898
|
+
<Pressable onPress={() => revoke(i.id).catch(() => {})}><Text>Revoke</Text></Pressable>
|
|
899
|
+
</View>
|
|
900
|
+
))}
|
|
901
|
+
</View>
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Rows are snake_case: `{ id, email, name, group_ids, status, expires_at, accepted_at,
|
|
907
|
+
revoked_at, created_at }`. Trust the server-computed `status` rather than comparing
|
|
908
|
+
`expires_at` against the device clock.
|
|
909
|
+
|
|
910
|
+
**The gate is `users.write`, not `users.read`.** A pending invite exposes the email of
|
|
911
|
+
someone who is not a member yet, so the backend gates the entire invite surface —
|
|
912
|
+
listing included — on the `users.write` capability plus a signed `users.write:*`
|
|
913
|
+
scope. The `invites.read:*` / `invites.write:*` scope names mint but no route
|
|
914
|
+
enforces them, so declaring only those yields `FORBIDDEN`. Both mutations refetch
|
|
915
|
+
the list on success.
|
|
869
916
|
## Managing per-record permissions from a widget
|
|
870
917
|
|
|
871
918
|
`useRecordPermissions(tableId, recordId)` is the in-app surface for sharing a single record with another user or group. The chat widget uses it to invite members into a channel — the channel record's per-record grants ARE the membership list (messages inherit those grants). The hook also covers project-workspace, document-sharing, and team-roster widgets that grant access record-by-record.
|
package/dist/contract.cjs
CHANGED
|
@@ -1023,6 +1023,44 @@ const HOOKS = [
|
|
|
1023
1023
|
requiredContextSlice: ["directory.groups"],
|
|
1024
1024
|
scopes: ["groups.read:*"],
|
|
1025
1025
|
},
|
|
1026
|
+
// sc-5097 — pending AppUser invite administration. Returns
|
|
1027
|
+
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1028
|
+
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1029
|
+
// caller's `users.write` SystemAcl capability.
|
|
1030
|
+
{
|
|
1031
|
+
name: "useInvites",
|
|
1032
|
+
signature: "useInvites(query?)",
|
|
1033
|
+
description:
|
|
1034
|
+
"Pending AppUser invite administration via the injected " +
|
|
1035
|
+
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1036
|
+
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1037
|
+
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1038
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1039
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1040
|
+
"value. query is { status?, limit?, offset? } passed verbatim; the " +
|
|
1041
|
+
"endpoint defaults to status 'all', so pass { status: 'pending' } " +
|
|
1042
|
+
"for the usual outstanding-invite list. The WHOLE invite surface — " +
|
|
1043
|
+
"list included — is gated on users.write:* plus the users.write " +
|
|
1044
|
+
"SystemAcl capability, because a pending invite exposes the email of " +
|
|
1045
|
+
"someone who is not a member yet; a users.read-only caller cannot see " +
|
|
1046
|
+
"it. (invites.read:* / invites.write:* mint but gate nothing today.) " +
|
|
1047
|
+
"Without the capability every call rejects with DirectoryError code " +
|
|
1048
|
+
"FORBIDDEN — surface that, do not hide the tab. Both mutations " +
|
|
1049
|
+
"refetch on success.",
|
|
1050
|
+
returnShape: {
|
|
1051
|
+
invites:
|
|
1052
|
+
"Array<{ id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1053
|
+
loading: "boolean",
|
|
1054
|
+
error: "DirectoryError | null // { code, message, retryable }",
|
|
1055
|
+
refetch: "() => Promise<void>",
|
|
1056
|
+
resend:
|
|
1057
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1058
|
+
revoke:
|
|
1059
|
+
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1060
|
+
},
|
|
1061
|
+
requiredContextSlice: ["directory.invites"],
|
|
1062
|
+
scopes: ["users.write:*"],
|
|
1063
|
+
},
|
|
1026
1064
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
|
|
1027
1065
|
// app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.js.
|
|
1028
1066
|
{
|
|
@@ -1590,17 +1628,6 @@ const MANIFEST_SCHEMA = {
|
|
|
1590
1628
|
description:
|
|
1591
1629
|
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
|
|
1592
1630
|
},
|
|
1593
|
-
actions: {
|
|
1594
|
-
type: "object[]",
|
|
1595
|
-
required: false,
|
|
1596
|
-
description:
|
|
1597
|
-
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerTypes (a non-empty array of unique values from " +
|
|
1598
|
-
ACTION_TRIGGER_TYPES.join(", ") +
|
|
1599
|
-
"; combine them so one script serves several events — the script's `triggerType` global names the event that actually fired), scheduleCron? (required iff triggerTypes contains 'schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
|
|
1600
|
-
ACTION_SCRIPT_GLOBALS.join(", ") +
|
|
1601
|
-
" — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
|
|
1602
|
-
default: [],
|
|
1603
|
-
},
|
|
1604
1631
|
translations: {
|
|
1605
1632
|
type: "object",
|
|
1606
1633
|
required: false,
|
|
@@ -1688,9 +1715,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1688
1715
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
1689
1716
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
1690
1717
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
1691
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:* (destructive user removal by users.delete:*).",
|
|
1718
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke pending invites, gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:* (destructive user removal by users.delete:*).",
|
|
1692
1719
|
required: true,
|
|
1693
|
-
fields: { users: "object", groups: "object", bankid: "object" },
|
|
1720
|
+
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
1694
1721
|
},
|
|
1695
1722
|
assets: {
|
|
1696
1723
|
description:
|
|
@@ -2891,7 +2918,11 @@ const CONTRACT = deepFreeze({
|
|
|
2891
2918
|
// published widget keeps validating. The script's `triggerType` global
|
|
2892
2919
|
// now names the trigger that actually FIRED the run ('manual' and 'app'
|
|
2893
2920
|
// included), which is what makes a multi-trigger script able to branch.
|
|
2894
|
-
|
|
2921
|
+
// 1.66.0: BREAKING — `actions` is removed from the manifest contract and
|
|
2922
|
+
// REFUSED by the validator (sc-4879). Server-side automation ships as its own
|
|
2923
|
+
// marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
|
|
2924
|
+
// installs and configures separately. A widget renders; it does not automate.
|
|
2925
|
+
version: "1.66.0",
|
|
2895
2926
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2896
2927
|
hooks: HOOKS,
|
|
2897
2928
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1023,6 +1023,44 @@ const HOOKS = [
|
|
|
1023
1023
|
requiredContextSlice: ["directory.groups"],
|
|
1024
1024
|
scopes: ["groups.read:*"],
|
|
1025
1025
|
},
|
|
1026
|
+
// sc-5097 — pending AppUser invite administration. Returns
|
|
1027
|
+
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1028
|
+
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1029
|
+
// caller's `users.write` SystemAcl capability.
|
|
1030
|
+
{
|
|
1031
|
+
name: "useInvites",
|
|
1032
|
+
signature: "useInvites(query?)",
|
|
1033
|
+
description:
|
|
1034
|
+
"Pending AppUser invite administration via the injected " +
|
|
1035
|
+
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1036
|
+
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1037
|
+
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1038
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1039
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1040
|
+
"value. query is { status?, limit?, offset? } passed verbatim; the " +
|
|
1041
|
+
"endpoint defaults to status 'all', so pass { status: 'pending' } " +
|
|
1042
|
+
"for the usual outstanding-invite list. The WHOLE invite surface — " +
|
|
1043
|
+
"list included — is gated on users.write:* plus the users.write " +
|
|
1044
|
+
"SystemAcl capability, because a pending invite exposes the email of " +
|
|
1045
|
+
"someone who is not a member yet; a users.read-only caller cannot see " +
|
|
1046
|
+
"it. (invites.read:* / invites.write:* mint but gate nothing today.) " +
|
|
1047
|
+
"Without the capability every call rejects with DirectoryError code " +
|
|
1048
|
+
"FORBIDDEN — surface that, do not hide the tab. Both mutations " +
|
|
1049
|
+
"refetch on success.",
|
|
1050
|
+
returnShape: {
|
|
1051
|
+
invites:
|
|
1052
|
+
"Array<{ id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1053
|
+
loading: "boolean",
|
|
1054
|
+
error: "DirectoryError | null // { code, message, retryable }",
|
|
1055
|
+
refetch: "() => Promise<void>",
|
|
1056
|
+
resend:
|
|
1057
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1058
|
+
revoke:
|
|
1059
|
+
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1060
|
+
},
|
|
1061
|
+
requiredContextSlice: ["directory.invites"],
|
|
1062
|
+
scopes: ["users.write:*"],
|
|
1063
|
+
},
|
|
1026
1064
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
|
|
1027
1065
|
// app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.cjs.
|
|
1028
1066
|
{
|
|
@@ -1590,17 +1628,6 @@ const MANIFEST_SCHEMA = {
|
|
|
1590
1628
|
description:
|
|
1591
1629
|
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
|
|
1592
1630
|
},
|
|
1593
|
-
actions: {
|
|
1594
|
-
type: "object[]",
|
|
1595
|
-
required: false,
|
|
1596
|
-
description:
|
|
1597
|
-
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerTypes (a non-empty array of unique values from " +
|
|
1598
|
-
ACTION_TRIGGER_TYPES.join(", ") +
|
|
1599
|
-
"; combine them so one script serves several events — the script's `triggerType` global names the event that actually fired), scheduleCron? (required iff triggerTypes contains 'schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
|
|
1600
|
-
ACTION_SCRIPT_GLOBALS.join(", ") +
|
|
1601
|
-
" — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
|
|
1602
|
-
default: [],
|
|
1603
|
-
},
|
|
1604
1631
|
translations: {
|
|
1605
1632
|
type: "object",
|
|
1606
1633
|
required: false,
|
|
@@ -1688,9 +1715,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1688
1715
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
1689
1716
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
1690
1717
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
1691
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:* (destructive user removal by users.delete:*).",
|
|
1718
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke pending invites, gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:* (destructive user removal by users.delete:*).",
|
|
1692
1719
|
required: true,
|
|
1693
|
-
fields: { users: "object", groups: "object", bankid: "object" },
|
|
1720
|
+
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
1694
1721
|
},
|
|
1695
1722
|
assets: {
|
|
1696
1723
|
description:
|
|
@@ -2891,7 +2918,11 @@ const CONTRACT = deepFreeze({
|
|
|
2891
2918
|
// published widget keeps validating. The script's `triggerType` global
|
|
2892
2919
|
// now names the trigger that actually FIRED the run ('manual' and 'app'
|
|
2893
2920
|
// included), which is what makes a multi-trigger script able to branch.
|
|
2894
|
-
|
|
2921
|
+
// 1.66.0: BREAKING — `actions` is removed from the manifest contract and
|
|
2922
|
+
// REFUSED by the validator (sc-4879). Server-side automation ships as its own
|
|
2923
|
+
// marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
|
|
2924
|
+
// installs and configures separately. A widget renders; it does not automate.
|
|
2925
|
+
version: "1.66.0",
|
|
2895
2926
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2896
2927
|
hooks: HOOKS,
|
|
2897
2928
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -3467,6 +3467,118 @@ export function useGroups(query) {
|
|
|
3467
3467
|
return { groups, loading, error, refetch, create, remove, addMember, removeMember };
|
|
3468
3468
|
}
|
|
3469
3469
|
|
|
3470
|
+
/**
|
|
3471
|
+
* sc-5097 — pending AppUser invite administration.
|
|
3472
|
+
*
|
|
3473
|
+
* Returns `{ invites, loading, error, refetch, resend, revoke }`. Reads the
|
|
3474
|
+
* injected `@colixsystems/directory-client` at
|
|
3475
|
+
* `ctx.directory.invites.{list, resend, revoke}` — `list` resolves to the
|
|
3476
|
+
* `{ data, meta }` envelope VERBATIM, so we unwrap `res.data` (default `[]`).
|
|
3477
|
+
*
|
|
3478
|
+
* Rows are snake_case exactly as the backend projects them: `{ id, tenant_id,
|
|
3479
|
+
* email, name, group_ids, status, expires_at, accepted_at, revoked_at,
|
|
3480
|
+
* created_at, invited_by_studio_user_id }`. `status` is the server-computed
|
|
3481
|
+
* lifecycle value (`"pending" | "accepted" | "revoked" | "expired"`) — prefer
|
|
3482
|
+
* it over re-deriving expiry on the client, whose clock may disagree.
|
|
3483
|
+
*
|
|
3484
|
+
* `query` is an optional `{ status?, limit?, offset? }` passed through
|
|
3485
|
+
* verbatim; `status` accepts `"pending" | "accepted" | "revoked" | "expired" |
|
|
3486
|
+
* "all"` and the endpoint defaults to `"all"`, so pass `{ status: "pending" }`
|
|
3487
|
+
* for the usual "who hasn't accepted yet" list.
|
|
3488
|
+
*
|
|
3489
|
+
* The WHOLE invite surface — list included — is gated on the `users.write`
|
|
3490
|
+
* capability plus a signed `users.write:*` scope: a pending invite exposes
|
|
3491
|
+
* the email of someone who is not a member yet, so a `users.read`-only
|
|
3492
|
+
* caller cannot see it. The `invites.read:*` / `invites.write:*` scope names
|
|
3493
|
+
* mint but gate nothing today, so declaring them alone yields FORBIDDEN.
|
|
3494
|
+
* Every call rejects with a `DirectoryError` carrying `code: "FORBIDDEN"`
|
|
3495
|
+
* when the capability is missing — surface it rather than hiding the tab.
|
|
3496
|
+
*/
|
|
3497
|
+
export function useInvites(query) {
|
|
3498
|
+
const ctx = useWidgetContextOrThrow("useInvites");
|
|
3499
|
+
if (
|
|
3500
|
+
!ctx.directory ||
|
|
3501
|
+
!ctx.directory.invites ||
|
|
3502
|
+
typeof ctx.directory.invites.list !== "function"
|
|
3503
|
+
) {
|
|
3504
|
+
throw new Error(
|
|
3505
|
+
"useInvites: host did not inject a directory client (ctx.directory.invites)",
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
const [invites, setInvites] = useState([]);
|
|
3509
|
+
const [loading, setLoading] = useState(true);
|
|
3510
|
+
const [error, setError] = useState(null);
|
|
3511
|
+
|
|
3512
|
+
const queryRef = useRef(query);
|
|
3513
|
+
const invitesRef = useRef(ctx.directory.invites);
|
|
3514
|
+
queryRef.current = query;
|
|
3515
|
+
invitesRef.current = ctx.directory.invites;
|
|
3516
|
+
|
|
3517
|
+
const runRef = useRef(0);
|
|
3518
|
+
|
|
3519
|
+
const doFetch = useCallback(async () => {
|
|
3520
|
+
const myRun = ++runRef.current;
|
|
3521
|
+
setLoading(true);
|
|
3522
|
+
setError(null);
|
|
3523
|
+
try {
|
|
3524
|
+
const res = await invitesRef.current.list(queryRef.current);
|
|
3525
|
+
// Directory invites.list returns the { data, meta } envelope verbatim.
|
|
3526
|
+
const rows = res && Array.isArray(res.data) ? res.data : [];
|
|
3527
|
+
if (runRef.current !== myRun) return;
|
|
3528
|
+
setInvites(rows);
|
|
3529
|
+
setLoading(false);
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
if (runRef.current !== myRun) return;
|
|
3532
|
+
setError(toDirectoryError(err));
|
|
3533
|
+
setLoading(false);
|
|
3534
|
+
}
|
|
3535
|
+
}, []);
|
|
3536
|
+
|
|
3537
|
+
const queryKey = (() => {
|
|
3538
|
+
try {
|
|
3539
|
+
return JSON.stringify(query);
|
|
3540
|
+
} catch (_e) {
|
|
3541
|
+
return null;
|
|
3542
|
+
}
|
|
3543
|
+
})();
|
|
3544
|
+
useEffect(() => {
|
|
3545
|
+
doFetch();
|
|
3546
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3547
|
+
}, [queryKey]);
|
|
3548
|
+
|
|
3549
|
+
const refetch = useCallback(async () => {
|
|
3550
|
+
await doFetch();
|
|
3551
|
+
}, [doFetch]);
|
|
3552
|
+
|
|
3553
|
+
// Both mutations re-list on success: revoking drops the row and resending
|
|
3554
|
+
// moves `expires_at`, so a stale list would show the operator the old state.
|
|
3555
|
+
const resend = useCallback(
|
|
3556
|
+
async (inviteId) => {
|
|
3557
|
+
try {
|
|
3558
|
+
const row = await invitesRef.current.resend(inviteId);
|
|
3559
|
+
await doFetch();
|
|
3560
|
+
return row;
|
|
3561
|
+
} catch (err) {
|
|
3562
|
+
throw toDirectoryError(err);
|
|
3563
|
+
}
|
|
3564
|
+
},
|
|
3565
|
+
[doFetch],
|
|
3566
|
+
);
|
|
3567
|
+
const revoke = useCallback(
|
|
3568
|
+
async (inviteId) => {
|
|
3569
|
+
try {
|
|
3570
|
+
await invitesRef.current.revoke(inviteId);
|
|
3571
|
+
await doFetch();
|
|
3572
|
+
} catch (err) {
|
|
3573
|
+
throw toDirectoryError(err);
|
|
3574
|
+
}
|
|
3575
|
+
},
|
|
3576
|
+
[doFetch],
|
|
3577
|
+
);
|
|
3578
|
+
|
|
3579
|
+
return { invites, loading, error, refetch, resend, revoke };
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3470
3582
|
/**
|
|
3471
3583
|
* REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in app-user,
|
|
3472
3584
|
* and read whether BankID is available + already linked. Returns
|
package/dist/index.d.ts
CHANGED
|
@@ -1523,12 +1523,26 @@ export interface InviteArgs {
|
|
|
1523
1523
|
group_ids?: string[];
|
|
1524
1524
|
}
|
|
1525
1525
|
|
|
1526
|
+
export type AppUserInviteStatus =
|
|
1527
|
+
| "pending"
|
|
1528
|
+
| "accepted"
|
|
1529
|
+
| "revoked"
|
|
1530
|
+
| "expired";
|
|
1531
|
+
|
|
1526
1532
|
export interface AppUserInviteRow {
|
|
1527
1533
|
id: string;
|
|
1528
1534
|
email: string;
|
|
1529
|
-
|
|
1535
|
+
/** Server-computed lifecycle value — prefer it over re-deriving expiry. */
|
|
1536
|
+
status: AppUserInviteStatus;
|
|
1537
|
+
name?: string;
|
|
1538
|
+
group_ids?: string[];
|
|
1530
1539
|
invited_at?: string;
|
|
1531
1540
|
expires_at?: string;
|
|
1541
|
+
accepted_at?: string | null;
|
|
1542
|
+
revoked_at?: string | null;
|
|
1543
|
+
created_at?: string;
|
|
1544
|
+
tenant_id?: string;
|
|
1545
|
+
invited_by_studio_user_id?: string | null;
|
|
1532
1546
|
}
|
|
1533
1547
|
|
|
1534
1548
|
export interface UsersApi {
|
|
@@ -1585,6 +1599,40 @@ export interface GroupsApi {
|
|
|
1585
1599
|
*/
|
|
1586
1600
|
export function useGroups(query?: GroupsQuery): GroupsApi;
|
|
1587
1601
|
|
|
1602
|
+
// --------------------------------------------------------------- useInvites
|
|
1603
|
+
//
|
|
1604
|
+
// sc-5097 — pending AppUser invite administration.
|
|
1605
|
+
|
|
1606
|
+
export interface InvitesQuery {
|
|
1607
|
+
/** Defaults to "all" server-side; pass "pending" for outstanding invites. */
|
|
1608
|
+
status?: AppUserInviteStatus | "all";
|
|
1609
|
+
limit?: number;
|
|
1610
|
+
offset?: number;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
export interface InvitesApi {
|
|
1614
|
+
invites: AppUserInviteRow[];
|
|
1615
|
+
loading: boolean;
|
|
1616
|
+
error: DirectoryError | null;
|
|
1617
|
+
refetch(): Promise<void>;
|
|
1618
|
+
/** Re-send the invitation email. Refetches on success. */
|
|
1619
|
+
resend(inviteId: string): Promise<AppUserInviteRow>;
|
|
1620
|
+
/** Cancel a pending invitation. Refetches on success. */
|
|
1621
|
+
revoke(inviteId: string): Promise<void>;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Pending AppUser invite administration through the injected directory-client
|
|
1626
|
+
* at `ctx.directory.invites.{list,resend,revoke}`.
|
|
1627
|
+
*
|
|
1628
|
+
* The WHOLE surface — listing included — requires the `users.write:*` scope
|
|
1629
|
+
* AND the SystemAcl `users.write` capability, because a pending invite exposes
|
|
1630
|
+
* the email of someone who is not a member yet. The `invites.read:*` /
|
|
1631
|
+
* `invites.write:*` scope names mint but no route enforces them, so declaring
|
|
1632
|
+
* only those yields a `FORBIDDEN` DirectoryError.
|
|
1633
|
+
*/
|
|
1634
|
+
export function useInvites(query?: InvitesQuery): InvitesApi;
|
|
1635
|
+
|
|
1588
1636
|
// ----------------------------------------------------- useBankIdLink
|
|
1589
1637
|
//
|
|
1590
1638
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in app-user.
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/linter.cjs
CHANGED
|
@@ -447,6 +447,7 @@ function _scopeRules(source, manifest) {
|
|
|
447
447
|
);
|
|
448
448
|
const usesUsersHook = /\buseUsers\s*\(/.test(source);
|
|
449
449
|
const usesGroupsHook = /\buseGroups\s*\(/.test(source);
|
|
450
|
+
const usesInvitesHook = /\buseInvites\s*\(/.test(source);
|
|
450
451
|
|
|
451
452
|
if (usesUsersHook) {
|
|
452
453
|
const reads = declared.has("users.read:*") || declared.has("users.read");
|
|
@@ -473,6 +474,22 @@ function _scopeRules(source, manifest) {
|
|
|
473
474
|
}
|
|
474
475
|
}
|
|
475
476
|
|
|
477
|
+
// sc-5097 — the whole invite surface (list, resend, revoke) is gated on
|
|
478
|
+
// the "users.write" capability, and the scope gate accepts only a signed
|
|
479
|
+
// scope matching that key. `invites.*` scopes mint but gate nothing.
|
|
480
|
+
if (usesInvitesHook) {
|
|
481
|
+
const writes =
|
|
482
|
+
declared.has("users.write:*") || declared.has("users.write");
|
|
483
|
+
if (!writes) {
|
|
484
|
+
findings.push({
|
|
485
|
+
rule: "scope-required-for-useInvites",
|
|
486
|
+
label:
|
|
487
|
+
"useInvites() requires `users.write:*` in manifest.requestedScopes",
|
|
488
|
+
line: 0,
|
|
489
|
+
snippet: "",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
476
493
|
const lines = source.split(/\r?\n/);
|
|
477
494
|
let firedUsersWrite = false;
|
|
478
495
|
let firedUsersDelete = false;
|
package/dist/linter.js
CHANGED
|
@@ -522,6 +522,7 @@ function _scopeRules(source, manifest) {
|
|
|
522
522
|
);
|
|
523
523
|
const usesUsersHook = /\buseUsers\s*\(/.test(source);
|
|
524
524
|
const usesGroupsHook = /\buseGroups\s*\(/.test(source);
|
|
525
|
+
const usesInvitesHook = /\buseInvites\s*\(/.test(source);
|
|
525
526
|
|
|
526
527
|
if (usesUsersHook) {
|
|
527
528
|
const reads = declared.has("users.read:*") || declared.has("users.read");
|
|
@@ -547,6 +548,22 @@ function _scopeRules(source, manifest) {
|
|
|
547
548
|
}
|
|
548
549
|
}
|
|
549
550
|
|
|
551
|
+
// sc-5097 — the whole invite surface (list, resend, revoke) is gated on
|
|
552
|
+
// the "users.write" capability, and the scope gate accepts only a signed
|
|
553
|
+
// scope matching that key. `invites.*` scopes mint but gate nothing.
|
|
554
|
+
if (usesInvitesHook) {
|
|
555
|
+
const writes =
|
|
556
|
+
declared.has("users.write:*") || declared.has("users.write");
|
|
557
|
+
if (!writes) {
|
|
558
|
+
findings.push({
|
|
559
|
+
rule: "scope-required-for-useInvites",
|
|
560
|
+
label:
|
|
561
|
+
"useInvites() requires `users.write:*` in manifest.requestedScopes",
|
|
562
|
+
line: 0,
|
|
563
|
+
snippet: "",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
550
567
|
const lines = source.split(/\r?\n/);
|
|
551
568
|
let firedUsersWrite = false;
|
|
552
569
|
let firedUsersDelete = false;
|
package/dist/manifest.cjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// re-exports the same functions defined here so there is exactly one
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
|
-
const { CONTRACT
|
|
7
|
+
const { CONTRACT } = require("./contract.cjs");
|
|
8
8
|
|
|
9
9
|
const PAYLOAD_VALUE_TYPES = CONTRACT.payloadValueTypes;
|
|
10
10
|
|
|
@@ -39,20 +39,6 @@ const VALID_CATEGORIES = new Set([
|
|
|
39
39
|
]);
|
|
40
40
|
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
41
41
|
|
|
42
|
-
// REQ-WIDGET-ACTION — structural validation for manifest-declared server
|
|
43
|
-
// actions. Mirrors the backend action.service caps; the runner re-validates
|
|
44
|
-
// the cron expression (node-cron) and binds the tenant-local API key + table
|
|
45
|
-
// at enable time, so we only check shape + size here (cross-env: no Buffer).
|
|
46
|
-
const VALID_ACTION_TRIGGERS = new Set([
|
|
47
|
-
"schedule",
|
|
48
|
-
"record_created",
|
|
49
|
-
"record_updated",
|
|
50
|
-
"record_deleted",
|
|
51
|
-
]);
|
|
52
|
-
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
53
|
-
const ACTION_TIMEOUT_MIN_MS = 100;
|
|
54
|
-
const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
|
|
55
|
-
|
|
56
42
|
// REQ-L10N-WIDGET — caps + patterns for manifest-declared translations.
|
|
57
43
|
// The relative key pattern is deliberately tighter than the dictionary's
|
|
58
44
|
// own KEY_RE: once the host namespaces it (`widget.<id>.<key>`) the result
|
|
@@ -133,54 +119,22 @@ function validateManifestTranslations(translations, manifestId, errors) {
|
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
121
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
seenKeys.add(a.key);
|
|
153
|
-
}
|
|
154
|
-
pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
|
|
155
|
-
const triggerTypes = normaliseActionTriggerTypes(a);
|
|
156
|
-
if (triggerTypes === null) {
|
|
157
|
-
errors.push(
|
|
158
|
-
`manifest.actions[].triggerTypes must be a non-empty array of unique values from ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
|
|
159
|
-
);
|
|
160
|
-
} else if (triggerTypes.includes("schedule")) {
|
|
161
|
-
pushIf(
|
|
162
|
-
errors,
|
|
163
|
-
isNonEmptyString(a.scheduleCron),
|
|
164
|
-
"manifest.actions[].scheduleCron is required when triggerTypes contains 'schedule'",
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
if (!isNonEmptyString(a.scriptSource)) {
|
|
168
|
-
errors.push("manifest.actions[].scriptSource must be a non-empty string");
|
|
169
|
-
} else if (utf8ByteLength(a.scriptSource) > ACTION_SCRIPT_MAX_BYTES) {
|
|
170
|
-
errors.push("manifest.actions[].scriptSource exceeds 200 KiB");
|
|
171
|
-
}
|
|
172
|
-
if (a.timeoutMs !== undefined) {
|
|
173
|
-
const t = Number(a.timeoutMs);
|
|
174
|
-
if (!Number.isFinite(t) || t < ACTION_TIMEOUT_MIN_MS || t > ACTION_TIMEOUT_MAX_MS) {
|
|
175
|
-
errors.push("manifest.actions[].timeoutMs must be between 100 and 300000");
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
179
|
-
errors.push(
|
|
180
|
-
"manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
122
|
+
// REQ-MKT-ACTION (sc-4879): a widget no longer declares server-side actions.
|
|
123
|
+
//
|
|
124
|
+
// `manifest.actions` was how an automation shipped before Actions existed as
|
|
125
|
+
// their own marketplace deliverable — a widget was two products in one package,
|
|
126
|
+
// a component that renders and a script that never renders at all. It is now
|
|
127
|
+
// REFUSED rather than ignored: an author who declares it believes the
|
|
128
|
+
// automation will ship, and silence would publish that misunderstanding.
|
|
129
|
+
//
|
|
130
|
+
// The replacement is a marketplace Action (`@colixsystems/action-sdk`), which
|
|
131
|
+
// the workspace installs and configures on its own.
|
|
132
|
+
function validateManifestActions(_actions, errors) {
|
|
133
|
+
errors.push(
|
|
134
|
+
"manifest.actions is no longer supported — ship the automation as a " +
|
|
135
|
+
"marketplace Action (@colixsystems/action-sdk) instead of declaring it " +
|
|
136
|
+
"on a widget",
|
|
137
|
+
);
|
|
184
138
|
}
|
|
185
139
|
|
|
186
140
|
function canonicalCategory(c) {
|
|
@@ -355,7 +309,7 @@ function validateManifest(m) {
|
|
|
355
309
|
}
|
|
356
310
|
}
|
|
357
311
|
|
|
358
|
-
//
|
|
312
|
+
// Present at all is now an error (sc-4879) — see validateManifestActions.
|
|
359
313
|
if (manifest.actions !== undefined) {
|
|
360
314
|
validateManifestActions(manifest.actions, errors);
|
|
361
315
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|