@proveanything/smartlinks 2.0.53 → 2.0.55
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/dist/api/appConfiguration.d.ts +10 -15
- package/dist/api/appConfiguration.js +10 -15
- package/dist/api/index.d.ts +0 -1
- package/dist/api/index.js +0 -1
- package/dist/docs/API_SUMMARY.md +19 -28
- package/dist/docs/containers.md +7 -0
- package/dist/docs/overview.md +0 -1
- package/dist/docs/widgets.md +51 -0
- package/dist/openapi.yaml +26 -25
- package/dist/types/widgets.d.ts +28 -0
- package/docs/API_SUMMARY.md +19 -28
- package/docs/containers.md +7 -0
- package/docs/overview.md +0 -1
- package/docs/widgets.md +51 -0
- package/openapi.yaml +26 -25
- package/package.json +1 -1
- package/scripts/doctor.mjs +44 -0
- package/dist/api/sequence.d.ts +0 -38
- package/dist/api/sequence.js +0 -29
- package/dist/docs/sequences.md +0 -125
- package/docs/sequences.md +0 -125
|
@@ -379,21 +379,16 @@ export declare namespace appConfiguration {
|
|
|
379
379
|
* ```typescript
|
|
380
380
|
* // Fetch all widget data for a collection
|
|
381
381
|
* const { apps } = await Api.AppConfiguration.getWidgets(collectionId);
|
|
382
|
-
* // Returns: [{ appId,
|
|
383
|
-
*
|
|
384
|
-
* //
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* const styleTag = document.createElement('style');
|
|
393
|
-
* styleTag.textContent = app.bundleCss;
|
|
394
|
-
* document.head.appendChild(styleTag);
|
|
395
|
-
* }
|
|
396
|
-
* }
|
|
382
|
+
* // Returns: [{ appId, manifest, widget, container, admin }, ...]
|
|
383
|
+
* // widget / container: { js: { umd?, esm? }, css?, source?, styles? }
|
|
384
|
+
* // source / styles - the bundle inlined (when small enough); js / css - URLs otherwise.
|
|
385
|
+
* // admin - URL of the app's app.admin.json (setup / tunable options), or null.
|
|
386
|
+
* // Bundles don't ship React: they resolve shared libraries from the HOST's globals
|
|
387
|
+
* // (host-dependency-contract.md), so loading one is a host's job.
|
|
388
|
+
* //
|
|
389
|
+
* // Inside a host (Portal / Hub), an app showing ANOTHER app's widget should not load
|
|
390
|
+
* // bundles itself - use the host-provided EmbeddedWidget prop:
|
|
391
|
+
* // <EmbeddedWidget appId="raffle" size="standard" /> (docs/widgets.md)
|
|
397
392
|
*
|
|
398
393
|
* // Force refresh all widgets
|
|
399
394
|
* const { apps } = await Api.AppConfiguration.getWidgets(collectionId, { force: true });
|
|
@@ -482,21 +482,16 @@ export var appConfiguration;
|
|
|
482
482
|
* ```typescript
|
|
483
483
|
* // Fetch all widget data for a collection
|
|
484
484
|
* const { apps } = await Api.AppConfiguration.getWidgets(collectionId);
|
|
485
|
-
* // Returns: [{ appId,
|
|
486
|
-
*
|
|
487
|
-
* //
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
* const styleTag = document.createElement('style');
|
|
496
|
-
* styleTag.textContent = app.bundleCss;
|
|
497
|
-
* document.head.appendChild(styleTag);
|
|
498
|
-
* }
|
|
499
|
-
* }
|
|
485
|
+
* // Returns: [{ appId, manifest, widget, container, admin }, ...]
|
|
486
|
+
* // widget / container: { js: { umd?, esm? }, css?, source?, styles? }
|
|
487
|
+
* // source / styles - the bundle inlined (when small enough); js / css - URLs otherwise.
|
|
488
|
+
* // admin - URL of the app's app.admin.json (setup / tunable options), or null.
|
|
489
|
+
* // Bundles don't ship React: they resolve shared libraries from the HOST's globals
|
|
490
|
+
* // (host-dependency-contract.md), so loading one is a host's job.
|
|
491
|
+
* //
|
|
492
|
+
* // Inside a host (Portal / Hub), an app showing ANOTHER app's widget should not load
|
|
493
|
+
* // bundles itself - use the host-provided EmbeddedWidget prop:
|
|
494
|
+
* // <EmbeddedWidget appId="raffle" size="standard" /> (docs/widgets.md)
|
|
500
495
|
*
|
|
501
496
|
* // Force refresh all widgets
|
|
502
497
|
* const { apps } = await Api.AppConfiguration.getWidgets(collectionId, { force: true });
|
package/dist/api/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export { auth } from "./auth.js";
|
|
|
10
10
|
export { form } from "./form.js";
|
|
11
11
|
export { authKit } from "./authKit.js";
|
|
12
12
|
export { claimSet } from "./claimSet.js";
|
|
13
|
-
export { sequence } from "./sequence.js";
|
|
14
13
|
export { crate } from "./crate.js";
|
|
15
14
|
export { batch } from "./batch.js";
|
|
16
15
|
export { variant } from "./variant.js";
|
package/dist/api/index.js
CHANGED
|
@@ -12,7 +12,6 @@ export { auth } from "./auth.js";
|
|
|
12
12
|
export { form } from "./form.js";
|
|
13
13
|
export { authKit } from "./authKit.js";
|
|
14
14
|
export { claimSet } from "./claimSet.js";
|
|
15
|
-
export { sequence } from "./sequence.js";
|
|
16
15
|
export { crate } from "./crate.js";
|
|
17
16
|
export { batch } from "./batch.js";
|
|
18
17
|
export { variant } from "./variant.js";
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.55 | Generated: 2026-10-12T20:04:55.294Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -154,7 +154,6 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
154
154
|
- **realtime** - Functions for realtime operations
|
|
155
155
|
- **research** - Functions for research operations
|
|
156
156
|
- **secrets** - Functions for secrets operations
|
|
157
|
-
- **sequence** - Functions for sequence operations
|
|
158
157
|
- **tags** - Functions for tags operations
|
|
159
158
|
- **template** - Functions for template operations
|
|
160
159
|
- **translations** - Functions for translations operations
|
|
@@ -9744,9 +9743,26 @@ interface SmartLinksWidgetProps {
|
|
|
9744
9743
|
size?: 'compact' | 'standard' | 'large'
|
|
9745
9744
|
lang?: string
|
|
9746
9745
|
translations?: Record<string, string>
|
|
9746
|
+
EmbeddedWidget?: EmbeddedWidgetComponent
|
|
9747
9747
|
}
|
|
9748
9748
|
```
|
|
9749
9749
|
|
|
9750
|
+
**EmbeddedWidgetProps** (interface)
|
|
9751
|
+
```typescript
|
|
9752
|
+
interface EmbeddedWidgetProps {
|
|
9753
|
+
appId: string
|
|
9754
|
+
component?: string
|
|
9755
|
+
size?: 'compact' | 'standard' | 'large'
|
|
9756
|
+
widgetId?: string
|
|
9757
|
+
props?: Record<string, unknown>
|
|
9758
|
+
className?: string
|
|
9759
|
+
fallback?: unknown
|
|
9760
|
+
loading?: unknown
|
|
9761
|
+
}
|
|
9762
|
+
```
|
|
9763
|
+
|
|
9764
|
+
**EmbeddedWidgetComponent** = `(props: EmbeddedWidgetProps) => any`
|
|
9765
|
+
|
|
9750
9766
|
### appConfiguration (api)
|
|
9751
9767
|
|
|
9752
9768
|
**AppConfigOptions** (type)
|
|
@@ -9852,26 +9868,6 @@ interface AppSecretOptions {
|
|
|
9852
9868
|
}
|
|
9853
9869
|
```
|
|
9854
9870
|
|
|
9855
|
-
### sequence (api)
|
|
9856
|
-
|
|
9857
|
-
**AllocateSequenceInput** (interface)
|
|
9858
|
-
```typescript
|
|
9859
|
-
interface AllocateSequenceInput {
|
|
9860
|
-
appId: string
|
|
9861
|
-
sequenceId: string
|
|
9862
|
-
subjectId: string
|
|
9863
|
-
productId?: string
|
|
9864
|
-
}
|
|
9865
|
-
```
|
|
9866
|
-
|
|
9867
|
-
**AllocatedSequence** (interface)
|
|
9868
|
-
```typescript
|
|
9869
|
-
interface AllocatedSequence {
|
|
9870
|
-
number: number
|
|
9871
|
-
isNew: boolean
|
|
9872
|
-
}
|
|
9873
|
-
```
|
|
9874
|
-
|
|
9875
9871
|
### conditions (utils)
|
|
9876
9872
|
|
|
9877
9873
|
**BaseCondition** (interface)
|
|
@@ -10380,7 +10376,7 @@ Delete a keyed data item by ID within a scope. Requires admin authentication. ``
|
|
|
10380
10376
|
|
|
10381
10377
|
**getWidgets**(collectionId: string,
|
|
10382
10378
|
options?: GetCollectionWidgetsOptions) → `Promise<CollectionWidgetsResponse>`
|
|
10383
|
-
Fetches ALL widget data (manifests + bundle files) for a collection in one call. Returns everything needed to render widgets with zero additional requests. This solves N+1 query problems by fetching manifests, JavaScript bundles, and CSS files in parallel on the server. ```typescript // Fetch all widget data for a collection const { apps } = await Api.AppConfiguration.getWidgets(collectionId); // Returns: [{ appId,
|
|
10379
|
+
Fetches ALL widget data (manifests + bundle files) for a collection in one call. Returns everything needed to render widgets with zero additional requests. This solves N+1 query problems by fetching manifests, JavaScript bundles, and CSS files in parallel on the server. ```typescript // Fetch all widget data for a collection const { apps } = await Api.AppConfiguration.getWidgets(collectionId); // Returns: [{ appId, manifest, widget, container, admin }, ...] // widget / container: { js: { umd?, esm? }, css?, source?, styles? } // source / styles - the bundle inlined (when small enough); js / css - URLs otherwise. // admin - URL of the app's app.admin.json (setup / tunable options), or null. // Bundles don't ship React: they resolve shared libraries from the HOST's globals // (host-dependency-contract.md), so loading one is a host's job. // // Inside a host (Portal / Hub), an app showing ANOTHER app's widget should not load // bundles itself - use the host-provided EmbeddedWidget prop: // <EmbeddedWidget appId="raffle" size="standard" /> (docs/widgets.md) // Force refresh all widgets const { apps } = await Api.AppConfiguration.getWidgets(collectionId, { force: true }); ```
|
|
10384
10380
|
|
|
10385
10381
|
**resolveFeature**(system: SystemBlock | undefined, flag: string) → `boolean`
|
|
10386
10382
|
Resolve whether a feature flag is on, applying the `accountType` default: `enterprise` accounts default every flag to **on** unless `features[flag]` is explicitly `false`; `standard` accounts (or missing `accountType`) default every flag to **off** unless `features[flag]` is explicitly `true`. An explicit value always wins over the default either way. This is the same logic `isFeatureEnabled()` / `isFeatureEnabledSync()` use internally — call those instead in normal code. `system.features` and `accountType` are public data (no admin-only feature-check path exists), so there's no reason to fetch differently for an admin surface. This is exposed separately only for the rare case where you already have a `system` block in hand from somewhere other than `getAppConfig()` (e.g. server-rendered props) and want to resolve a flag from it directly. ```typescript const enabled = appConfiguration.resolveFeature(someSystemBlock, 'custom_domain'); ```
|
|
@@ -12234,11 +12230,6 @@ Soft-delete a secret. DELETE /secrets/:ref
|
|
|
12234
12230
|
id: string,
|
|
12235
12231
|
query: { limit?: number; offset?: number } = {}) → `Promise<SegmentRecipientsResponse>`
|
|
12236
12232
|
|
|
12237
|
-
### sequence
|
|
12238
|
-
|
|
12239
|
-
**allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
|
|
12240
|
-
Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, }) `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
|
|
12241
|
-
|
|
12242
12233
|
### sessions
|
|
12243
12234
|
|
|
12244
12235
|
**stats**(collectionId: string) → `Promise<SessionStatistics>`
|
package/dist/docs/containers.md
CHANGED
|
@@ -226,6 +226,13 @@ All standard widget props apply:
|
|
|
226
226
|
|
|
227
227
|
---
|
|
228
228
|
|
|
229
|
+
### Showing another app's widget
|
|
230
|
+
|
|
231
|
+
A container can show another installed app's widget inline with the host-provided `EmbeddedWidget` prop:
|
|
232
|
+
`<EmbeddedWidget appId="raffle" size="standard" />`. The host loads it, isolates it and contains any crash.
|
|
233
|
+
See [widgets.md → Using another app's widget](widgets.md#using-another-apps-widget). Don't hand-roll a
|
|
234
|
+
widget loader.
|
|
235
|
+
|
|
229
236
|
## Cross-App Navigation
|
|
230
237
|
|
|
231
238
|
Containers support the same **structured navigation requests** as widgets. When a container needs to navigate to another app within the portal, it emits a `NavigationRequest` via the `onNavigate` callback. The portal orchestrator interprets the request, preserves hierarchy context, and performs the navigation.
|
package/dist/docs/overview.md
CHANGED
|
@@ -93,7 +93,6 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
93
93
|
| **Container Tracking** | `docs/container-tracking.md` | Physical/logical **item** container groupings — *not* app containers (see containers.md) |
|
|
94
94
|
| **Lots** | `docs/lots.md` | Cross-SKU production "Lot" entity |
|
|
95
95
|
| **Loyalty** | `docs/loyalty.md` | Points, members, earning rules |
|
|
96
|
-
| **Sequences** | `docs/sequences.md` | RETIRING — number things in your own server function: `ctx.sl.counters` + `appRecords.claim` (server-functions.md) |
|
|
97
96
|
| **Integrations** | `docs/integrations.md` | Inbound/outbound external-system integration flows |
|
|
98
97
|
| **Item Context** | `docs/item-context.md` | The `itemContext` container prop (serial / NFC authenticity context) |
|
|
99
98
|
| **Native Facade** | `docs/native-facade.md` | `host.native` / `SL.native` device-capability facade |
|
package/dist/docs/widgets.md
CHANGED
|
@@ -146,6 +146,7 @@ interface SmartLinksWidgetProps {
|
|
|
146
146
|
| `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
|
|
147
147
|
| `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
|
|
148
148
|
| `translations` | `object?` | Translation overrides |
|
|
149
|
+
| `EmbeddedWidget` | `component?` | Host-provided: show another app's widget inside yours — see [Using another app's widget](#using-another-apps-widget) |
|
|
149
150
|
|
|
150
151
|
---
|
|
151
152
|
|
|
@@ -557,6 +558,56 @@ If `VITE_ENABLE_WIDGETS` is not set to `true`, the build produces a minimal stub
|
|
|
557
558
|
|
|
558
559
|
---
|
|
559
560
|
|
|
561
|
+
## Using another app's widget
|
|
562
|
+
|
|
563
|
+
Your container, page or widget can show **another installed app's widget** inline (a raffle entry, a
|
|
564
|
+
donate button, a loyalty card), natively, with no iframe. **The host does the work.** Portal and Hub pass
|
|
565
|
+
every app they mount an `EmbeddedWidget` component:
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
export function EventGuide({ EmbeddedWidget, ...props }: SmartLinksContainerProps) {
|
|
569
|
+
return (
|
|
570
|
+
<section>
|
|
571
|
+
<h2>Win a signed shirt</h2>
|
|
572
|
+
{EmbeddedWidget ? (
|
|
573
|
+
<EmbeddedWidget appId="raffle" size="standard" fallback={null} />
|
|
574
|
+
) : null}
|
|
575
|
+
</section>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
| Prop | What it does |
|
|
581
|
+
|---|---|
|
|
582
|
+
| `appId` | The app whose widget to show, as installed on this collection. |
|
|
583
|
+
| `component` | Which of its widget components (`widgets.components[].name` in its manifest). Default: the first. |
|
|
584
|
+
| `size` | `'compact' \| 'standard' \| 'large'` — a hint the widget may honour. Default `standard`. |
|
|
585
|
+
| `widgetId` | A configured instance, for apps with widget instances (see [Widget Instance Resolution](#widget-instance-resolution)). |
|
|
586
|
+
| `props` | Extra props for the widget: its documented props and settings. |
|
|
587
|
+
| `fallback` / `loading` | What to show if it can't be shown / while it loads. Default: nothing. |
|
|
588
|
+
|
|
589
|
+
What the host does for you:
|
|
590
|
+
- Loads the widget through its own bundle cache: the same code that renders the portal's widgets. It
|
|
591
|
+
provides the shared libraries (React, the SDK, the router, …) and picks the component.
|
|
592
|
+
- Gives it the same context you have: collection, product, proof, user, language, and navigation
|
|
593
|
+
(`onNavigate` goes straight to the portal, so the widget's links work as they do anywhere else).
|
|
594
|
+
- Mounts it in **its own router**, so it never touches your routes. Don't wrap it in a router.
|
|
595
|
+
- Contains any crash: a broken widget renders `fallback`, never takes down your page.
|
|
596
|
+
- Renders `fallback` when the app isn't installed on this collection, exports no widget, or isn't allowed.
|
|
597
|
+
|
|
598
|
+
**What can be embedded:** first-party SmartLinks widgets (app IDs without a developer handle), and your
|
|
599
|
+
own publisher's apps (`acme--raffle` from `acme--events`). The host enforces this; anything else renders
|
|
600
|
+
`fallback`, with a console warning. An app can't embed itself, and nesting stops two levels deep.
|
|
601
|
+
|
|
602
|
+
**Don't build your own loader** (getWidgets → evaluate the bundle → shared globals → require shim). Every
|
|
603
|
+
copy drifts and breaks; `EmbeddedWidget` is absent outside a host, so render nothing there.
|
|
604
|
+
|
|
605
|
+
**Finding out what's available:** in Forge, `forge-widgets catalog` lists the widgets you can embed
|
|
606
|
+
(apps installed on the preview's collection), and `forge-widgets describe <app>` gives a widget's
|
|
607
|
+
components, sizes, props, settings and setup options.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
560
611
|
## Parent Integration
|
|
561
612
|
|
|
562
613
|
### Dynamic Import
|
package/dist/openapi.yaml
CHANGED
|
@@ -30830,10 +30830,36 @@ components:
|
|
|
30830
30830
|
type: object
|
|
30831
30831
|
additionalProperties:
|
|
30832
30832
|
type: string
|
|
30833
|
+
EmbeddedWidget:
|
|
30834
|
+
$ref: "#/components/schemas/EmbeddedWidgetComponent"
|
|
30833
30835
|
required:
|
|
30834
30836
|
- collectionId
|
|
30835
30837
|
- appId
|
|
30836
30838
|
- SL
|
|
30839
|
+
EmbeddedWidgetProps:
|
|
30840
|
+
type: object
|
|
30841
|
+
properties:
|
|
30842
|
+
appId:
|
|
30843
|
+
type: string
|
|
30844
|
+
component:
|
|
30845
|
+
type: string
|
|
30846
|
+
size:
|
|
30847
|
+
type: string
|
|
30848
|
+
enum:
|
|
30849
|
+
- compact
|
|
30850
|
+
- standard
|
|
30851
|
+
- large
|
|
30852
|
+
widgetId:
|
|
30853
|
+
type: string
|
|
30854
|
+
props:
|
|
30855
|
+
type: object
|
|
30856
|
+
additionalProperties: true
|
|
30857
|
+
className:
|
|
30858
|
+
type: string
|
|
30859
|
+
fallback: {}
|
|
30860
|
+
loading: {}
|
|
30861
|
+
required:
|
|
30862
|
+
- appId
|
|
30837
30863
|
AppConfigOptions:
|
|
30838
30864
|
type: object
|
|
30839
30865
|
properties:
|
|
@@ -30932,28 +30958,3 @@ components:
|
|
|
30932
30958
|
properties:
|
|
30933
30959
|
appId:
|
|
30934
30960
|
type: string
|
|
30935
|
-
AllocateSequenceInput:
|
|
30936
|
-
type: object
|
|
30937
|
-
properties:
|
|
30938
|
-
appId:
|
|
30939
|
-
type: string
|
|
30940
|
-
sequenceId:
|
|
30941
|
-
type: string
|
|
30942
|
-
subjectId:
|
|
30943
|
-
type: string
|
|
30944
|
-
productId:
|
|
30945
|
-
type: string
|
|
30946
|
-
required:
|
|
30947
|
-
- appId
|
|
30948
|
-
- sequenceId
|
|
30949
|
-
- subjectId
|
|
30950
|
-
AllocatedSequence:
|
|
30951
|
-
type: object
|
|
30952
|
-
properties:
|
|
30953
|
-
number:
|
|
30954
|
-
type: number
|
|
30955
|
-
isNew:
|
|
30956
|
-
type: boolean
|
|
30957
|
-
required:
|
|
30958
|
-
- number
|
|
30959
|
-
- isNew
|
package/dist/types/widgets.d.ts
CHANGED
|
@@ -83,4 +83,32 @@ export interface SmartLinksWidgetProps {
|
|
|
83
83
|
lang?: string;
|
|
84
84
|
/** Translation key overrides */
|
|
85
85
|
translations?: Record<string, string>;
|
|
86
|
+
/**
|
|
87
|
+
* Show ANOTHER app's widget inside this app, rendered by the host (Portal / Hub):
|
|
88
|
+
* `<EmbeddedWidget appId="raffle" size="standard" widgetId="…" />`. The host loads it, gives it the
|
|
89
|
+
* same context you have, keeps it in its own router and contains any crash. Absent outside a host.
|
|
90
|
+
* Only first-party widgets and your own publisher's apps can be embedded. See docs/widgets.md
|
|
91
|
+
* "Using another app's widget".
|
|
92
|
+
*/
|
|
93
|
+
EmbeddedWidget?: EmbeddedWidgetComponent;
|
|
94
|
+
}
|
|
95
|
+
/** Props of the host-provided `EmbeddedWidget` (see {@link SmartLinksWidgetProps.EmbeddedWidget}). */
|
|
96
|
+
export interface EmbeddedWidgetProps {
|
|
97
|
+
/** The app whose widget to show, as installed on this collection. */
|
|
98
|
+
appId: string;
|
|
99
|
+
/** Which of its widget components (its manifest `widgets.components[].name`). Default: the first. */
|
|
100
|
+
component?: string;
|
|
101
|
+
/** Size hint the widget may honour. */
|
|
102
|
+
size?: 'compact' | 'standard' | 'large';
|
|
103
|
+
/** A configured instance of the widget (apps with widget instances — see "Widget Instance Resolution"). */
|
|
104
|
+
widgetId?: string;
|
|
105
|
+
/** Extra props for the widget: its documented props / settings. */
|
|
106
|
+
props?: Record<string, unknown>;
|
|
107
|
+
className?: string;
|
|
108
|
+
/** Shown when the widget can't be shown (not installed, no widget, not allowed, failed). Default: nothing. */
|
|
109
|
+
fallback?: unknown;
|
|
110
|
+
/** Shown while it loads. Default: nothing. */
|
|
111
|
+
loading?: unknown;
|
|
86
112
|
}
|
|
113
|
+
/** A React component taking {@link EmbeddedWidgetProps} (typed loosely: the SDK doesn't depend on React). */
|
|
114
|
+
export type EmbeddedWidgetComponent = (props: EmbeddedWidgetProps) => any;
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.55 | Generated: 2026-10-12T20:04:55.294Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -154,7 +154,6 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
154
154
|
- **realtime** - Functions for realtime operations
|
|
155
155
|
- **research** - Functions for research operations
|
|
156
156
|
- **secrets** - Functions for secrets operations
|
|
157
|
-
- **sequence** - Functions for sequence operations
|
|
158
157
|
- **tags** - Functions for tags operations
|
|
159
158
|
- **template** - Functions for template operations
|
|
160
159
|
- **translations** - Functions for translations operations
|
|
@@ -9744,9 +9743,26 @@ interface SmartLinksWidgetProps {
|
|
|
9744
9743
|
size?: 'compact' | 'standard' | 'large'
|
|
9745
9744
|
lang?: string
|
|
9746
9745
|
translations?: Record<string, string>
|
|
9746
|
+
EmbeddedWidget?: EmbeddedWidgetComponent
|
|
9747
9747
|
}
|
|
9748
9748
|
```
|
|
9749
9749
|
|
|
9750
|
+
**EmbeddedWidgetProps** (interface)
|
|
9751
|
+
```typescript
|
|
9752
|
+
interface EmbeddedWidgetProps {
|
|
9753
|
+
appId: string
|
|
9754
|
+
component?: string
|
|
9755
|
+
size?: 'compact' | 'standard' | 'large'
|
|
9756
|
+
widgetId?: string
|
|
9757
|
+
props?: Record<string, unknown>
|
|
9758
|
+
className?: string
|
|
9759
|
+
fallback?: unknown
|
|
9760
|
+
loading?: unknown
|
|
9761
|
+
}
|
|
9762
|
+
```
|
|
9763
|
+
|
|
9764
|
+
**EmbeddedWidgetComponent** = `(props: EmbeddedWidgetProps) => any`
|
|
9765
|
+
|
|
9750
9766
|
### appConfiguration (api)
|
|
9751
9767
|
|
|
9752
9768
|
**AppConfigOptions** (type)
|
|
@@ -9852,26 +9868,6 @@ interface AppSecretOptions {
|
|
|
9852
9868
|
}
|
|
9853
9869
|
```
|
|
9854
9870
|
|
|
9855
|
-
### sequence (api)
|
|
9856
|
-
|
|
9857
|
-
**AllocateSequenceInput** (interface)
|
|
9858
|
-
```typescript
|
|
9859
|
-
interface AllocateSequenceInput {
|
|
9860
|
-
appId: string
|
|
9861
|
-
sequenceId: string
|
|
9862
|
-
subjectId: string
|
|
9863
|
-
productId?: string
|
|
9864
|
-
}
|
|
9865
|
-
```
|
|
9866
|
-
|
|
9867
|
-
**AllocatedSequence** (interface)
|
|
9868
|
-
```typescript
|
|
9869
|
-
interface AllocatedSequence {
|
|
9870
|
-
number: number
|
|
9871
|
-
isNew: boolean
|
|
9872
|
-
}
|
|
9873
|
-
```
|
|
9874
|
-
|
|
9875
9871
|
### conditions (utils)
|
|
9876
9872
|
|
|
9877
9873
|
**BaseCondition** (interface)
|
|
@@ -10380,7 +10376,7 @@ Delete a keyed data item by ID within a scope. Requires admin authentication. ``
|
|
|
10380
10376
|
|
|
10381
10377
|
**getWidgets**(collectionId: string,
|
|
10382
10378
|
options?: GetCollectionWidgetsOptions) → `Promise<CollectionWidgetsResponse>`
|
|
10383
|
-
Fetches ALL widget data (manifests + bundle files) for a collection in one call. Returns everything needed to render widgets with zero additional requests. This solves N+1 query problems by fetching manifests, JavaScript bundles, and CSS files in parallel on the server. ```typescript // Fetch all widget data for a collection const { apps } = await Api.AppConfiguration.getWidgets(collectionId); // Returns: [{ appId,
|
|
10379
|
+
Fetches ALL widget data (manifests + bundle files) for a collection in one call. Returns everything needed to render widgets with zero additional requests. This solves N+1 query problems by fetching manifests, JavaScript bundles, and CSS files in parallel on the server. ```typescript // Fetch all widget data for a collection const { apps } = await Api.AppConfiguration.getWidgets(collectionId); // Returns: [{ appId, manifest, widget, container, admin }, ...] // widget / container: { js: { umd?, esm? }, css?, source?, styles? } // source / styles - the bundle inlined (when small enough); js / css - URLs otherwise. // admin - URL of the app's app.admin.json (setup / tunable options), or null. // Bundles don't ship React: they resolve shared libraries from the HOST's globals // (host-dependency-contract.md), so loading one is a host's job. // // Inside a host (Portal / Hub), an app showing ANOTHER app's widget should not load // bundles itself - use the host-provided EmbeddedWidget prop: // <EmbeddedWidget appId="raffle" size="standard" /> (docs/widgets.md) // Force refresh all widgets const { apps } = await Api.AppConfiguration.getWidgets(collectionId, { force: true }); ```
|
|
10384
10380
|
|
|
10385
10381
|
**resolveFeature**(system: SystemBlock | undefined, flag: string) → `boolean`
|
|
10386
10382
|
Resolve whether a feature flag is on, applying the `accountType` default: `enterprise` accounts default every flag to **on** unless `features[flag]` is explicitly `false`; `standard` accounts (or missing `accountType`) default every flag to **off** unless `features[flag]` is explicitly `true`. An explicit value always wins over the default either way. This is the same logic `isFeatureEnabled()` / `isFeatureEnabledSync()` use internally — call those instead in normal code. `system.features` and `accountType` are public data (no admin-only feature-check path exists), so there's no reason to fetch differently for an admin surface. This is exposed separately only for the rare case where you already have a `system` block in hand from somewhere other than `getAppConfig()` (e.g. server-rendered props) and want to resolve a flag from it directly. ```typescript const enabled = appConfiguration.resolveFeature(someSystemBlock, 'custom_domain'); ```
|
|
@@ -12234,11 +12230,6 @@ Soft-delete a secret. DELETE /secrets/:ref
|
|
|
12234
12230
|
id: string,
|
|
12235
12231
|
query: { limit?: number; offset?: number } = {}) → `Promise<SegmentRecipientsResponse>`
|
|
12236
12232
|
|
|
12237
|
-
### sequence
|
|
12238
|
-
|
|
12239
|
-
**allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
|
|
12240
|
-
Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, }) `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
|
|
12241
|
-
|
|
12242
12233
|
### sessions
|
|
12243
12234
|
|
|
12244
12235
|
**stats**(collectionId: string) → `Promise<SessionStatistics>`
|
package/docs/containers.md
CHANGED
|
@@ -226,6 +226,13 @@ All standard widget props apply:
|
|
|
226
226
|
|
|
227
227
|
---
|
|
228
228
|
|
|
229
|
+
### Showing another app's widget
|
|
230
|
+
|
|
231
|
+
A container can show another installed app's widget inline with the host-provided `EmbeddedWidget` prop:
|
|
232
|
+
`<EmbeddedWidget appId="raffle" size="standard" />`. The host loads it, isolates it and contains any crash.
|
|
233
|
+
See [widgets.md → Using another app's widget](widgets.md#using-another-apps-widget). Don't hand-roll a
|
|
234
|
+
widget loader.
|
|
235
|
+
|
|
229
236
|
## Cross-App Navigation
|
|
230
237
|
|
|
231
238
|
Containers support the same **structured navigation requests** as widgets. When a container needs to navigate to another app within the portal, it emits a `NavigationRequest` via the `onNavigate` callback. The portal orchestrator interprets the request, preserves hierarchy context, and performs the navigation.
|
package/docs/overview.md
CHANGED
|
@@ -93,7 +93,6 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
93
93
|
| **Container Tracking** | `docs/container-tracking.md` | Physical/logical **item** container groupings — *not* app containers (see containers.md) |
|
|
94
94
|
| **Lots** | `docs/lots.md` | Cross-SKU production "Lot" entity |
|
|
95
95
|
| **Loyalty** | `docs/loyalty.md` | Points, members, earning rules |
|
|
96
|
-
| **Sequences** | `docs/sequences.md` | RETIRING — number things in your own server function: `ctx.sl.counters` + `appRecords.claim` (server-functions.md) |
|
|
97
96
|
| **Integrations** | `docs/integrations.md` | Inbound/outbound external-system integration flows |
|
|
98
97
|
| **Item Context** | `docs/item-context.md` | The `itemContext` container prop (serial / NFC authenticity context) |
|
|
99
98
|
| **Native Facade** | `docs/native-facade.md` | `host.native` / `SL.native` device-capability facade |
|
package/docs/widgets.md
CHANGED
|
@@ -146,6 +146,7 @@ interface SmartLinksWidgetProps {
|
|
|
146
146
|
| `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
|
|
147
147
|
| `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
|
|
148
148
|
| `translations` | `object?` | Translation overrides |
|
|
149
|
+
| `EmbeddedWidget` | `component?` | Host-provided: show another app's widget inside yours — see [Using another app's widget](#using-another-apps-widget) |
|
|
149
150
|
|
|
150
151
|
---
|
|
151
152
|
|
|
@@ -557,6 +558,56 @@ If `VITE_ENABLE_WIDGETS` is not set to `true`, the build produces a minimal stub
|
|
|
557
558
|
|
|
558
559
|
---
|
|
559
560
|
|
|
561
|
+
## Using another app's widget
|
|
562
|
+
|
|
563
|
+
Your container, page or widget can show **another installed app's widget** inline (a raffle entry, a
|
|
564
|
+
donate button, a loyalty card), natively, with no iframe. **The host does the work.** Portal and Hub pass
|
|
565
|
+
every app they mount an `EmbeddedWidget` component:
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
export function EventGuide({ EmbeddedWidget, ...props }: SmartLinksContainerProps) {
|
|
569
|
+
return (
|
|
570
|
+
<section>
|
|
571
|
+
<h2>Win a signed shirt</h2>
|
|
572
|
+
{EmbeddedWidget ? (
|
|
573
|
+
<EmbeddedWidget appId="raffle" size="standard" fallback={null} />
|
|
574
|
+
) : null}
|
|
575
|
+
</section>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
| Prop | What it does |
|
|
581
|
+
|---|---|
|
|
582
|
+
| `appId` | The app whose widget to show, as installed on this collection. |
|
|
583
|
+
| `component` | Which of its widget components (`widgets.components[].name` in its manifest). Default: the first. |
|
|
584
|
+
| `size` | `'compact' \| 'standard' \| 'large'` — a hint the widget may honour. Default `standard`. |
|
|
585
|
+
| `widgetId` | A configured instance, for apps with widget instances (see [Widget Instance Resolution](#widget-instance-resolution)). |
|
|
586
|
+
| `props` | Extra props for the widget: its documented props and settings. |
|
|
587
|
+
| `fallback` / `loading` | What to show if it can't be shown / while it loads. Default: nothing. |
|
|
588
|
+
|
|
589
|
+
What the host does for you:
|
|
590
|
+
- Loads the widget through its own bundle cache: the same code that renders the portal's widgets. It
|
|
591
|
+
provides the shared libraries (React, the SDK, the router, …) and picks the component.
|
|
592
|
+
- Gives it the same context you have: collection, product, proof, user, language, and navigation
|
|
593
|
+
(`onNavigate` goes straight to the portal, so the widget's links work as they do anywhere else).
|
|
594
|
+
- Mounts it in **its own router**, so it never touches your routes. Don't wrap it in a router.
|
|
595
|
+
- Contains any crash: a broken widget renders `fallback`, never takes down your page.
|
|
596
|
+
- Renders `fallback` when the app isn't installed on this collection, exports no widget, or isn't allowed.
|
|
597
|
+
|
|
598
|
+
**What can be embedded:** first-party SmartLinks widgets (app IDs without a developer handle), and your
|
|
599
|
+
own publisher's apps (`acme--raffle` from `acme--events`). The host enforces this; anything else renders
|
|
600
|
+
`fallback`, with a console warning. An app can't embed itself, and nesting stops two levels deep.
|
|
601
|
+
|
|
602
|
+
**Don't build your own loader** (getWidgets → evaluate the bundle → shared globals → require shim). Every
|
|
603
|
+
copy drifts and breaks; `EmbeddedWidget` is absent outside a host, so render nothing there.
|
|
604
|
+
|
|
605
|
+
**Finding out what's available:** in Forge, `forge-widgets catalog` lists the widgets you can embed
|
|
606
|
+
(apps installed on the preview's collection), and `forge-widgets describe <app>` gives a widget's
|
|
607
|
+
components, sizes, props, settings and setup options.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
560
611
|
## Parent Integration
|
|
561
612
|
|
|
562
613
|
### Dynamic Import
|
package/openapi.yaml
CHANGED
|
@@ -30830,10 +30830,36 @@ components:
|
|
|
30830
30830
|
type: object
|
|
30831
30831
|
additionalProperties:
|
|
30832
30832
|
type: string
|
|
30833
|
+
EmbeddedWidget:
|
|
30834
|
+
$ref: "#/components/schemas/EmbeddedWidgetComponent"
|
|
30833
30835
|
required:
|
|
30834
30836
|
- collectionId
|
|
30835
30837
|
- appId
|
|
30836
30838
|
- SL
|
|
30839
|
+
EmbeddedWidgetProps:
|
|
30840
|
+
type: object
|
|
30841
|
+
properties:
|
|
30842
|
+
appId:
|
|
30843
|
+
type: string
|
|
30844
|
+
component:
|
|
30845
|
+
type: string
|
|
30846
|
+
size:
|
|
30847
|
+
type: string
|
|
30848
|
+
enum:
|
|
30849
|
+
- compact
|
|
30850
|
+
- standard
|
|
30851
|
+
- large
|
|
30852
|
+
widgetId:
|
|
30853
|
+
type: string
|
|
30854
|
+
props:
|
|
30855
|
+
type: object
|
|
30856
|
+
additionalProperties: true
|
|
30857
|
+
className:
|
|
30858
|
+
type: string
|
|
30859
|
+
fallback: {}
|
|
30860
|
+
loading: {}
|
|
30861
|
+
required:
|
|
30862
|
+
- appId
|
|
30837
30863
|
AppConfigOptions:
|
|
30838
30864
|
type: object
|
|
30839
30865
|
properties:
|
|
@@ -30932,28 +30958,3 @@ components:
|
|
|
30932
30958
|
properties:
|
|
30933
30959
|
appId:
|
|
30934
30960
|
type: string
|
|
30935
|
-
AllocateSequenceInput:
|
|
30936
|
-
type: object
|
|
30937
|
-
properties:
|
|
30938
|
-
appId:
|
|
30939
|
-
type: string
|
|
30940
|
-
sequenceId:
|
|
30941
|
-
type: string
|
|
30942
|
-
subjectId:
|
|
30943
|
-
type: string
|
|
30944
|
-
productId:
|
|
30945
|
-
type: string
|
|
30946
|
-
required:
|
|
30947
|
-
- appId
|
|
30948
|
-
- sequenceId
|
|
30949
|
-
- subjectId
|
|
30950
|
-
AllocatedSequence:
|
|
30951
|
-
type: object
|
|
30952
|
-
properties:
|
|
30953
|
-
number:
|
|
30954
|
-
type: number
|
|
30955
|
-
isNew:
|
|
30956
|
-
type: boolean
|
|
30957
|
-
required:
|
|
30958
|
-
- number
|
|
30959
|
-
- isNew
|
package/package.json
CHANGED
package/scripts/doctor.mjs
CHANGED
|
@@ -269,6 +269,50 @@ if (meta.respectsHostTheme) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
// ---- showing another app's widget: use the host's EmbeddedWidget, for widgets it allows -----------
|
|
273
|
+
// Hosts (Portal, Hub) pass every app an `EmbeddedWidget` prop (docs/widgets.md → "Using another app's
|
|
274
|
+
// widget"). A hand-rolled loader (getWidgets → evaluate the bundle) drifts and breaks; and the host only
|
|
275
|
+
// renders first-party widgets and the same publisher's apps, so anything else silently shows nothing.
|
|
276
|
+
// Static scan of the source; WARN-only.
|
|
277
|
+
{
|
|
278
|
+
const ownId = String(manifest.meta?.appId || '');
|
|
279
|
+
const handleOf = (id) => { const i = id.indexOf('--'); return i > 0 ? id.slice(0, i) : null; };
|
|
280
|
+
const EXT = /\.(tsx|ts|jsx|js)$/;
|
|
281
|
+
const SKIP = new Set(['node_modules', 'dist', '.git', 'public']);
|
|
282
|
+
const files = [];
|
|
283
|
+
(function walkSrc(dir) {
|
|
284
|
+
let entries = [];
|
|
285
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
286
|
+
for (const e of entries) {
|
|
287
|
+
if (e.isDirectory()) { if (!SKIP.has(e.name)) walkSrc(join(dir, e.name)); }
|
|
288
|
+
else if (EXT.test(e.name)) files.push(join(dir, e.name));
|
|
289
|
+
}
|
|
290
|
+
})(existsSync(join(appDir, 'src')) ? join(appDir, 'src') : appDir);
|
|
291
|
+
const rel = (f) => (f.startsWith(appDir) ? f.slice(appDir.length + 1) : f);
|
|
292
|
+
const handRolled = [];
|
|
293
|
+
const blocked = new Set();
|
|
294
|
+
let embeds = 0;
|
|
295
|
+
for (const f of files) {
|
|
296
|
+
const text = readFileSync(f, 'utf8');
|
|
297
|
+
if (/\bgetWidgets\s*\(/.test(text) && /(new\s+Blob\s*\(|new\s+Function\s*\(|createRequireShim|import\s*\(\s*\/\*\s*@vite-ignore)/.test(text)) handRolled.push(rel(f));
|
|
298
|
+
for (const m of text.matchAll(/<EmbeddedWidget\b[^>]*?\bappId\s*=\s*\{?\s*["'`]([A-Za-z0-9._-]+)["'`]/g)) {
|
|
299
|
+
embeds++;
|
|
300
|
+
const id = m[1];
|
|
301
|
+
const owner = handleOf(id);
|
|
302
|
+
if (id === ownId || (owner !== null && owner !== handleOf(ownId))) blocked.add(id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (handRolled.length) {
|
|
306
|
+
console.log(`${YELLOW}⚠${RESET} embedded widgets — a hand-rolled widget loader in ${handRolled.slice(0, 3).join(', ')}`);
|
|
307
|
+
warnings.push(`${handRolled.join(', ')} load${handRolled.length === 1 ? 's' : ''} another app's widget bundle by hand (getWidgets + evaluating it). Use the host's EmbeddedWidget prop instead: <EmbeddedWidget appId="…" /> (docs/widgets.md → "Using another app's widget").`);
|
|
308
|
+
}
|
|
309
|
+
if (blocked.size) {
|
|
310
|
+
console.log(`${YELLOW}⚠${RESET} embedded widgets — the host won't show: ${[...blocked].join(', ')}`);
|
|
311
|
+
warnings.push(`EmbeddedWidget for ${[...blocked].join(', ')} will render nothing: hosts only show first-party widgets and the same publisher's apps${ownId ? ` (this app is "${ownId}")` : ''}, and never the app itself.`);
|
|
312
|
+
}
|
|
313
|
+
if (embeds && !handRolled.length && !blocked.size) console.log(`${GREEN}✓${RESET} embedded widgets ${DIM}— ${embeds} via the host's EmbeddedWidget${RESET}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
272
316
|
// ---- package.json scripts point at files that exist ------------------------------------------
|
|
273
317
|
// An upgrade that copies a script LINE but not the script FILE breaks every build at that step
|
|
274
318
|
// (e.g. `postbuild: node scripts/smartlinks-deploy.mjs` with no scripts/smartlinks-deploy.mjs).
|
package/dist/api/sequence.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface AllocateSequenceInput {
|
|
2
|
-
/** The app that owns the sequence config. */
|
|
3
|
-
appId: string;
|
|
4
|
-
/** The configured sequence id — the key of data.sequenceConfigs on the app config. */
|
|
5
|
-
sequenceId: string;
|
|
6
|
-
/**
|
|
7
|
-
* The STABLE subject identity. For a `claimSet` sequence, pass the tap's virtual-proof id
|
|
8
|
-
* `<claimSetId>-<code>` (e.g. "23-oOkf8o") for a per-wristband raffle — the code is the tag's
|
|
9
|
-
* permanent id, so re-taps of the same wristband return the same number; the number is stamped
|
|
10
|
-
* on that code's doc and surfaces in `tagData`. Pass a bare claim-set id for one number per
|
|
11
|
-
* group. Never a value that changes per tap.
|
|
12
|
-
*/
|
|
13
|
-
subjectId: string;
|
|
14
|
-
/** Optional product scope, when the sequence config is scoped per product. */
|
|
15
|
-
productId?: string;
|
|
16
|
-
}
|
|
17
|
-
export interface AllocatedSequence {
|
|
18
|
-
/** The allocated (or already-held) number. */
|
|
19
|
-
number: number;
|
|
20
|
-
/** True when this call allocated a new number; false when the subject already had one. */
|
|
21
|
-
isNew: boolean;
|
|
22
|
-
}
|
|
23
|
-
export declare namespace sequence {
|
|
24
|
-
/**
|
|
25
|
-
* Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
|
|
26
|
-
* call on load (auto-enter) and on a button tap; a subject that already has a number gets
|
|
27
|
-
* it back with `isNew: false`.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* const { number, isNew } = await sequence.allocate(collectionId, {
|
|
31
|
-
* appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
|
|
32
|
-
* })
|
|
33
|
-
*
|
|
34
|
-
* @deprecated Being removed. Number things in your app's own server function with
|
|
35
|
-
* `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
|
|
36
|
-
*/
|
|
37
|
-
function allocate(collectionId: string, input: AllocateSequenceInput): Promise<AllocatedSequence>;
|
|
38
|
-
}
|
package/dist/api/sequence.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// src/api/sequence.ts
|
|
2
|
-
//
|
|
3
|
-
// Sequences — allocate a guaranteed-unique, monotonic number (raffle tickets, "Nth to
|
|
4
|
-
// claim", queue positions) and stamp it onto a record. The sequence (counter key, target,
|
|
5
|
-
// field, scope) is configured server-side in app config (data.sequenceConfigs[sequenceId]);
|
|
6
|
-
// this call supplies only the subject and is idempotent — a re-tap returns the same number.
|
|
7
|
-
// See docs/sequences.md.
|
|
8
|
-
import { post } from "../http.js";
|
|
9
|
-
export var sequence;
|
|
10
|
-
(function (sequence) {
|
|
11
|
-
const base = (collectionId) => `/public/collection/${encodeURIComponent(collectionId)}/sequence`;
|
|
12
|
-
/**
|
|
13
|
-
* Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
|
|
14
|
-
* call on load (auto-enter) and on a button tap; a subject that already has a number gets
|
|
15
|
-
* it back with `isNew: false`.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* const { number, isNew } = await sequence.allocate(collectionId, {
|
|
19
|
-
* appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
|
|
20
|
-
* })
|
|
21
|
-
*
|
|
22
|
-
* @deprecated Being removed. Number things in your app's own server function with
|
|
23
|
-
* `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
|
|
24
|
-
*/
|
|
25
|
-
async function allocate(collectionId, input) {
|
|
26
|
-
return post(`${base(collectionId)}/allocate`, input);
|
|
27
|
-
}
|
|
28
|
-
sequence.allocate = allocate;
|
|
29
|
-
})(sequence || (sequence = {}));
|
package/dist/docs/sequences.md
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Sequences & claim-order allocation
|
|
2
|
-
|
|
3
|
-
> **Retiring — don't build on this.** `SL.sequence.allocate` is being removed. Number things in your
|
|
4
|
-
> app's own server function instead: `ctx.sl.counters.next()` for the next number and
|
|
5
|
-
> `ctx.sl.appRecords.claim()` to key it to a tag/person and make sure no number is ever held twice —
|
|
6
|
-
> see server-functions.md → "Uniqueness, numbering and people". (The counter here lived inside the
|
|
7
|
-
> app config doc, where an admin save could rewind it.)
|
|
8
|
-
|
|
9
|
-
> **SmartLinks SDK 2.x** (current `latest`). Install `@proveanything/smartlinks@^2`.
|
|
10
|
-
|
|
11
|
-
A **sequence** hands out a guaranteed-unique, monotonic number — `1, 2, 3, …` — and stamps it
|
|
12
|
-
onto a record. It's the primitive behind raffle tickets, "you're the Nth to claim", queue
|
|
13
|
-
positions, and limited-edition numbering: cases where you need *the next number*, exactly
|
|
14
|
-
once per subject, even with a whole room (or stadium) acting at the same instant.
|
|
15
|
-
|
|
16
|
-
## The model: allocate, then stamp — and the target is the ledger
|
|
17
|
-
|
|
18
|
-
Two steps, one call:
|
|
19
|
-
|
|
20
|
-
1. **Allocate** — get the next number from an **atomic counter**. The counter lives inside
|
|
21
|
-
your **app-config** doc at `data.sequences.<key>`, so it's scoped exactly like your other
|
|
22
|
-
app config (per collection / product / variant / batch) and needs no dedicated storage.
|
|
23
|
-
It's incremented in a single statement, so 250 simultaneous taps each get a distinct
|
|
24
|
-
number in microseconds — never two the same.
|
|
25
|
-
2. **Stamp** — write that number onto the **subject's own record** (a claim set, a proof, or
|
|
26
|
-
an app record). **That record is the ledger** — there's no separate bookkeeping table.
|
|
27
|
-
Next time you read the subject, the number is just there.
|
|
28
|
-
|
|
29
|
-
Because the target is the ledger, allocation is **idempotent**: on a re-tap we read the
|
|
30
|
-
subject first and return the number it already has, rather than allocating a second one.
|
|
31
|
-
|
|
32
|
-
## Guarantees
|
|
33
|
-
|
|
34
|
-
- **Unique + monotonic** — the counter is an atomic increment, never a read-modify-write, so
|
|
35
|
-
there are no duplicates and no lost updates under load.
|
|
36
|
-
- **Idempotent per subject** — one number per subject; a re-tap returns the same number.
|
|
37
|
-
- **Concurrency-safe at scale** — the only shared hot spot is the one counter row (Postgres
|
|
38
|
-
serializes it); the stamp is per-subject (its own record), so the model holds from 250 in a
|
|
39
|
-
room to 60,000 in a stadium. A concurrent double-tap on the *same* subject at worst skips
|
|
40
|
-
one number (a harmless gap) — never puts two numbers on one person.
|
|
41
|
-
|
|
42
|
-
## Scoping
|
|
43
|
-
|
|
44
|
-
- **Counter scope** — the app-config doc it lives on: collection-wide, or per product /
|
|
45
|
-
variant / batch. All wristbands mapped to one product? Scope the counter to that product.
|
|
46
|
-
- **`key`** — a name within that doc (e.g. `raffle:2026-cup`), so one config doc can hold
|
|
47
|
-
several independent sequences.
|
|
48
|
-
- **`start`** — the first number (default `1`); use it to reserve a block (start at `51` and
|
|
49
|
-
tell people it's `1`, or just filter `< N` at draw time).
|
|
50
|
-
|
|
51
|
-
## Idempotency subject — use a *stable* id
|
|
52
|
-
|
|
53
|
-
The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
|
|
54
|
-
- **Virtual-proof id `<claimSetId>-<code>`** (e.g. `23-oOkf8o`) — the right key for a
|
|
55
|
-
per-wristband raffle. The `code` is the physical tag's permanent id, so a re-tap of the same
|
|
56
|
-
wristband resolves to the same code doc → same number. The virtual proof record is transient,
|
|
57
|
-
but the **code** inside its id is stable — that's what the sink keys on (it splits the id and
|
|
58
|
-
uses only `<claimSetId>` + `<code>`).
|
|
59
|
-
- **Bare claim-set id** — when one number per *group* is intended.
|
|
60
|
-
- The authenticated **user/contact** — if they sign in / claim.
|
|
61
|
-
|
|
62
|
-
Do **not** key on the transient part of a per-tap identity (a freshly-minted virtual proof
|
|
63
|
-
record) — key on the stable **code**, which is exactly what the `<claimSetId>-<code>` subject
|
|
64
|
-
carries.
|
|
65
|
-
|
|
66
|
-
## Where the number is stored (the sink)
|
|
67
|
-
|
|
68
|
-
The stamp target is configurable — the number lives wherever you'll read it:
|
|
69
|
-
|
|
70
|
-
| Target | Subject id | Use |
|
|
71
|
-
|---|---|---|
|
|
72
|
-
| `claimSet` | the virtual-proof id `<claimSetId>-<code>` (e.g. `23-oOkf8o`) **or** a bare `<claimSetId>` | The Firestore claim-set world. **The subject decides the exact doc:** a `<claimSetId>-<code>` id stamps the individual **code** doc (per-wristband — rides back as `tagData` on the next tap, every wristband its own number); a bare id stamps the whole **set** doc (one shared value). Claim-set ids are hyphen-free, so a hyphen unambiguously means the per-code form. |
|
|
73
|
-
| `proof` | the proof id | A minted proof (value or attestation) — when the number should travel with the proof. |
|
|
74
|
-
| `appRecord` | the record id | A structured app record — for queryable, per-app data. |
|
|
75
|
-
|
|
76
|
-
## Configure the sequence (once, server-side)
|
|
77
|
-
|
|
78
|
-
You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
|
|
79
|
-
makes the public endpoint safe: the target/field/scope are set by you, not the caller.
|
|
80
|
-
|
|
81
|
-
```jsonc
|
|
82
|
-
// app config: data.sequenceConfigs
|
|
83
|
-
{
|
|
84
|
-
"raffle": {
|
|
85
|
-
"key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
|
|
86
|
-
"target": "claimSet", // claimSet | proof | appRecord — the ledger domain
|
|
87
|
-
"field": "raffleNumber", // the property written on the target (comes back in tagData)
|
|
88
|
-
"start": 1 // first number (optional, default 1)
|
|
89
|
-
// no productId → one collection-wide counter (all wristbands share the sequence)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
> **Per-wristband raffle.** Keep `target: "claimSet"` and pass the tap's virtual-proof id
|
|
95
|
-
> (`<claimSetId>-<code>`) as the subject — the sink writes the individual tag's **code** doc, so
|
|
96
|
-
> each wristband gets its own number and it surfaces in `tagData` on the next tap. Pass a bare
|
|
97
|
-
> claim-set id instead only if you want one number for the whole group.
|
|
98
|
-
|
|
99
|
-
## Call it (the widget)
|
|
100
|
-
|
|
101
|
-
Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
|
|
102
|
-
never the target/field:
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
POST /api/v1/public/collection/:collectionId/sequence/allocate
|
|
106
|
-
{ "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
|
|
107
|
-
→ { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- **`subjectId`** is the STABLE identity. For `claimCode` it's the virtual-proof id
|
|
111
|
-
`<claimSetId>-<code>` (e.g. `23-oOkf8o`) straight off the tap — re-taps of the same wristband
|
|
112
|
-
collapse to one number. (For `claimSet` it's the claim-set id.)
|
|
113
|
-
- **Idempotent**, so both your flows are the *same call*:
|
|
114
|
-
- **Auto:** on load, call allocate → get your number (existing or freshly minted).
|
|
115
|
-
- **Button:** click → animate → same call → "Your raffle number is 42."
|
|
116
|
-
- **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
|
|
117
|
-
ledger. Or call allocate again; you'll get the same number with `isNew: false`.
|
|
118
|
-
|
|
119
|
-
Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
|
|
120
|
-
`400 BAD_REQUEST` (missing fields).
|
|
121
|
-
|
|
122
|
-
## Drawing the winner
|
|
123
|
-
|
|
124
|
-
No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
|
|
125
|
-
is your entry list, in allocation order.
|
package/docs/sequences.md
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Sequences & claim-order allocation
|
|
2
|
-
|
|
3
|
-
> **Retiring — don't build on this.** `SL.sequence.allocate` is being removed. Number things in your
|
|
4
|
-
> app's own server function instead: `ctx.sl.counters.next()` for the next number and
|
|
5
|
-
> `ctx.sl.appRecords.claim()` to key it to a tag/person and make sure no number is ever held twice —
|
|
6
|
-
> see server-functions.md → "Uniqueness, numbering and people". (The counter here lived inside the
|
|
7
|
-
> app config doc, where an admin save could rewind it.)
|
|
8
|
-
|
|
9
|
-
> **SmartLinks SDK 2.x** (current `latest`). Install `@proveanything/smartlinks@^2`.
|
|
10
|
-
|
|
11
|
-
A **sequence** hands out a guaranteed-unique, monotonic number — `1, 2, 3, …` — and stamps it
|
|
12
|
-
onto a record. It's the primitive behind raffle tickets, "you're the Nth to claim", queue
|
|
13
|
-
positions, and limited-edition numbering: cases where you need *the next number*, exactly
|
|
14
|
-
once per subject, even with a whole room (or stadium) acting at the same instant.
|
|
15
|
-
|
|
16
|
-
## The model: allocate, then stamp — and the target is the ledger
|
|
17
|
-
|
|
18
|
-
Two steps, one call:
|
|
19
|
-
|
|
20
|
-
1. **Allocate** — get the next number from an **atomic counter**. The counter lives inside
|
|
21
|
-
your **app-config** doc at `data.sequences.<key>`, so it's scoped exactly like your other
|
|
22
|
-
app config (per collection / product / variant / batch) and needs no dedicated storage.
|
|
23
|
-
It's incremented in a single statement, so 250 simultaneous taps each get a distinct
|
|
24
|
-
number in microseconds — never two the same.
|
|
25
|
-
2. **Stamp** — write that number onto the **subject's own record** (a claim set, a proof, or
|
|
26
|
-
an app record). **That record is the ledger** — there's no separate bookkeeping table.
|
|
27
|
-
Next time you read the subject, the number is just there.
|
|
28
|
-
|
|
29
|
-
Because the target is the ledger, allocation is **idempotent**: on a re-tap we read the
|
|
30
|
-
subject first and return the number it already has, rather than allocating a second one.
|
|
31
|
-
|
|
32
|
-
## Guarantees
|
|
33
|
-
|
|
34
|
-
- **Unique + monotonic** — the counter is an atomic increment, never a read-modify-write, so
|
|
35
|
-
there are no duplicates and no lost updates under load.
|
|
36
|
-
- **Idempotent per subject** — one number per subject; a re-tap returns the same number.
|
|
37
|
-
- **Concurrency-safe at scale** — the only shared hot spot is the one counter row (Postgres
|
|
38
|
-
serializes it); the stamp is per-subject (its own record), so the model holds from 250 in a
|
|
39
|
-
room to 60,000 in a stadium. A concurrent double-tap on the *same* subject at worst skips
|
|
40
|
-
one number (a harmless gap) — never puts two numbers on one person.
|
|
41
|
-
|
|
42
|
-
## Scoping
|
|
43
|
-
|
|
44
|
-
- **Counter scope** — the app-config doc it lives on: collection-wide, or per product /
|
|
45
|
-
variant / batch. All wristbands mapped to one product? Scope the counter to that product.
|
|
46
|
-
- **`key`** — a name within that doc (e.g. `raffle:2026-cup`), so one config doc can hold
|
|
47
|
-
several independent sequences.
|
|
48
|
-
- **`start`** — the first number (default `1`); use it to reserve a block (start at `51` and
|
|
49
|
-
tell people it's `1`, or just filter `< N` at draw time).
|
|
50
|
-
|
|
51
|
-
## Idempotency subject — use a *stable* id
|
|
52
|
-
|
|
53
|
-
The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
|
|
54
|
-
- **Virtual-proof id `<claimSetId>-<code>`** (e.g. `23-oOkf8o`) — the right key for a
|
|
55
|
-
per-wristband raffle. The `code` is the physical tag's permanent id, so a re-tap of the same
|
|
56
|
-
wristband resolves to the same code doc → same number. The virtual proof record is transient,
|
|
57
|
-
but the **code** inside its id is stable — that's what the sink keys on (it splits the id and
|
|
58
|
-
uses only `<claimSetId>` + `<code>`).
|
|
59
|
-
- **Bare claim-set id** — when one number per *group* is intended.
|
|
60
|
-
- The authenticated **user/contact** — if they sign in / claim.
|
|
61
|
-
|
|
62
|
-
Do **not** key on the transient part of a per-tap identity (a freshly-minted virtual proof
|
|
63
|
-
record) — key on the stable **code**, which is exactly what the `<claimSetId>-<code>` subject
|
|
64
|
-
carries.
|
|
65
|
-
|
|
66
|
-
## Where the number is stored (the sink)
|
|
67
|
-
|
|
68
|
-
The stamp target is configurable — the number lives wherever you'll read it:
|
|
69
|
-
|
|
70
|
-
| Target | Subject id | Use |
|
|
71
|
-
|---|---|---|
|
|
72
|
-
| `claimSet` | the virtual-proof id `<claimSetId>-<code>` (e.g. `23-oOkf8o`) **or** a bare `<claimSetId>` | The Firestore claim-set world. **The subject decides the exact doc:** a `<claimSetId>-<code>` id stamps the individual **code** doc (per-wristband — rides back as `tagData` on the next tap, every wristband its own number); a bare id stamps the whole **set** doc (one shared value). Claim-set ids are hyphen-free, so a hyphen unambiguously means the per-code form. |
|
|
73
|
-
| `proof` | the proof id | A minted proof (value or attestation) — when the number should travel with the proof. |
|
|
74
|
-
| `appRecord` | the record id | A structured app record — for queryable, per-app data. |
|
|
75
|
-
|
|
76
|
-
## Configure the sequence (once, server-side)
|
|
77
|
-
|
|
78
|
-
You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
|
|
79
|
-
makes the public endpoint safe: the target/field/scope are set by you, not the caller.
|
|
80
|
-
|
|
81
|
-
```jsonc
|
|
82
|
-
// app config: data.sequenceConfigs
|
|
83
|
-
{
|
|
84
|
-
"raffle": {
|
|
85
|
-
"key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
|
|
86
|
-
"target": "claimSet", // claimSet | proof | appRecord — the ledger domain
|
|
87
|
-
"field": "raffleNumber", // the property written on the target (comes back in tagData)
|
|
88
|
-
"start": 1 // first number (optional, default 1)
|
|
89
|
-
// no productId → one collection-wide counter (all wristbands share the sequence)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
> **Per-wristband raffle.** Keep `target: "claimSet"` and pass the tap's virtual-proof id
|
|
95
|
-
> (`<claimSetId>-<code>`) as the subject — the sink writes the individual tag's **code** doc, so
|
|
96
|
-
> each wristband gets its own number and it surfaces in `tagData` on the next tap. Pass a bare
|
|
97
|
-
> claim-set id instead only if you want one number for the whole group.
|
|
98
|
-
|
|
99
|
-
## Call it (the widget)
|
|
100
|
-
|
|
101
|
-
Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
|
|
102
|
-
never the target/field:
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
POST /api/v1/public/collection/:collectionId/sequence/allocate
|
|
106
|
-
{ "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
|
|
107
|
-
→ { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- **`subjectId`** is the STABLE identity. For `claimCode` it's the virtual-proof id
|
|
111
|
-
`<claimSetId>-<code>` (e.g. `23-oOkf8o`) straight off the tap — re-taps of the same wristband
|
|
112
|
-
collapse to one number. (For `claimSet` it's the claim-set id.)
|
|
113
|
-
- **Idempotent**, so both your flows are the *same call*:
|
|
114
|
-
- **Auto:** on load, call allocate → get your number (existing or freshly minted).
|
|
115
|
-
- **Button:** click → animate → same call → "Your raffle number is 42."
|
|
116
|
-
- **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
|
|
117
|
-
ledger. Or call allocate again; you'll get the same number with `isNew: false`.
|
|
118
|
-
|
|
119
|
-
Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
|
|
120
|
-
`400 BAD_REQUEST` (missing fields).
|
|
121
|
-
|
|
122
|
-
## Drawing the winner
|
|
123
|
-
|
|
124
|
-
No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
|
|
125
|
-
is your entry list, in allocation order.
|