@colixsystems/widget-sdk 0.52.0 → 0.54.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 CHANGED
@@ -19,7 +19,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
19
19
  | ----- | ---------------- | ------- | ------------- |
20
20
  | **CORE** | `useTheme()` | `{ colors, spacing, radii, typography }` | `ctx.workspace.theme` — no scope |
21
21
  | **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
22
- | **CORE** | `useUser()` | `{ id, email, display_name, roles, group_ids }` | `ctx.user` (snake_case verbatim; `id` null when anonymous) — no scope |
22
+ | **CORE** | `useUser()` | `{ id, email, displayName, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope |
23
23
  | **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute }` | `ctx.navigation` — no scope (external URLs use the `Linking` primitive) |
24
24
  | **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope |
25
25
  | **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
@@ -40,6 +40,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
40
40
  | **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (edits also `users.write:*`; `remove()` also `users.delete:*`) |
41
41
  | **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
42
42
  | **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
43
+ | **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. |
43
44
  | **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser` |
44
45
  | **NOTIFICATIONS** (`ctx.notifications`) | `useSendNotification()` | `{ send, sending, error }` | `ctx.notifications.send` — `notifications.send:appUser`. `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace; call from an event handler (never render); rejects with `NotificationError`. |
45
46
 
@@ -51,7 +52,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
51
52
 
52
53
  ## Status
53
54
 
