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

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.2/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.2/dist/cube-sdk.js"></script>
56
56
  ```
57
57
 
58
58
  Fallback, for an unreleased commit: `npm install
@@ -578,6 +578,63 @@ 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 · leest: orders, klanten · Plak
620
+ geen gegevens uit deze proefversie in je AI-gesprek."* (the time in nl-NL local time).
621
+ - **The poll.** It asks `GET statusUrl` (with the token, `cache: 'no-store'`, a 10 s timeout) every 5 s while the
622
+ page is visible and every 60 s while hidden, and right away when the page becomes visible again. When `expiresAt`
623
+ passes it shows the expired state without a request.
624
+
625
+ | Answer | Banner |
626
+ |---|---|
627
+ | `200` | Nothing changes, polling goes on |
628
+ | Network error, timeout, `429`, `5xx` | Polling goes on |
629
+ | `401 trial_replaced`, `wider: false` | *"Nieuwe versie wordt geladen…"*, then `location.assign(newer)` |
630
+ | `401 trial_replaced`, otherwise | *"Er is een nieuwe versie die meer gegevens leest."* with **Laden**: only the maker's click navigates |
631
+ | `401 trial_elsewhere` | *"Laad de pagina opnieuw."* with **Opnieuw laden** |
632
+ | `401 trial_expired` | *"Deze proefversie is verlopen. Vraag in je gesprek om een nieuwe."* |
633
+ | `401 trial_closed`, any other answer | *"Deze proefversie is gesloten."* |
634
+
635
+ `newer` is followed only when it has `dmpUrl`'s origin and a path under `/apps/proef/`; any other URL counts as
636
+ closed. Every state except `200` and the transient ones stops the polling.
637
+
581
638
  ### Mock data
582
639
 
583
640
  The shape is `cube.mock_data`'s output as is: per REST resource the `dmp.describe` payload and the
@@ -617,12 +674,12 @@ Without them the preview is permissive: every field, every function; only a fiel
617
674
  |---|---|
618
675
  | `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
676
  | `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'` |
677
+ | `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.2'` |
621
678
  | `CubeApp.sdk` | The manifest `"sdk"` this build serves, `'1'` |
622
- | `session.mode` | `'preview'` or `'live'` |
679
+ | `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
680
  | `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 |
681
+ | `session.signIn()` | Live: navigates to the DMP's sign-in. Preview: signs the stand-in in. Trial: reloads the page |
682
+ | `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
683
  | `session.resource(name)` | A `ResourceClient`: `list`, `pages`, `all`, `get`, `aggregate`, `create`, `update`, `delete`, `operation`, `collectionOperation` |
627
684
  | `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
685
  | `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 +702,8 @@ app with a build (route A).
645
702
  | | |
646
703
  |---|---|
647
704
  | 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` |
705
+ | Size | 134.7 KiB minified, 45.4 KiB gzipped (17.7 KiB core + 27.6 KiB blocks and the trial banner) in 1.1.0-rc.2; `dist/size.json` has the exact numbers |
706
+ | Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.1.0-rc.2 cube-sdk.js … */`, and `CubeApp.version` |
650
707
 
651
708
  Pin it by copying the built file of a known version into the host, rather than fetching it at
652
709
  runtime: `npm pack @cube-nl/dmp-sdk@<version>` downloads the tarball, `package/dist/cube-sdk.js` is
@@ -778,13 +835,13 @@ blocks work from 320 px wide.
778
835
  |---|---|
779
836
  | `cube-app-shell` | `name` (required), `logo`, `logo-alt`. Header, user with Inloggen/Uitloggen, page navigation that follows `#/<name>` |
780
837
  | `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` |
838
+ | `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
839
  | `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
840
  | `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
841
  | `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
842
  | `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
843
  | `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` |
844
+ | `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
845
  | `cube-detail` | `resource` (required), `record-id`, `fields`, `editable`. Needs `get`, and `update` for `editable`. Events `cube-saved`, `cube-data-change` |
