@cube-nl/dmp-sdk 1.1.0-rc.1 → 1.1.0-rc.3

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
@@ -50,9 +50,9 @@ For an app without a build step, the single-file browser build is on the npm CDN
50
50
  version:
51
51
 
52
52
  ```html
53
- <script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
53
+ <script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.1.0-rc.3/dist/cube-sdk.js"></script>
54
54
  <!-- or -->
55
- <script src="https://unpkg.com/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
55
+ <script src="https://unpkg.com/@cube-nl/dmp-sdk@1.1.0-rc.3/dist/cube-sdk.js"></script>
56
56
  ```
57
57
 
58
58
  Fallback, for an unreleased commit: `npm install
@@ -578,6 +578,76 @@ What the preview cannot do: mock data names no relations, so an `include` or `wi
578
578
  in `meta.omitted` (show it as "not visible", as live) and `has` is not applied; custom operations
579
579
  are `404 not_found`; `location` and `etag` are null.
580
580
 
581
+ ### Trial (proefversie)
582
+
583
+ A maker can try an app on their own data before it is approved: the DMP serves the app's files in a sandboxed page
584
+ (CSP `sandbox`, an opaque origin) and writes a read-only token into that page. The SDK then runs the app as a third
585
+ mode, `'trial'`, with the same code as preview and live.
586
+
587
+ The DMP inserts one data block as the first child of `<head>` (a JSON data block, not a script, so the CSP does not
588
+ apply to it):
589
+
590
+ ```html
591
+ <script type="application/json" id="cube-trial-config">{"mode":"trial","dmpUrl":"https://dmp.example.nl",
592
+ "apiUrl":"https://dmp.example.nl/apps/api/v1","token":"cubeproef_…","expiresAt":"2026-10-20T15:42:00+02:00",
593
+ "statusUrl":"https://dmp.example.nl/apps/api/v1/_trial","version":"1.0.3","soort":null,"userId":"42",
594
+ "leest":["orders","klanten"]}</script>
595
+ ```
596
+
597
+ | Member | |
598
+ |---|---|
599
+ | `mode` | Must be `"trial"` |
600
+ | `dmpUrl`, `apiUrl`, `token`, `expiresAt`, `statusUrl` | Required non-empty strings. `expiresAt` is ISO 8601 |
601
+ | `version`, `soort`, `userId` | A string or `null` |
602
+ | `leest` | The resources the trial reads, for the banner; default `[]` |
603
+
604
+ - **Read once, then removed.** When `cube-sdk.js` (or the ES module, in a browser) evaluates, it reads the element,
605
+ removes it from the DOM and keeps the config in memory. A config that is not JSON, not `mode: "trial"` or misses a
606
+ member gives one `console.warn` (without its content) and the app starts as it would without it.
607
+ - **The session.** `connect()` checks the trial config first: it does not fetch `cube-app.config.json`, does no OAuth
608
+ (no discovery, no PKCE, no callback) and touches neither `sessionStorage` nor `localStorage`. Every request carries
609
+ `Authorization: Bearer cubeproef_…` to `apiUrl`. `user()` is `{ id: userId, name: null, scopes: ['api:read'] }`;
610
+ `signIn()` reloads the page (the DMP writes a token on every load); `signOut()` makes calls from the app stop carrying
611
+ the token (they answer `401`); the banner's status poll keeps it. The SDK refuses a write in a trial before it
612
+ leaves the page (the browser would block it at the CORS preflight): it fails as the DMP's `403` would, a `DmpError`
613
+ with status 403 that blocks show as "no access", never faked; only a
614
+ wireframe (`soort: "wireframe"`) runs in the same mixed mode as live, with simulated writes.
615
+ - **The token is never stored and never logged.** It lives in a variable of the page for as long as the page. This
616
+ keeps it out of storage, logs and an innocent `innerHTML` dump; it is no defence against hostile code in the page,
617
+ which the DMP bounds instead (read-only, bound to the maker's IP and `Origin: null`, at most 60 minutes).
618
+ - **The banner.** `cube-sdk.js` puts `<cube-trial-banner>` first in `<body>` (shadow DOM, constructable stylesheet,
619
+ house-style tokens): *"Proefversie · alleen jij · alleen lezen · verloopt om 15:42 CEST · leest: orders, klanten ·
620
+ Plak geen gegevens uit deze proefversie in je AI-gesprek."*
621
+ - **Which clock.** The time shown is the DMP's expiry moment (`expiresAt` from the page's config), formatted in nl-NL
622
+ in the browser's time zone with that zone's short name (a browser in UTC shows "13:42 UTC"). A page in the DMP,
623
+ such as the app page, shows the time in the DMP's time zone; the zone name keeps the two from seeming to disagree.
624
+ The banner ends as expired without a request when the browser's clock passes `expiresAt`; a `401 trial_expired`
625
+ from the DMP ends it whatever the browser's clock says. The DMP's refusal is what ends the trial: a browser clock
626
+ that runs behind only shows the expired state later, and one that runs ahead shows "verlopen" early while the DMP
627
+ would still answer.
628
+ - **The poll.** It asks `GET statusUrl` (with the token, `cache: 'no-store'`, a 10 s timeout) every 5 s while the
629
+ page is visible and every 60 s while hidden, and right away when the page becomes visible again. When `expiresAt`
630
+ passes it shows the expired state without a request. A `429` with a `Retry-After` in whole seconds (digits only)
631
+ makes the next request wait that long, at least the normal interval and at most 60 s (`RETRY_AFTER_CAP_MS`), and
632
+ never past `expiresAt`; while that back-off holds, becoming visible again waits out its rest instead of asking at
633
+ once; it covers only the request it delays. Another `Retry-After` (absent, an HTTP date) keeps the normal rhythm.
634
+ The page can only read `Retry-After` when the DMP exposes it (`cube/apps` does by default for trial requests, via
635
+ `Access-Control-Expose-Headers`); without it the normal rhythm holds.
636
+
637
+ | Answer | Banner |
638
+ |---|---|
639
+ | `200` | Nothing changes, polling goes on |
640
+ | Network error, timeout, `5xx` | Polling goes on |
641
+ | `429` | Polling goes on, after a numeric `Retry-After` (capped at 60 s) when it is longer than the interval |
642
+ | `401 trial_replaced`, `wider: false` | *"Nieuwe versie wordt geladen…"*, then `location.assign(newer)` |
643
+ | `401 trial_replaced`, otherwise | *"Er is een nieuwe versie die meer gegevens leest."* with **Laden**: only the maker's click navigates |
644
+ | `401 trial_elsewhere` | *"Laad de pagina opnieuw."* with **Opnieuw laden** |
645
+ | `401 trial_expired` | *"Deze proefversie is verlopen. Vraag in je gesprek om een nieuwe."* |
646
+ | `401 trial_closed`, any other answer | *"Deze proefversie is gesloten."* |
647
+
648
+ `newer` is followed only when it has `dmpUrl`'s origin and a path under `/apps/proef/`; any other URL counts as
649
+ closed. Every state except `200` and the transient ones stops the polling.
650
+
581
651
  ### Mock data