54
- `v0.52.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**.
55
+ `v0.54.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**.
56
+
57
+ ### What's new in 0.54.0
58
+
59
+ **Generate & save PDFs from a widget (sc-2314).** New `usePdfExport({ spaceType, folderId? })` hook. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF **server-side** (the platform's headless-Chromium pipeline) and saves it into the end-user's Filestore via `ctx.filestore.files.exportPdf`, resolving to the created file row (`application/pdf`). It reuses the filestore owner_id resolution + per-folder write gate and the existing `files.write:*` scope. Because the rendering is server-side, the capability behaves identically on the web Player and the native Expo export — no browser-only PDF library is added to the vetted set. Pairs with `@colixsystems/filestore-client@0.6.0`'s new `files.exportPdf(...)`. `CONTRACT.version` → `1.38.0`. Additive — no existing hook, primitive, manifest field, or token changed shape.
60
+
61
+ ### What's new in 0.53.0
62
+
63
+ **Server-action scripts gain a `connectors` global (REQ-ACTION-CONNECTORS, sc-2162).** A `scriptSource` action can now call `connectors.call(slug, { method, path, query, body, headers })` to invoke a tenant-configured REST connector by slug; it returns the upstream's `{ status, headers, body }`. Auth and SSRF protection are handled by the platform host — the script names only a slug (never a tenant or base URL), and the tenant is bound host-side. An unknown slug / SSRF rejection / timeout throws a catchable Error. New entry in `CONTRACT.actionScriptGlobals`; `CONTRACT.version` → `1.37.0`. Additive — no widget hook, primitive, manifest field, or token changed shape.
55
64
 
56
65
  ### What's new in 0.52.0
57
66
 
@@ -274,7 +283,7 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
274
283
 
275
284
  - **The SDK no longer owns any data facade.** The bespoke per-hook facades that used to live on `WidgetContext` (`ctx.datastore` as an opaque host object, `ctx.directory.listUsers`, `ctx.users`, `ctx.groups`, `ctx.recordPermissions`, `ctx.assets.get`) are replaced by four host-instantiated, host-injected domain clients: `ctx.datastore` (`@colixsystems/datastore-client`), `ctx.directory` (`@colixsystems/directory-client`), `ctx.assets` (`@colixsystems/assets-client`, flattened), `ctx.payments` (`@colixsystems/payments-client`). The SDK imports none of them and ships no HTTP.
276
285
  - **`ctx.recordPermissions`, `ctx.users`, `ctx.groups` are removed.** Per-record permission management moved under `ctx.datastore.records(tableId).permissions(recordId)`; user / group administration moved under `ctx.directory.users` / `ctx.directory.groups`. The hooks (`useRecordPermissions`, `useUsers`, `useGroups`) keep the same names and signatures — only the client slice they read changed.
277
- - **snake_case end to end, no client-side transform.** Clients send and return snake_case verbatim (`group_ids`, `can_read`, `is_active`, `amount_cents`, `data_type`, `created_at`, …). The SDK passes bodies straight through and unwraps the `{ data, meta }` list envelope without renaming a single field. Hook return rows and `useUser()` fields are therefore snake_case (`display_name`, `group_ids`, `is_active`, `member_count`, and `useRecordPermissions` rows carry `user_id` / `group_id` / `can_read` / `can_write` / `can_delete` / `can_grant`).
286
+ - **snake_case end to end, no client-side transform.** Clients send and return snake_case verbatim (`group_ids`, `can_read`, `is_active`, `amount_cents`, `data_type`, `created_at`, …). The SDK passes bodies straight through and unwraps the `{ data, meta }` list envelope without renaming a single field. Wire-payload hook return rows are therefore snake_case (`is_active`, `member_count`, and `useRecordPermissions` rows carry `user_id` / `group_id` / `can_read` / `can_write` / `can_delete` / `can_grant`). The one exception is `useUser()`: it reads the host-built `ctx.user` context object, not a wire payload, so its fields are **camelCase** (`displayName`, `groupIds`).
278
287
  - **Companion package versions:** `datastore-client 0.5.0`, `assets-client 0.4.0`, `directory-client 0.1.0`, `payments-client 0.1.0`.
279
288
  - **`CONTRACT.version` → `1.9.0`.** Breaking for `WidgetContext` consumers (removed slices, renamed wire fields); the hook export surface is unchanged.
280
289
 
@@ -284,7 +293,7 @@ Two additive features land in this version.
284
293
 
285
294
  **A widget may declare server-side actions in its manifest.**
286
295
 
287
- - **`WidgetManifest.actions` is now part of the public contract.** An optional array; each entry is `{ key, name, description?, triggerType, scheduleCron?, timeoutMs?, scriptSource }`. `triggerType` is one of `schedule`, `record_created`, `record_updated`, `record_deleted`; `scheduleCron` is required when `triggerType === "schedule"`. The `scriptSource` (≤ 200 KiB) runs in the **shared isolated-vm action runner** — against `datastore` / `fetch` / `console` / `record` / `tenantId` (the runner surface, **not** the React/SDK widget surface, so SDK imports and hooks are unavailable there and the component linter does not scan it). Actions never run in the rendered app, so they have **no effect on Player ↔ export parity**.
296
+ - **`WidgetManifest.actions` is now part of the public contract.** An optional array; each entry is `{ key, name, description?, triggerType, scheduleCron?, timeoutMs?, scriptSource }`. `triggerType` is one of `schedule`, `record_created`, `record_updated`, `record_deleted`; `scheduleCron` is required when `triggerType === "schedule"`. The `scriptSource` (≤ 200 KiB) runs in the **shared isolated-vm action runner** — against `datastore` / `fetch` / `connectors` / `console` / `record` / `tenantId` (the runner surface, **not** the React/SDK widget surface, so SDK imports and hooks are unavailable there and the component linter does not scan it). `connectors.call(slug, { method, path, query, body, headers })` resolves a tenant-configured REST connector by slug and returns `{ status, headers, body }` (auth + SSRF handled by the platform; an unknown slug / SSRF / timeout throws a catchable Error). Actions never run in the rendered app, so they have **no effect on Player ↔ export parity**.
288
297
  - **Operators enable actions per tenant** from the Properties Panel. An enabled action materialises a tenant `Action` row **DISABLED** until the operator binds an integration API key (and, for `record_*` triggers, a target table) in the Actions admin page — those bindings are tenant-local, so `triggerTableId` / `apiKeyId` must **not** appear in the manifest (the validator and linter reject them).
289
298
  - **New contract fields** `CONTRACT.actionTriggerTypes`, `CONTRACT.actionScriptGlobals`, `CONTRACT.actionScriptMaxBytes` expose the grammar so the Developer page, the AI agent prompt, and `validateManifest` derive it from one source. `validateManifest` now structurally validates `actions`; the marketplace linter rejects malformed / oversized declarations.
290
299
  - **New manifest category `ADMINISTRATION`** for app-administration widgets such as User Management. Added to `CONTRACT.manifestCategories`, `validateManifest`, the `WidgetCategory` type, the marketplace category list, and the master-DB `WidgetCategory` enum.
@@ -357,7 +366,7 @@ The "split-implementation + vetted package list" pivot.
357
366
 
358
367
  ### What's new in 0.9.0
359
368
 
360
- - **`usePayments()` — incoming app-user payments.** Returns `{ requestPayment, getPayment }`. `requestPayment({ amountCents, currency?, description, metadata?, returnPath? })` triggers a one-time charge from the signed-in app user and resolves to `{ id, status, checkoutUrl? }`: when `checkoutUrl` is present the widget opens it (web: navigate; native: `expo-web-browser`) — the user pays in **hosted Stripe Checkout**; when absent (the platform's built-in **mock** provider, the default until Stripe is configured) the charge auto-confirms (`status: "PAID"`). `getPayment(id)` polls the terminal status. Backed by a new `WidgetContext.payments` slice and gated by the new `payments.charge:appUser` scope. **No card data ever touches the widget** — never collect card fields yourself. The charge settles to the workspace owner; the amount is bounded by a platform per-charge cap. Rejections are a structured `PaymentError` (also a new named export) with a stable `.code`.
369
+ - **`usePayments()` — incoming app-user payments.** Returns `{ requestPayment, getPayment }`. `requestPayment({ amountCents, currency?, description, metadata?, returnPath? })` triggers a one-time charge from the signed-in app user and resolves to `{ id, status, checkoutUrl? }`: when `checkoutUrl` is present the widget opens it (web: navigate; native: `expo-web-browser`) — the user pays in the provider's **hosted checkout**; when absent (the platform's built-in **mock** provider, the default until a real provider is configured) the charge auto-confirms (`status: "PAID"`). `getPayment(id)` polls the terminal status. Backed by a new `WidgetContext.payments` slice and gated by the new `payments.charge:appUser` scope. **No card data ever touches the widget** — never collect card fields yourself. The charge settles to the workspace owner; the amount is bounded by a platform per-charge cap. Rejections are a structured `PaymentError` (also a new named export) with a stable `.code`.
361
370
  - **`CONTRACT.version` → `1.2.0`** (additive: one new hook, one new context slice, one new scope, one new error class). No existing export changed signature.
362
371
 
363
372
  ### What was in 0.8.0
@@ -419,7 +428,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
419
428
 
420
429
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
421
430
  - `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
422
- - `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, display_name, roles, group_ids }` (snake_case verbatim; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
431
+ - `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
423
432
  - `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
424
433
  - `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