789
846
  | `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
847
  | `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 }`) |
@@ -804,7 +861,9 @@ blocks work from 320 px wide.
804
861
  cancels.
805
862
  - **Filters**: `filter` and the followed period are always applied; the controls can only narrow them. A bound
806
863
  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
864
+ `filter` fixes to one value gets no control. A `search` box or a contains filter on a field that `filter` fixes or
865
+ gives an `ilike`, or a choice filter on a field that `filter` limits with an operator (such as `in`), changes
866
+ nothing (the base wins); the console warns once. A `filters` or `search` field the resource cannot be filtered on
808
867
  (not in the schema's `filterable`) shows an error naming that field instead of a table.
809
868
  - **Required fields**: a field the DMP requires shows " (verplicht)" after its label, besides `aria-required`.
810
869
  - **Writes**: every submit attempt carries an `Idempotency-Key` (`newIdempotencyKey()`), the button is disabled
@@ -847,7 +906,7 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
847
906
  #### Calculator and board
848
907
 
849
908
  - **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
909
+ attribute (an `async` one is refused with an error). A string is refused and nothing is evaluated, so it
851
910
  works under the CSP. It gets `values` by `cube-input` name and returns an object by `cube-result` name (or one
852
911
  number or text when there is a single result):
853
912
 
@@ -859,8 +918,12 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
859
918
 
860
919
  A `percent` input is typed as `21` and arrives as the fraction `0.21`; an empty or invalid input arrives as
861
920
  `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.
921
+ formula that throws, or a result that is `NaN`, `Infinity` or an object (for a number, integer, currency or percent
922
+ result), shows "—" and one alert that stays in place; the inputs keep working. A formula that throws logs
923
+ one `console.warn` per failure streak. Two or more `cube-result`s need an object back: a single number or text then
924
+ shows an error instead of the calculator on draw, but while typing it is a failed calculation ("—" and the alert),
925
+ as the formula may just have taken another branch. The `name` of each `cube-input`, and of each `cube-result`,
926
+ must be unique: a duplicate is refused.
864
927
  `cube-change` follows a recalculation by typing or by a new formula on a drawn calculator; not the first draw.
865
928
  - **Columns** (`cube-board`): the columns come from the `cube-column` children, in that order. Without them the
866
929
  board takes the `enum` of the status field, but live `_capabilities` carries no `enum`, so a starter must always
@@ -869,9 +932,13 @@ say so ("Te veel groepen…") and ask for a shorter period or a filter, without
869
932
  shows an error; a status value without a column is not shown.
870
933
  - **Moving**: drag and drop, or a compact button per other column on each card, so it works with the keyboard too.
871
934
  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).
935
+ moves at once, then is saved (`update` of only the status field); a refusal puts it back with an alert. The card
936
+ is placed in the new column by `sort` until the next load, which shows the server's order (on top without
937
+ `sort`). A status field of type integer or number
938
+ gets the column value as a number.
939
+ - **Read-only**: without `update` in the app's `uses`, when the status field is read-only, or when the resource has no
940
+ `id` field (the board then does not request `id`), cards cannot move and show no buttons.
941
+ Columns stack on a narrow screen (from 320 px).
875
942
 
876
943
  ### Wireframe
877
944
 
@@ -898,19 +965,23 @@ missing resource:
898
965
  `resource` is a REST resource name, `veld` the missing field (left out when the whole resource is missing), `type`
899
966
  one of `tekst`, `getal`, `bedrag`, `datum`, `keuze`, `ja/nee`, `waarden` the choices (required for `keuze`), `scherm`
900
967
  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`.
968
+ malformed entry, or an `ontbreekt` that is not a list, is dropped with one `console.warn`.
969
+
970
+ - **Mixed mode.** What exists runs live. A missing field is added to the live rows with a value invented from the row's
971
+ `id` (the same row shows the same value every time); a missing resource is answered from `cube-mock.json`. The live
972
+ request never names a missing field: a filter or sort on it applies to the returned page only (the page's count then
973
+ counts only the rows it shows), and an aggregate that groups by or sums it is answered from the mock rows. The preview
974
+ does the same; there, an aggregate on a missing field needs that field in the rows of `cube-mock.json`. A gap of type
975
+ `bedrag` shows as money also when the mock data has the field; other types follow the field's values.
908
976
  - **Marking.** Blocks mark what is invented: `Voorbeeld — bestaat nog niet`. A table or detail marks the column or
909
977
  field; the other blocks mark the whole block.
910
978
  - **Simulated writes.** `create`, `update`, `delete`, `operation` and `collectionOperation` are answered in the
911
979
  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
980
+ `Voorbeeld — wordt niet opgeslagen`. A sorted list places created rows, rows that join it and rows changed in a
981
+ sort field into the server's order when its rows carry the sort field; without a sort, created rows come first on
982
+ page 1. They also work signed out, and the preview does not check them against `uses`: a wireframe shows the idea,
983
+ not the access rules. A wireframe never writes real data: that covers everything that
984
+ goes through the session (`session.resource()` and the blocks). App code that builds its own client with the exported
914
985
  `createClient` bypasses it.
915
986
  - **Numbers.** KPI and chart numbers do not reflect simulated writes: they come from the rows the session holds
916
987
  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.2'`. */
119
+ version: "1.1.0-rc.2";
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.2'`. */
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,