582
652
 
583
653
  The shape is `cube.mock_data`'s output as is: per REST resource the `dmp.describe` payload and the
@@ -617,12 +687,12 @@ Without them the preview is permissive: every field, every function; only a fiel
617
687
  |---|---|
618
688
  | `CubeApp.connect(options?)` | `Promise<CubeAppSession>`. Options: `mock`, `manifest` (an object, or `false` to check nothing), `pageUrl` (default the page's URL), `fetch`, `tokenStorage` (`'session'`, the default, or `'memory'`), `autoSignIn` (default `false`) |
619
689
  | `CubeApp.mock(data)` | Registers preview data for the next `connect()`; `null` forgets it |
620
- | `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.1'` |
690
+ | `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.3'` |
621
691
  | `CubeApp.sdk` | The manifest `"sdk"` this build serves, `'1'` |
622
- | `session.mode` | `'preview'` or `'live'` |
692
+ | `session.mode` | `'preview'`, `'live'` or `'trial'` (a proefversie the DMP serves: the maker's own data, read-only, with a token written into the page) |
623
693
  | `session.user()` | `{ id, name, scopes }` or `null` |
624
- | `session.signIn()` | Live: navigates to the DMP's sign-in. Preview: signs the stand-in in |
625
- | `session.signOut()` | Forgets the tokens in this browser (not a revocation). Preview: signs the stand-in out |
694
+ | `session.signIn()` | Live: navigates to the DMP's sign-in. Preview: signs the stand-in in. Trial: reloads the page |
695
+ | `session.signOut()` | Forgets the tokens in this browser (not a revocation). Preview: signs the stand-in out. Trial: forgets the token, calls then answer 401 |
626
696
  | `session.resource(name)` | A `ResourceClient`: `list`, `pages`, `all`, `get`, `aggregate`, `create`, `update`, `delete`, `operation`, `collectionOperation` |
627
697
  | `session.signInError` | The error the last sign-in came back with (a refused consent: `code` `access_denied`), else `null`; `connect()` does not reject for it |
628
698
  | `session.schema(name)` | `Promise<ResourceSchema \| null>`: one resource as a block sees it (fields, actions, what may be filtered, sorted, grouped); `null` when unknown. In a wireframe it includes the missing fields |
@@ -645,8 +715,8 @@ app with a build (route A).
645
715
  | | |
646
716
  |---|---|
647
717
  | File | `dist/cube-sdk.js` in the package, `@cube-nl/dmp-sdk/cube-sdk.js` as an export; on jsDelivr/unpkg as `@cube-nl/dmp-sdk@<version>/dist/cube-sdk.js`; the CI's `pack` job also keeps it as an artifact |
648
- | Size | 126.9 KiB minified, 42.4 KiB gzipped (17.0 KiB core + 25.5 KiB blocks) in 1.1.0-rc.1; `dist/size.json` has the exact numbers |
649
- | Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.1.0-rc.1 cube-sdk.js … */`, and `CubeApp.version` |
718
+ | Size | 136.2 KiB minified, 45.9 KiB gzipped (17.7 KiB core + 28.1 KiB blocks and the trial banner) in 1.1.0-rc.3; `dist/size.json` has the exact numbers |
719
+ | Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.1.0-rc.3 cube-sdk.js … */`, and `CubeApp.version` |
650
720
 
651
721
  Pin it by copying the built file of a known version into the host, rather than fetching it at
652
722
  runtime: `npm pack @cube-nl/dmp-sdk@<version>` downloads the tarball, `package/dist/cube-sdk.js` is
@@ -778,19 +848,19 @@ blocks work from 320 px wide.
778
848
  |---|---|
779
849
  | `cube-app-shell` | `name` (required), `logo`, `logo-alt`. Header, user with Inloggen/Uitloggen, page navigation that follows `#/<name>` |
780
850
  | `cube-page` | `name` (id in the address), `label` (text in the navigation). A configuration element: it only holds blocks |
781
- | `cube-state` | `kind`: `loading`, `empty`, `error`, `forbidden`, `signed-out`; `message` replaces the standard text; `retry="false"` leaves out the retry button. Event `cube-retry` |
851
+ | `cube-state` | `kind`: `loading`, `empty`, `error`, `forbidden`, `signed-out`; `message` replaces the standard text; `retry="false"` leaves out the retry button. Event `cube-retry`. A block's error state shows the Dutch reason for a wrong attribute; any other failure shows the general text, and a network error or the SDK's own `TypeError` is also logged in the console |
782
852
  | `cube-period-picker` | `default` (`this-month`, `12m`, `this-year`, `custom`), `label`, `today`. Feeds other blocks; `value` is `{ key, from, to, previous, today }`, event `cube-change` |
783
853
  | `cube-kpi` | `resource`, `measure` (required), `label`, `format`, `currency`, `filter`, `period`, `date-field`, `compare`. One figure; `compare` adds the change against the previous period (`nieuw` when that was zero) |
784
854
  | `cube-chart` | `resource`, `measure`, `by` (required), `kind` (`line`/`bar`), `label`, `format`, `currency`, `filter`, `period`, `date-field`, `lookup`, `height`, `limit`. Own SVG, no chart library; a table for screen readers |
785
855
  | `cube-top` | `resource`, `measure`, `by` (required), `limit`, `order`, `label`, `format`, `currency`, `filter`, `period`, `date-field`, `lookup`. Sorted and cut in the browser (an aggregate cannot be sorted by the API). Over 1000 groups the DMP refuses the aggregate (`422 cardinality_exceeded`, reason `too_many_groups`); the block then asks for a shorter period or a filter, without a retry. Event `cube-select` |
786
856
  | `cube-field` | `name` (required), `label`, `format` (`text`, `number`, `integer`, `currency`, `percent`, `date`, `datetime`, `boolean`), `multiline`, `options` (`value=Label,...`), `hidden`. A configuration element, a direct child of `cube-table`, `cube-detail`, `cube-form` or `cube-board`; it is never rendered. Live `_capabilities` carries no `format` or `enum`, so a date column or a choice filter there comes from `format`/`options` here or from the values themselves, never from a guess at the type |
787
- | `cube-table` | `resource` (required), `fields`, `search` (one field, box "Zoeken"), `filters` (a control per field: choices, Alle/Ja/Nee, from-to dates or numbers, else contains), `sort` (`-` for descending), `page-size` (default 25, at most 200), `export`, `detail` (selector of a `cube-detail`), `filter`, `period`, `date-field`. Needs `list`. Search waits 300 ms and the last answer wins. Enter or Space chooses a row, which stays marked (`aria-current`). Event `cube-select` |
857
+ | `cube-table` | `resource` (required), `fields`, `search` (one field, box "Zoeken"), `filters` (a control per field: choices, Alle/Ja/Nee, from-to dates or numbers, else contains), `sort` (`-` for descending), `page-size` (default 25, at most 200), `export`, `detail` (selector of a `cube-detail`), `filter`, `period`, `date-field`. Needs `list`. Search waits 300 ms and the last answer wins. The rows are one Tab stop: arrow keys, Home and End move between rows; Enter or Space chooses one, which stays marked (`aria-current`). Event `cube-select` |
788
858
  | `cube-detail` | `resource` (required), `record-id`, `fields`, `editable`. Needs `get`, and `update` for `editable`. Events `cube-saved`, `cube-data-change` |
789
859
  | `cube-form` | `resource` (required), `fields`, `submit-label` (default "Opslaan"), `success-text` (default "Opgeslagen. Dank je wel!"). Needs `create`. Events `cube-created`, `cube-data-change` |
790
860
  | `cube-calc` | `label`. A calculator that needs no data: an editor per `cube-input`, an output per `cube-result`, recalculated on every keystroke by the `formula` property. Read-only `values` and `results`, event `cube-change` (`{ values, results }`) |
791
861
  | `cube-input` | `name`, `label` (required), `type` (`number`, `integer`, `currency`, `percent`, `text`, `select`, `checkbox`), `value`, `min`, `max`, `step`, `options` (a select needs it; `value=Label,...` as in `cube-field`). A configuration element, a direct child of `cube-calc`; it is never rendered. Numbers are typed the Dutch way (`7,5`) |
792
862
  | `cube-result` | `name`, `label` (required), `format` (`number`, `integer`, `currency`, `percent`, `text`), `currency` (default `EUR`), `emphasis` (the large figure). A configuration element, a direct child of `cube-calc`; it is never rendered |
793
- | `cube-board` | `resource`, `status-field`, `title-field` (required), `fields` (extra lines on a card), `sort`, `filter`, `page-size` (default 50, at most 200; a column with more says "en nog N"). `cube-field` children label and format the `fields` of a card. Cards in a column per status value. `status-field` must be filterable and `sort` a sortable field. Needs `list`; `update` is optional (without it the board is read-only). Event `cube-move` (`{ resource, id, from, to }`), followed by `cube-data-change` |
863
+ | `cube-board` | `resource`, `status-field`, `title-field` (required), `fields` (extra lines on a card), `sort`, `filter`, `page-size` (default 50, at most 200; a column with more says "en nog N"), `detail` (selector of a `cube-detail`). `cube-field` children label and format the `fields` of a card. Cards in a column per status value. `status-field` must be filterable and `sort` a sortable field. Needs `list`; `update` is optional (without it the board is read-only). Event `cube-move` (`{ resource, id, from, to }`), followed by `cube-data-change`; event `cube-select` (`{ resource, id, row }`) when a card is chosen |
794
864
  | `cube-column` | `value` (required), `label`. A configuration element, a direct child of `cube-board`, in order; it is never rendered |
795
865
 
796
866
  `cube-table`, `cube-detail` and `cube-form` need the matching actions in the app's `uses`
@@ -804,7 +874,9 @@ blocks work from 320 px wide.
804
874
  cancels.
805
875
  - **Filters**: `filter` and the followed period are always applied; the controls can only narrow them. A bound
806
876
  takes the stricter of the two (a "Vanaf" before the period start keeps the period start), and a field that
807
- `filter` fixes to one value gets no control. A `filters` or `search` field the resource cannot be filtered on
877
+ `filter` fixes to one value gets no control. A `search` box or a contains filter on a field that `filter` fixes or
878
+ gives an `ilike`, or a choice filter on a field that `filter` limits with an operator (such as `in`), changes
879
+ nothing (the base wins); the console warns once. A `filters` or `search` field the resource cannot be filtered on
808
880
  (not in the schema's `filterable`) shows an error naming that field instead of a table.
809
881
  - **Required fields**: a field the DMP requires shows " (verplicht)" after its label, besides `aria-required`.
810
882
  - **Writes**: every submit attempt carries an `Idempotency-Key` (`newIdempotencyKey()`), the button is disabled
@@ -847,7 +919,7 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
847
919
  #### Calculator and board
848
920
 
849
921
  - **Formula** (`cube-calc`): `formula` is a synchronous JavaScript function property, set from a script, never an
850
- attribute (an `async` one returns a Promise, which shows "—"). A string is refused and nothing is evaluated, so it
922
+ attribute (an `async` one is refused with an error). A string is refused and nothing is evaluated, so it
851
923
  works under the CSP. It gets `values` by `cube-input` name and returns an object by `cube-result` name (or one
852
924
  number or text when there is a single result):
853
925
 
@@ -859,8 +931,12 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
859
931
 
860
932
  A `percent` input is typed as `21` and arrives as the fraction `0.21`; an empty or invalid input arrives as
861
933
  `null` (also below `min`, above `max` or off `step`). A result that is `null` or `undefined` just shows "—". A
862
- formula that throws, or a result that is `NaN` or `Infinity`, shows "—" and one alert; the inputs keep working. Two
863
- or more `cube-result`s need an object back: a single number or text then shows an error instead of the calculator.
934
+ formula that throws, or a result that is `NaN`, `Infinity` or an object (for a number, integer, currency or percent
935
+ result), shows "—" and one alert that stays in place; the inputs keep working. A formula that throws logs
936
+ one `console.warn` per failure streak. Two or more `cube-result`s need an object back: a single number or text then
937
+ shows an error instead of the calculator on draw, but while typing it is a failed calculation ("—" and the alert),
938
+ as the formula may just have taken another branch. The `name` of each `cube-input`, and of each `cube-result`,
939
+ must be unique: a duplicate is refused.
864
940
  `cube-change` follows a recalculation by typing or by a new formula on a drawn calculator; not the first draw.
865
941
  - **Columns** (`cube-board`): the columns come from the `cube-column` children, in that order. Without them the
866
942
  board takes the `enum` of the status field, but live `_capabilities` carries no `enum`, so a starter must always
@@ -869,9 +945,17 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
869
945
  shows an error; a status value without a column is not shown.
870
946
  - **Moving**: drag and drop, or a compact button per other column on each card, so it works with the keyboard too.
871
947
  A button shows an arrow and the column name ("→ Bezig"); its accessible name is "Verplaats naar Bezig". The card
872
- moves at once, then is saved (`update` of only the status field); a refusal puts it back with an alert.
873
- - **Read-only**: without `update` in the app's `uses`, or when the status field is read-only, cards cannot move and
874
- show no buttons. Columns stack on a narrow screen (from 320 px).
948
+ moves at once, then is saved (`update` of only the status field); a refusal puts it back with an alert. The card
949
+ is placed in the new column by `sort` until the next load, which shows the server's order (on top without
950
+ `sort`). A status field of type integer or number
951
+ gets the column value as a number.
952
+ - **Read-only**: without `update` in the app's `uses`, when the status field is read-only, or when the resource has no
953
+ `id` field (the board then does not request `id`), cards cannot move and show no buttons.
954
+ - **Choosing**: a click on a card, or Enter or Space while the card itself has focus, chooses it: `cube-select` with
955
+ `{ resource, id, row }` (as `cube-table`), and `detail` sets the `record-id` of that `cube-detail`. A move (a
956
+ button or a drop) is never a choice. The chosen card stays marked (`aria-current`) across a move and a reload until
957
+ another `resource` is shown; a card without `id` emits `id: null`, is not marked and fills no detail.
958
+ Columns stack on a narrow screen (from 320 px).
875
959
 
876
960
  ### Wireframe
877
961
 
@@ -898,19 +982,23 @@ missing resource:
898
982
  `resource` is a REST resource name, `veld` the missing field (left out when the whole resource is missing), `type`
899
983
  one of `tekst`, `getal`, `bedrag`, `datum`, `keuze`, `ja/nee`, `waarden` the choices (required for `keuze`), `scherm`
900
984
  the screen that shows it and `reden` why it is needed. The SDK only reads this; `cube/apps` validates it. A
901
- malformed entry is dropped with one `console.warn`.
902
-
903
- - **Mixed mode.** What exists runs live. A missing field is added to the live rows with a value invented from the
904
- row's `id` (the same row shows the same value every time); a missing resource is answered from `cube-mock.json`.
905
- The live request never names a missing field: a filter or sort on it applies to the returned page only, and an
906
- aggregate that groups by or sums it is answered from the mock rows. The preview does the same; there, an aggregate
907
- on a missing field needs that field in the rows of `cube-mock.json`.
985
+ malformed entry, or an `ontbreekt` that is not a list, is dropped with one `console.warn`.
986
+
987
+ - **Mixed mode.** What exists runs live. A missing field is added to the live rows with a value invented from the row's
988
+ `id` (the same row shows the same value every time); a missing resource is answered from `cube-mock.json`. The live
989
+ request never names a missing field: a filter or sort on it applies to the returned page only (the page's count then
990
+ counts only the rows it shows), and an aggregate that groups by or sums it is answered from the mock rows. The preview
991
+ does the same; there, an aggregate on a missing field needs that field in the rows of `cube-mock.json`. A gap of type
992
+ `bedrag` shows as money also when the mock data has the field; other types follow the field's values.
908
993
  - **Marking.** Blocks mark what is invented: `Voorbeeld — bestaat nog niet`. A table or detail marks the column or
909
994
  field; the other blocks mark the whole block.
910
995
  - **Simulated writes.** `create`, `update`, `delete`, `operation` and `collectionOperation` are answered in the
911
996
  browser, in live and in preview, and forgotten on reload. A block that writes says
912
- `Voorbeeld — wordt niet opgeslagen`. A wireframe never writes real data: that covers everything that goes through
913
- the session (`session.resource()` and the blocks). App code that builds its own client with the exported
997
+ `Voorbeeld — wordt niet opgeslagen`. A sorted list places created rows, rows that join it and rows changed in a
998
+ sort field into the server's order when its rows carry the sort field; without a sort, created rows come first on
999
+ page 1. They also work signed out, and the preview does not check them against `uses`: a wireframe shows the idea,
1000
+ not the access rules. A wireframe never writes real data: that covers everything that
1001
+ goes through the session (`session.resource()` and the blocks). App code that builds its own client with the exported
914
1002
  `createClient` bypasses it.
915
1003
  - **Numbers.** KPI and chart numbers do not reflect simulated writes: they come from the rows the session holds
916
1004
  without them.
package/dist/app.d.ts CHANGED
@@ -60,8 +60,11 @@ export interface CubeAppUser {
60
60
  scopes: string[];
61
61
  }
62
62
  export interface CubeAppSession {
63
- /** `'preview'`: mock data in this browser, no DMP. `'live'`: the real DMP, as the signed-in person. */
64
- readonly mode: 'preview' | 'live';
63
+ /**
64
+ * `'preview'`: mock data in this browser, no DMP. `'live'`: the real DMP, as the signed-in person. `'trial'`: the
65
+ * maker's own data, read-only, with the token the DMP wrote into the page.
66
+ */
67
+ readonly mode: 'preview' | 'live' | 'trial';
65
68
  /** Who is signed in, or null. In the preview a stand-in user is signed in from the start. */
66
69
  user(): CubeAppUser | null;
67
70
  /** Live: navigates to the DMP's sign-in (the page is gone after this). Preview: signs the stand-in in again. */
@@ -112,8 +115,8 @@ export declare const CubeApp: Readonly<{
112
115
  ready: typeof ready;
113
116
  mock: typeof mock;
114
117
  theme: typeof theme;
115
- /** This SDK's version, e.g. `'1.1.0-rc.1'`. */
116
- version: "1.1.0-rc.1";
118
+ /** This SDK's version, e.g. `'1.1.0-rc.3'`. */
119
+ version: "1.1.0-rc.3";
117
120
  /** The manifest `"sdk"` value this build serves. */
118
121
  sdk: "1";
119
122
  }>;
package/dist/app.js CHANGED
@@ -5,6 +5,7 @@ import { PreviewStore } from './preview.js';
5
5
  import { schemaFromCapability } from './ui/schema.js';
6
6
  import { PREVIEW_USER_NAME } from './ui/texts.js';
7
7
  import { resolveTheme, setTheme } from './ui/theme.js';
8
+ import { trialConfig } from './trial.js';
8
9
  import { MANIFEST_SDK, VERSION } from './version.js';
9
10
  import { mixedResource, readGaps, simulatedWrites, withGaps } from './wireframe.js';
10
11
  const CONFIG_FILE = './cube-app.config.json';
@@ -91,13 +92,18 @@ function announceSessionChange() {
91
92
  async function start(options, isCurrent) {
92
93
  const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
93
94
  const pageUrl = options.pageUrl ?? currentPage();
95
+ // A trial page never has cube-app.config.json and never signs in with OAuth: the DMP wrote the token into it.
96
+ const trial = trialConfig();
94
97
  const [config, theme] = await Promise.all([
95
- readJson(doFetch, resolve(CONFIG_FILE, pageUrl)),
98
+ trial === null ? readJson(doFetch, resolve(CONFIG_FILE, pageUrl)) : null,
96
99
  loadTheme(options, doFetch, pageUrl),
97
100
  ]);
98
101
  if (isCurrent()) {
99
102
  setTheme(theme);
100
103
  }
104
+ if (trial !== null) {
105
+ return trialSession(trial, options, doFetch, pageUrl);
106
+ }
101
107
  if (config === null) {
102
108
  return preview(options, doFetch, pageUrl);
103
109
  }
@@ -198,6 +204,47 @@ async function preview(options, doFetch, pageUrl) {
198
204
  schema: async (name) => withGaps(store(name).describe(name), marked.gaps),
199
205
  };
200
206
  }
207
+ /** A wireframe's marks and its mock data. Only a wireframe reads the mock: an ordinary app never fetches it. */
208
+ async function wireframeParts(soort, options, doFetch, pageUrl) {
209
+ const [mock, manifest] = soort === 'wireframe' ? await appFiles(options, doFetch, pageUrl) : [{}, null];
210
+ // The manifest's ceiling is the live client's: a missing resource is not in it, so the store checks none.
211
+ return { marked: marks(soort, manifest), examples: new PreviewStore(mock) };
212
+ }
213
+ /** The session on the DMP, live or trial: a wireframe mixes in its gaps, everything else goes to the client. */
214
+ function onDmp(connection, { marked, examples }) {
215
+ const { client } = connection;
216
+ let capabilities = null;
217
+ return {
218
+ ...marked,
219
+ mode: connection.mode,
220
+ get signInError() {
221
+ return connection.signInError();
222
+ },
223
+ user: connection.user,
224
+ signIn: connection.signIn,
225
+ signOut: connection.signOut,
226
+ resource: writes(marked.simulatesWrites, (name) => marked.isExample(name)
227
+ ? examples.resource(name)
228
+ : marked.gaps.some((gap) => gap.resource === name)
229
+ ? mixedResource(client.resources(name), marked.gaps, examples, examples.rows(name))
230
+ : client.resources(name)),
231
+ async schema(name) {
232
+ if (marked.isExample(name)) {
233
+ return withGaps(examples.describe(name), marked.gaps);
234
+ }
235
+ capabilities ??= client.capabilities();
236
+ try {
237
+ const found = (await capabilities).resources.find((resource) => resource.name === name);
238
+ return found === undefined ? null : withGaps(schemaFromCapability(found), marked.gaps);
239
+ }
240
+ catch (error) {
241
+ // A failed call (signed out, offline) must not stay cached for the whole session.
242
+ capabilities = null;
243
+ throw error;
244
+ }
245
+ },
246
+ };
247
+ }
201
248
  async function live(config, options, doFetch, pageUrl) {
202
249
  const dmpUrl = config.dmpUrl.replace(/\/+$/, '');
203
250
  let tokens = null;
@@ -215,12 +262,7 @@ async function live(config, options, doFetch, pageUrl) {
215
262
  });
216
263
  const client = createClient({ baseUrl: config.apiUrl ?? `${dmpUrl}/apps/api/v1`, auth: oauth, fetch: doFetch });
217
264
  let signInError = null;
218
- let capabilities = null;
219
- // Only a wireframe reads the mock data: an ordinary live app never fetches cube-mock.json, even when it ships one.
220
- const [mock, manifest] = config.soort === 'wireframe' ? await appFiles(options, doFetch, pageUrl) : [{}, null];
221
- const marked = marks(config.soort, manifest);
222
- // The manifest's ceiling is the live client's: a missing resource is not in it, so the store checks none.
223
- const examples = new PreviewStore(mock);
265
+ const parts = await wireframeParts(config.soort, options, doFetch, pageUrl);
224
266
  tokens = await store.get();
225
267
  try {
226
268
  await oauth.handleCallback();
@@ -232,12 +274,10 @@ async function live(config, options, doFetch, pageUrl) {
232
274
  if (tokens === null && signInError === null && options.autoSignIn === true) {
233
275
  await oauth.signIn();
234
276
  }
235
- return {
236
- ...marked,
277
+ return onDmp({
237
278
  mode: 'live',
238
- get signInError() {
239
- return signInError;
240
- },
279
+ client,
280
+ signInError: () => signInError,
241
281
  user: () => (tokens === null ? null : userFrom(tokens)),
242
282
  async signIn() {
243
283
  signInError = null;
@@ -246,28 +286,44 @@ async function live(config, options, doFetch, pageUrl) {
246
286
  async signOut() {
247
287
  await oauth.signOut();
248
288
  },
249
- resource: writes(marked.simulatesWrites, (name) => marked.isExample(name)
250
- ? examples.resource(name)
251
- : marked.gaps.some((gap) => gap.resource === name)
252
- ? mixedResource(client.resources(name), marked.gaps, examples, examples.rows(name))
253
- : client.resources(name)),
254
- async schema(name) {
255
- if (marked.isExample(name)) {
256
- return withGaps(examples.describe(name), marked.gaps);
257
- }
258
- capabilities ??= client.capabilities();
259
- try {
260
- const found = (await capabilities).resources.find((resource) => resource.name === name);
261
- return found === undefined ? null : withGaps(schemaFromCapability(found), marked.gaps);
262
- }
263
- catch (error) {
264
- // A failed call (signed out, offline) must not stay cached for the whole session.
265
- capabilities = null;
266
- throw error;
267
- }
268
- },
289
+ }, parts);
290
+ }
291
+ /**
292
+ * A trial reads only. A write never leaves the page (the browser would block it at the CORS preflight and the app would
293
+ * see a TypeError): it answers the 403 the DMP would, so it surfaces as a DmpError like any refusal.
294
+ */
295
+ function readOnly(doFetch) {
296
+ return async (input, init) => {
297
+ const method = (init?.method ?? 'GET').toUpperCase();
298
+ if (method === 'GET' || method === 'HEAD') {
299
+ return doFetch(input, init);
300
+ }
301
+ return new Response(JSON.stringify({ status: 403, code: 'insufficient_scope' }), {
302
+ status: 403,
303
+ headers: { 'Content-Type': 'application/problem+json' },
304
+ });
269
305
  };
270
306
  }
307
+ /** The live session with the injected token instead of OAuth: no discovery, no callback, no storage. */
308
+ async function trialSession(config, options, doFetch, pageUrl) {
309
+ let token = config.token;
310
+ // Read on every request: once signed out no token goes out, so the DMP answers 401.
311
+ const client = createClient({ baseUrl: config.apiUrl, auth: () => token, fetch: readOnly(doFetch) });
312
+ const parts = await wireframeParts(config.soort ?? undefined, options, doFetch, pageUrl);
313
+ return onDmp({
314
+ mode: 'trial',
315
+ client,
316
+ signInError: () => null,
317
+ user: () => (token === null ? null : { id: config.userId, name: null, scopes: ['api:read'] }),
318
+ // The DMP writes a token into the page on every load: a reload is the sign-in.
319
+ async signIn() {
320
+ globalThis.location?.reload();
321
+ },
322
+ async signOut() {
323
+ token = null;
324
+ },
325
+ }, parts);
326
+ }
271
327
  /**
272
328
  * Who the token belongs to, read from the access token itself (cube/oauth-server issues JWTs).
273
329
  * For display only: the DMP checks the token on every request, this SDK does not verify it.
@@ -420,7 +476,7 @@ export const CubeApp = Object.freeze({
420
476
  ready,
421
477
  mock,
422
478
  theme,
423
- /** This SDK's version, e.g. `'1.1.0-rc.1'`. */
479
+ /** This SDK's version, e.g. `'1.1.0-rc.3'`. */
424
480
  version: VERSION,
425
481
  /** The manifest `"sdk"` value this build serves. */
426
482
  sdk: MANIFEST_SDK,
package/dist/browser.js CHANGED
@@ -1,8 +1,16 @@
1
1
  import { CubeApp as Core } from './app.js';
2
+ import { trialConfig } from './trial.js';
2
3
  import { defineBlocks } from './ui/define.js';
4
+ import { showTrialBanner } from './ui/trial-banner.js';
3
5
  // The entry of dist/cube-sdk.js: one classic script, no imports, `window.CubeApp` for a page without a build step.
4
6
  const CubeApp = Object.freeze({ ...Core, defineBlocks });
5
7
  globalThis.CubeApp = CubeApp;
6
8
  // An engine without custom elements (an old browser, a script run outside a page) keeps the plain CubeApp.
7
9
  if (typeof customElements !== 'undefined')
8
10
  defineBlocks();
11
+ // A trial page (the config was read when trial.ts evaluated): say so at the top, and follow the trial's status.
12
+ const trial = trialConfig();
13
+ const page = globalThis;
14
+ if (trial !== null && page.document !== undefined && page.CSSStyleSheet !== undefined) {
15
+ showTrialBanner(page, trial);
16
+ }
package/dist/client.js CHANGED
@@ -99,7 +99,11 @@ export function createClient(options) {
99
99
  body: input ?? {},
100
100
  headers: writeHeaders(opts),
101
101
  });
102
- return { ...response, data: response.status === 204 ? null : response.data, accepted: response.status === 202 };
102
+ return {
103
+ ...response,
104
+ data: response.status === 204 ? null : response.data,
105
+ accepted: response.status === 202,
106
+ };
103
107
  };
104
108
  return {
105
109
  name,