425
434
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
package/dist/contract.cjs CHANGED
@@ -235,6 +235,29 @@ const HOOKS = [
235
235
  requiredContextSlice: ["filestore.files"],
236
236
  scopes: ["files.write:*"],
237
237
  },
238
+ {
239
+ name: "usePdfExport",
240
+ signature: "usePdfExport({ spaceType, folderId? })",
241
+ description:
242
+ "Render an HTML string to a PDF server-side and SAVE it as a file in " +
243
+ "the end-user's Filestore space. The widget passes the SPACE " +
244
+ "(`{ spaceType, folderId? }`); the hook resolves owner_id from the host " +
245
+ "context and POSTs JSON through ctx.filestore.files.exportPdf. " +
246
+ "`exportToPdf(html, { fileName?, folderId? })` resolves to the created " +
247
+ "file row (mime_type `application/pdf`) or throws the wire error; a 404 " +
248
+ "means the destination folder denied a write, a 413 that the rendered " +
249
+ "PDF exceeded the size cap. The HTML is rendered by the host's PDF " +
250
+ "service (headless Chromium) — the SAME server-side pipeline on the web " +
251
+ "Player and the native export, so no browser-only PDF library is used.",
252
+ returnShape: {
253
+ exportToPdf: "(html, { fileName?, folderId? }) => Promise<FilestoreFile>",
254
+ exporting: "boolean",
255
+ error: "Error | null",
256
+ lastExported: "FilestoreFile | null",
257
+ },
258
+ requiredContextSlice: ["filestore.files"],
259
+ scopes: ["files.write:*"],
260
+ },
238
261
  {
239
262
  name: "useFilestoreFolders",
240
263
  signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
@@ -818,6 +841,7 @@ const ACTION_TRIGGER_TYPES = [
818
841
  const ACTION_SCRIPT_GLOBALS = [
819
842
  "datastore",
820
843
  "fetch",
844
+ "connectors",
821
845
  "console",
822
846
  "record",
823
847
  "tenantId",
@@ -974,9 +998,9 @@ const WIDGET_CONTEXT_SHAPE = {
974
998
  },
975
999
  user: {
976
1000
  description:
977
- "Signed-in user, host-provided VERBATIM (snake_case: { id, email, display_name, roles, group_ids }). Not a data-client.",
1001
+ "Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
978
1002
  required: true,
979
- fields: { id: "string", email: "string", display_name: "string" },
1003
+ fields: { id: "string", email: "string", displayName: "string" },
980
1004
  },
981
1005
  workspace: {
982
1006
  description:
@@ -1815,7 +1839,26 @@ const CONTRACT = deepFreeze({
1815
1839
  // `notifications.send:appUser` scope it gates on. No existing hook,
1816
1840
  // primitive, manifest field, or token changed shape — minor bump on the
1817
1841
  // pre-1.0 channel.
1818
- version: "1.36.0",
1842
+ //
1843
+ // 1.37.0: additive (REQ-ACTION-CONNECTORS, sc-2162) — server-action scripts
1844
+ // gain a `connectors` global. `connectors.call(slug, { method, path,
1845
+ // query, body, headers })` resolves a tenant-configured REST connector by
1846
+ // slug and returns `{ status, headers, body }`; auth + SSRF are handled by
1847
+ // the platform host, and an unknown slug / SSRF / timeout throws a
1848
+ // catchable Error. New entry in ACTION_SCRIPT_GLOBALS only — no widget
1849
+ // hook, primitive, manifest field, or token changed shape; minor bump.
1850
+ //
1851
+ // 1.38.0: additive (sc-2314) — new `usePdfExport({ spaceType, folderId? })`
1852
+ // hook. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to
1853
+ // a PDF server-side (the host's headless-Chromium pipeline) and SAVES it
1854
+ // into the Filestore via `ctx.filestore.files.exportPdf`, resolving to the
1855
+ // created file row (`application/pdf`). Reuses the filestore owner_id
1856
+ // resolution + per-folder write gate; gated on the existing
1857
+ // `files.write:*` scope. The same server-side renderer backs the web
1858
+ // Player and the native export, so no browser-only PDF library is added
1859
+ // to the vetted set. No existing hook, primitive, manifest field, or
1860
+ // token changed shape — minor bump.
1861
+ version: "1.38.0",
1819
1862
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1820
1863
  hooks: HOOKS,
1821
1864
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -235,6 +235,29 @@ const HOOKS = [
235
235
  requiredContextSlice: ["filestore.files"],
236
236
  scopes: ["files.write:*"],
237
237
  },
238
+ {
239
+ name: "usePdfExport",
240
+ signature: "usePdfExport({ spaceType, folderId? })",
241
+ description:
242
+ "Render an HTML string to a PDF server-side and SAVE it as a file in " +
243
+ "the end-user's Filestore space. The widget passes the SPACE " +
244
+ "(`{ spaceType, folderId? }`); the hook resolves owner_id from the host " +
245
+ "context and POSTs JSON through ctx.filestore.files.exportPdf. " +
246
+ "`exportToPdf(html, { fileName?, folderId? })` resolves to the created " +
247
+ "file row (mime_type `application/pdf`) or throws the wire error; a 404 " +
248
+ "means the destination folder denied a write, a 413 that the rendered " +
249
+ "PDF exceeded the size cap. The HTML is rendered by the host's PDF " +
250
+ "service (headless Chromium) — the SAME server-side pipeline on the web " +
251
+ "Player and the native export, so no browser-only PDF library is used.",
252
+ returnShape: {
253
+ exportToPdf: "(html, { fileName?, folderId? }) => Promise<FilestoreFile>",
254
+ exporting: "boolean",
255
+ error: "Error | null",
256
+ lastExported: "FilestoreFile | null",
257
+ },
258
+ requiredContextSlice: ["filestore.files"],
259
+ scopes: ["files.write:*"],
260
+ },
238
261
  {
239
262
  name: "useFilestoreFolders",
240
263
  signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
@@ -818,6 +841,7 @@ const ACTION_TRIGGER_TYPES = [
818
841
  const ACTION_SCRIPT_GLOBALS = [
819
842
  "datastore",
820
843
  "fetch",
844
+ "connectors",
821
845
  "console",
822
846
  "record",
823
847
  "tenantId",
@@ -974,9 +998,9 @@ const WIDGET_CONTEXT_SHAPE = {
974
998
  },
975
999
  user: {
976
1000
  description:
977
- "Signed-in user, host-provided VERBATIM (snake_case: { id, email, display_name, roles, group_ids }). Not a data-client.",
1001
+ "Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
978
1002
  required: true,
979
- fields: { id: "string", email: "string", display_name: "string" },
1003
+ fields: { id: "string", email: "string", displayName: "string" },
980
1004
  },
981
1005
  workspace: {
982
1006
  description:
@@ -1815,7 +1839,26 @@ const CONTRACT = deepFreeze({
1815
1839
  // `notifications.send:appUser` scope it gates on. No existing hook,
1816
1840
  // primitive, manifest field, or token changed shape — minor bump on the
1817
1841
  // pre-1.0 channel.
1818
- version: "1.36.0",
1842
+ //
1843
+ // 1.37.0: additive (REQ-ACTION-CONNECTORS, sc-2162) — server-action scripts
1844
+ // gain a `connectors` global. `connectors.call(slug, { method, path,
1845
+ // query, body, headers })` resolves a tenant-configured REST connector by
1846
+ // slug and returns `{ status, headers, body }`; auth + SSRF are handled by
1847
+ // the platform host, and an unknown slug / SSRF / timeout throws a
1848
+ // catchable Error. New entry in ACTION_SCRIPT_GLOBALS only — no widget
1849
+ // hook, primitive, manifest field, or token changed shape; minor bump.
1850
+ //
1851
+ // 1.38.0: additive (sc-2314) — new `usePdfExport({ spaceType, folderId? })`
1852
+ // hook. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to
1853
+ // a PDF server-side (the host's headless-Chromium pipeline) and SAVES it
1854
+ // into the Filestore via `ctx.filestore.files.exportPdf`, resolving to the
1855
+ // created file row (`application/pdf`). Reuses the filestore owner_id
1856
+ // resolution + per-folder write gate; gated on the existing
1857
+ // `files.write:*` scope. The same server-side renderer backs the web
1858
+ // Player and the native export, so no browser-only PDF library is added
1859
+ // to the vetted set. No existing hook, primitive, manifest field, or
1860
+ // token changed shape — minor bump.
1861
+ version: "1.38.0",
1819
1862
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1820
1863
  hooks: HOOKS,
1821
1864
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -113,7 +113,7 @@ export function useWidgetStyle() {
113
113
 
114
114
  /**
115
115
  * Returns the active end-user identity VERBATIM from the host, e.g.
116
- * `{ id, email, display_name, roles, group_ids }` (snake_case fields).
116
+ * `{ id, email, displayName, roles, groupIds }` (camelCase — host-built context, not a wire payload).
117
117
  *
118
118
  * `id` is `null` for anonymous visitors (and on the Studio canvas preview,
119
119
  * which renders widgets as if signed-out so the public branch shows). The
@@ -1578,6 +1578,87 @@ export function useFilestoreUpload(options) {
1578
1578
  return { upload, uploading, error, lastUploaded };
1579
1579
  }
1580
1580
 
1581
+ /**
1582
+ * sc-2314 — render an HTML string to a PDF server-side and SAVE it into the
1583
+ * end-user's Filestore space. Returns `{ exportToPdf, exporting, error,
1584
+ * lastExported }`. The widget passes the SPACE (`{ spaceType, folderId? }`);
1585
+ * the hook resolves `owner_id` the same way the upload hook does (tenant for
1586
+ * PROJECT, app user for PERSONAL) and posts JSON to
1587
+ * `ctx.filestore.files.exportPdf`.
1588
+ *
1589
+ * `exportToPdf(html, { fileName?, folderId? })` resolves to the created file
1590
+ * record (`{ id, name, mime_type: "application/pdf", presigned_url, … }`).
1591
+ * `fileName` defaults to `document.pdf` and is forced to a `.pdf` suffix by the
1592
+ * backend. The HTML is rendered by the host's PDF service (headless Chromium)
1593
+ * — the SAME server-side pipeline backs the web Player and the native export,
1594
+ * so the capability behaves identically on both platforms (no browser-only PDF
1595
+ * library is involved).
1596
+ *
1597
+ * A 404 means the destination folder denied a write; a 413 means the rendered
1598
+ * PDF exceeded the size cap.
1599
+ */
1600
+ export function usePdfExport(options) {
1601
+ const ctx = useWidgetContextOrThrow("usePdfExport");
1602
+ if (
1603
+ !ctx.filestore ||
1604
+ !ctx.filestore.files ||
1605
+ typeof ctx.filestore.files.exportPdf !== "function"
1606
+ ) {
1607
+ throw new Error(
1608
+ "usePdfExport: host did not inject a filestore client (ctx.filestore.files.exportPdf)",
1609
+ );
1610
+ }
1611
+ const { spaceType = "project", folderId: defaultFolderId = null } = options || {};
1612
+ const ownerId = _filestoreOwnerId(ctx, spaceType);
1613
+
1614
+ const [exporting, setExporting] = useState(false);
1615
+ const [error, setError] = useState(null);
1616
+ const [lastExported, setLastExported] = useState(null);
1617
+
1618
+ const filesRef = useRef(ctx.filestore.files);
1619
+ filesRef.current = ctx.filestore.files;
1620
+
1621
+ const exportToPdf = useCallback(
1622
+ async (html, overrides) => {
1623
+ if (typeof html !== "string" || html.trim().length === 0) {
1624
+ throw new Error("usePdfExport: html is required");
1625
+ }
1626
+ if (!ownerId) {
1627
+ const err = new Error("Sign in to save a PDF");
1628
+ setError(err);
1629
+ throw err;
1630
+ }
1631
+ const folderId =
1632
+ overrides && Object.prototype.hasOwnProperty.call(overrides, "folderId")
1633
+ ? overrides.folderId
1634
+ : defaultFolderId;
1635
+ const fileName = overrides && overrides.fileName;
1636
+ const body = {
1637
+ html,
1638
+ space_type: String(spaceType || "project").toUpperCase(),
1639
+ owner_id: ownerId,
1640
+ };
1641
+ if (folderId) body.folder_id = folderId;
1642
+ if (fileName) body.file_name = fileName;
1643
+ setExporting(true);
1644
+ setError(null);
1645
+ try {
1646
+ const created = await filesRef.current.exportPdf(body);
1647
+ setLastExported(created || null);
1648
+ setExporting(false);
1649
+ return created;
1650
+ } catch (err) {
1651
+ setError(err);
1652
+ setExporting(false);
1653
+ throw err;
1654
+ }
1655
+ },
1656
+ [ownerId, defaultFolderId, spaceType],
1657
+ );
1658
+
1659
+ return { exportToPdf, exporting, error, lastExported };
1660
+ }
1661
+
1581
1662
  /**
1582
1663
  * Browse the end-user's Filestore folders. Returns { folders, loading, error,
1583
1664
  * refetch }. Mirrors useFilestoreFiles for subfolder navigation: the widget
@@ -2638,8 +2719,8 @@ function toPaymentError(err) {
2638
2719
  * requestPayment({ amountCents, currency?, description, metadata? })
2639
2720
  * → Promise<{ id, status, checkoutUrl?, ... }>. The host either
2640
2721
  * auto-confirms (mock provider, `status: "PAID"`, no redirect) or
2641
- * returns a hosted-Checkout `checkoutUrl` the widget should open
2642
- * (Stripe provider, `status: "PENDING"`). Rejects with a
2722
+ * returns a hosted-checkout `checkoutUrl` the widget should open
2723
+ * (Mollie provider, `status: "PENDING"`). Rejects with a
2643
2724
  * `PaymentError`.
2644
2725
  * getPayment(paymentId) → Promise<payment> — poll the terminal status.
2645
2726
  *
package/dist/index.d.ts CHANGED
@@ -424,13 +424,13 @@ export interface WidgetContext<TProps = unknown> {
424
424
  * Absent / `false` everywhere the host has not opted the widget into filling.
425
425
  */
426
426
  fill?: boolean;
427
- /** Active end-user identity, snake_case verbatim. `id` is null when anonymous. */
427
+ /** Active end-user identity from the host-built context (camelCase, not a wire payload). `id` is null when anonymous. */
428
428
  user: {
429
429
  id: string | null;
430
430
  email: string | null;
431
- display_name: string | null;
431
+ displayName: string | null;
432
432
  roles: string[];
433
- group_ids: string[];
433
+ groupIds: string[];
434
434
  };
435
435
  workspace: {
436
436
  id: string;
@@ -624,8 +624,8 @@ export interface PaymentResult {
624
624
  currency?: string;
625
625
  description?: string;
626
626
  /**
627
- * Present (Stripe provider) when the app user must complete a hosted
628
- * Checkout: the widget should open this URL. Absent under the mock
627
+ * Present (Mollie provider) when the app user must complete a hosted
628
+ * checkout: the widget should open this URL. Absent under the mock
629
629
  * provider, where the charge auto-confirms (`status: "PAID"`).
630
630
  */
631
631
  checkoutUrl?: string | null;
@@ -883,9 +883,9 @@ export function useI18n(): {
883
883
  export function useUser(): {
884
884
  id: string | null;
885
885
  email: string | null;
886
- display_name: string | null;
886
+ displayName: string | null;
887
887
  roles: string[];
888
- group_ids: string[];
888
+ groupIds: string[];
889
889
  };
890
890
 
891
891
  /**
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export {
19
19
  useAssetsByTag,
20
20
  useFilestoreFiles,
21
21
  useFilestoreUpload,
22
+ usePdfExport,
22
23
  useFilestoreFolders,
23
24
  useFileSignature,
24
25
  useFileSignatures,
@@ -19,6 +19,7 @@ export {
19
19
  useAssetsByTag,
20
20
  useFilestoreFiles,
21
21
  useFilestoreUpload,
22
+ usePdfExport,
22
23
  useFilestoreFolders,
23
24
  useFileSignature,
24
25
  useFileSignatures,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.52.0",
3
+ "version": "0.54.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",