@proveanything/smartlinks 2.0.49 → 2.0.51
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/sequence.d.ts +3 -0
- package/dist/api/sequence.js +3 -0
- package/dist/docs/API_SUMMARY.md +83 -3
- package/dist/docs/ai.md +15 -9
- package/dist/docs/comms.md +17 -1
- package/dist/docs/overview.md +1 -1
- package/dist/docs/sequences.md +6 -0
- package/dist/docs/server-functions.md +86 -3
- package/dist/openapi.yaml +106 -1
- package/dist/testing/index.d.ts +38 -0
- package/dist/testing/index.js +140 -7
- package/dist/types/ai.d.ts +1 -1
- package/dist/types/appManifest.d.ts +89 -2
- package/dist/types/comms.d.ts +22 -0
- package/docs/API_SUMMARY.md +83 -3
- package/docs/ai.md +15 -9
- package/docs/comms.md +17 -1
- package/docs/overview.md +1 -1
- package/docs/sequences.md +6 -0
- package/docs/server-functions.md +86 -3
- package/openapi.yaml +106 -1
- package/package.json +1 -1
- package/scripts/doctor.mjs +3 -0
package/docs/server-functions.md
CHANGED
|
@@ -182,8 +182,9 @@ interface ServerFunctionContext {
|
|
|
182
182
|
appId: string
|
|
183
183
|
|
|
184
184
|
/** SmartLinks SDK, pre-scoped to your declared `authority`. Capabilities cap what it may do.
|
|
185
|
-
* Key resources: `sl.appRecords` (per-app records), `sl.appData`
|
|
186
|
-
* data — URLs, flags,
|
|
185
|
+
* Key resources: `sl.appRecords` (per-app records, incl. race-safe `claim`), `sl.appData`
|
|
186
|
+
* (general/app-wide config + data — URLs, flags, arrays), `sl.counters` (atomic numbering),
|
|
187
|
+
* `sl.tags`, `sl.contacts`, `sl.interactions`, `sl.products`, `sl.attestations`. */
|
|
187
188
|
sl: SmartLinks
|
|
188
189
|
|
|
189
190
|
/** Capability-gated secrets (need `secrets:<name>`). `get(name)` reads this app's own secret on the
|
|
@@ -506,7 +507,11 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
|
|
|
506
507
|
| Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
|
|
507
508
|
| Read/write your app's own data (config, URLs, counters, arrays) | `ctx.sl.appData.get()` / `ctx.sl.appData.set({…})` — collection-scoped by default; `ctx.sl.appData.global.*` (or `{ scope:'global' }`, or declare `dataScope:'global'`) for the app-wide bucket. It's your OWN namespace, so **any** authority may read/write it | `sl:data:read` / `sl:data:write` |
|
|
508
509
|
| Read ANOTHER app's data (as the caller may see it) | `ctx.sl.app('other-app').data.get()` — caller-authority, this collection, public-filtered, read-only | `sl:data:read` |
|
|
509
|
-
| Guarantee a UNIQUE claim (
|
|
510
|
+
| Guarantee a UNIQUE claim (one entry per tag, one-per-user, idempotency key, a number nobody else holds) | `ctx.sl.appRecords.claim({ …, singletonPer, customId })` → `{ record, created, conflict? }` — see below | `sl:records:write` |
|
|
511
|
+
| Hand out the next number (tickets, order numbers) | `ctx.sl.counters.next('tickets')` — atomic, distinct per caller | `sl:data:write` |
|
|
512
|
+
| Check a tapped tag is real | `ctx.sl.tags.resolve(proofId)` → `{ id, claimSetId, code }` or `null` | `sl:tags:read` |
|
|
513
|
+
| Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
|
|
514
|
+
| Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
|
|
510
515
|
|
|
511
516
|
Notes:
|
|
512
517
|
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
@@ -525,6 +530,81 @@ Notes:
|
|
|
525
530
|
|
|
526
531
|
---
|
|
527
532
|
|
|
533
|
+
## Uniqueness, numbering and people — the race-safe primitives
|
|
534
|
+
|
|
535
|
+
A public function is called by many people at once. These primitives make the outcome right no matter
|
|
536
|
+
how the calls interleave — the **database** decides, never a read-then-write in your code.
|
|
537
|
+
|
|
538
|
+
### `appRecords.claim(fields)` — a unique create
|
|
539
|
+
|
|
540
|
+
Like `create`, but the record names a **unique key**, and it's inserted only if that key is free.
|
|
541
|
+
Otherwise you get the record already holding it back, **unchanged** (never updated):
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
const { record, created, conflict } = await ctx.sl.appRecords.claim({
|
|
545
|
+
recordType: 'entry',
|
|
546
|
+
singletonPer: 'proof', proofId: tag.id, // key 1: one live entry per tag
|
|
547
|
+
customId: String(number), // key 2: one record per number, ever
|
|
548
|
+
data: { number },
|
|
549
|
+
})
|
|
550
|
+
// created: true → you got it
|
|
551
|
+
// created: false → conflict 'singleton' (this tag already has an entry: `record` is it)
|
|
552
|
+
// or 'customId' (that number is held by someone else: take another)
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
- **`singletonPer`** — `'collection' | 'product' | 'variant' | 'batch' | 'proof'`, with its anchor id
|
|
556
|
+
(`productId`, `proofId`, …). One **live** record per key; deleting it frees the key.
|
|
557
|
+
- **`customId`** — unique per `(recordType, scopeType, scopeId, sourceSystem)`, case-insensitive, and
|
|
558
|
+
**never reused** (deleted records keep theirs). Right for numbers that must never be handed out twice.
|
|
559
|
+
- Give at least one; with both, the record is created only if both are free.
|
|
560
|
+
- Of any number of concurrent claims on one key, **exactly one** gets `created: true`.
|
|
561
|
+
|
|
562
|
+
### `counters.next(name, { start?, by? })` — the next number
|
|
563
|
+
|
|
564
|
+
An atomic counter for your app in this collection: every caller gets a distinct value, even under a
|
|
565
|
+
burst (one row, incremented in one statement). The first call returns `start` (default 1). Counters only
|
|
566
|
+
move forward and live apart from your config, so no config save can rewind one. `counters.get(name)`
|
|
567
|
+
reads the last value handed out (`null` before the first). Name a new counter to start again from `start`.
|
|
568
|
+
|
|
569
|
+
**Counter + claim = a number nobody else can ever hold.** Take `next()`, then `claim` with
|
|
570
|
+
`customId: String(n)`. If the claim says `conflict: 'customId'`, take `next()` again. A failed or raced
|
|
571
|
+
attempt only leaves a gap — never a duplicate.
|
|
572
|
+
|
|
573
|
+
### `tags.resolve(id)` — is this tap real?
|
|
574
|
+
|
|
575
|
+
A tap hands your app the virtual-proof id `<claimSetId>-<code>`. `resolve` returns
|
|
576
|
+
`{ id, claimSetId, code }` when that code exists in a claim set of **this** collection, else `null`.
|
|
577
|
+
Key records to the returned canonical `id`, and refuse `null` — otherwise anyone can invent ids.
|
|
578
|
+
|
|
579
|
+
### `contacts.upsert(details)` / `contacts.forCaller()`
|
|
580
|
+
|
|
581
|
+
`upsert` finds the contact by `email` or `phone` (one is required) or creates it, and only **fills
|
|
582
|
+
empty fields** — it never overwrites a name or field the contact already has, because a visitor's typed
|
|
583
|
+
details are unverified. It's race-safe: two simultaneous entries with one email end on one contact.
|
|
584
|
+
Returns `{ contactId, created }`. `forCaller()` is the signed-in caller's own contact (`null` when
|
|
585
|
+
anonymous). **Never return contact ids to public callers** — keep them on your records.
|
|
586
|
+
|
|
587
|
+
This is not consent. A ticked "keep me updated" box is evidence you can record (below); subscribing
|
|
588
|
+
someone to marketing is the double opt-in flow (`comms.signup`, see comms.md).
|
|
589
|
+
|
|
590
|
+
### `interactions.record(event)` — "they did X"
|
|
591
|
+
|
|
592
|
+
Records an event of one of **your app's interaction types**: create the type once in admin with
|
|
593
|
+
`SL.interactions.ensureType(collectionId, { appId, key: 'raffle-entry' })`, keep its id in your app
|
|
594
|
+
config, and pass that id. Unknown ids are refused (`INTERACTION_TYPE_NOT_FOUND`). The event runs the
|
|
595
|
+
type's effects and emits `interaction.submitted`, like any submission.
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
await ctx.sl.interactions.record({
|
|
599
|
+
interactionId: config.interactionIds.entered,
|
|
600
|
+
contactId, // optional
|
|
601
|
+
outcome: 'entered',
|
|
602
|
+
metadata: { number, consent: true },
|
|
603
|
+
})
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
528
608
|
## Worked example — a public competition entry
|
|
529
609
|
|
|
530
610
|
```ts
|
|
@@ -756,6 +836,9 @@ const ctx = createFunctionTestContext({
|
|
|
756
836
|
|
|
757
837
|
const res = await submitCompetitionEntry(ctx, { method: 'POST', body: { email: 'a@b.com', answer: '42' } })
|
|
758
838
|
// ctx.sl.appRecords.create(...) throws CapabilityError unless `def` declares sl:records:write
|
|
839
|
+
// With no `sl` impl, records / counters / contacts / interactions run in memory with the platform's
|
|
840
|
+
// uniqueness rules — assert on ctx.memory. Share createFunctionTestMemory() between contexts to
|
|
841
|
+
// simulate concurrent callers; restrict ids with knownTags / knownInteractionTypes.
|
|
759
842
|
```
|
|
760
843
|
|
|
761
844
|
Pass the **`def`** (not a hand-typed capability list) so "tested" can't drift from
|
package/openapi.yaml
CHANGED
|
@@ -19218,16 +19218,113 @@ components:
|
|
|
19218
19218
|
AppDataHandle:
|
|
19219
19219
|
type: object
|
|
19220
19220
|
properties: {}
|
|
19221
|
+
AppRecordClaimResult:
|
|
19222
|
+
type: object
|
|
19223
|
+
properties:
|
|
19224
|
+
record:
|
|
19225
|
+
type: object
|
|
19226
|
+
additionalProperties: true
|
|
19227
|
+
created:
|
|
19228
|
+
type: boolean
|
|
19229
|
+
conflict:
|
|
19230
|
+
type: string
|
|
19231
|
+
enum:
|
|
19232
|
+
- singleton
|
|
19233
|
+
- customId
|
|
19234
|
+
required:
|
|
19235
|
+
- record
|
|
19236
|
+
- created
|
|
19237
|
+
ServerFunctionCounters:
|
|
19238
|
+
type: object
|
|
19239
|
+
properties: {}
|
|
19240
|
+
ResolvedTag:
|
|
19241
|
+
type: object
|
|
19242
|
+
properties:
|
|
19243
|
+
id:
|
|
19244
|
+
type: string
|
|
19245
|
+
claimSetId:
|
|
19246
|
+
type: string
|
|
19247
|
+
code:
|
|
19248
|
+
type: string
|
|
19249
|
+
required:
|
|
19250
|
+
- id
|
|
19251
|
+
- claimSetId
|
|
19252
|
+
- code
|
|
19253
|
+
ContactUpsertDetails:
|
|
19254
|
+
type: object
|
|
19255
|
+
properties:
|
|
19256
|
+
email:
|
|
19257
|
+
type: string
|
|
19258
|
+
phone:
|
|
19259
|
+
type: string
|
|
19260
|
+
firstName:
|
|
19261
|
+
type: string
|
|
19262
|
+
lastName:
|
|
19263
|
+
type: string
|
|
19264
|
+
displayName:
|
|
19265
|
+
type: string
|
|
19266
|
+
company:
|
|
19267
|
+
type: string
|
|
19268
|
+
locale:
|
|
19269
|
+
type: string
|
|
19270
|
+
timezone:
|
|
19271
|
+
type: string
|
|
19272
|
+
customFields:
|
|
19273
|
+
type: object
|
|
19274
|
+
additionalProperties:
|
|
19275
|
+
type: object
|
|
19276
|
+
additionalProperties: true
|
|
19277
|
+
ServerFunctionInteractionEvent:
|
|
19278
|
+
type: object
|
|
19279
|
+
properties:
|
|
19280
|
+
interactionId:
|
|
19281
|
+
type: string
|
|
19282
|
+
contactId:
|
|
19283
|
+
type: string
|
|
19284
|
+
outcome:
|
|
19285
|
+
type: string
|
|
19286
|
+
metadata:
|
|
19287
|
+
type: object
|
|
19288
|
+
additionalProperties: true
|
|
19289
|
+
productId:
|
|
19290
|
+
type: string
|
|
19291
|
+
proofId:
|
|
19292
|
+
type: string
|
|
19293
|
+
variantId:
|
|
19294
|
+
type: string
|
|
19295
|
+
batchId:
|
|
19296
|
+
type: string
|
|
19297
|
+
eventType:
|
|
19298
|
+
type: string
|
|
19299
|
+
required:
|
|
19300
|
+
- interactionId
|
|
19221
19301
|
ServerFunctionSl:
|
|
19222
19302
|
type: object
|
|
19223
19303
|
properties:
|
|
19224
|
-
appRecords:
|
|
19304
|
+
appRecords:
|
|
19305
|
+
type: object
|
|
19306
|
+
additionalProperties: true
|
|
19307
|
+
counters:
|
|
19308
|
+
$ref: "#/components/schemas/ServerFunctionCounters"
|
|
19309
|
+
tags:
|
|
19310
|
+
type: object
|
|
19311
|
+
additionalProperties: true
|
|
19312
|
+
contacts:
|
|
19313
|
+
type: object
|
|
19314
|
+
additionalProperties: true
|
|
19315
|
+
interactions:
|
|
19316
|
+
type: object
|
|
19317
|
+
additionalProperties: true
|
|
19225
19318
|
products: {}
|
|
19226
19319
|
attestations: {}
|
|
19227
19320
|
appData:
|
|
19228
19321
|
$ref: "#/components/schemas/AppDataHandle"
|
|
19229
19322
|
required:
|
|
19230
19323
|
- appRecords
|
|
19324
|
+
- counters
|
|
19325
|
+
- tags
|
|
19326
|
+
- contacts
|
|
19327
|
+
- interactions
|
|
19231
19328
|
- products
|
|
19232
19329
|
- attestations
|
|
19233
19330
|
- appData
|
|
@@ -23658,14 +23755,22 @@ components:
|
|
|
23658
23755
|
properties:
|
|
23659
23756
|
email:
|
|
23660
23757
|
type: string
|
|
23758
|
+
templateId:
|
|
23759
|
+
type: string
|
|
23661
23760
|
topics:
|
|
23662
23761
|
type: array
|
|
23663
23762
|
items:
|
|
23664
23763
|
type: string
|
|
23665
23764
|
firstName:
|
|
23666
23765
|
type: string
|
|
23766
|
+
fields:
|
|
23767
|
+
type: object
|
|
23768
|
+
additionalProperties: true
|
|
23769
|
+
appId:
|
|
23770
|
+
type: string
|
|
23667
23771
|
required:
|
|
23668
23772
|
- email
|
|
23773
|
+
- templateId
|
|
23669
23774
|
CommsSignupResponse:
|
|
23670
23775
|
type: object
|
|
23671
23776
|
properties:
|
package/package.json
CHANGED
package/scripts/doctor.mjs
CHANGED
|
@@ -163,6 +163,9 @@ if (meta.cssBaseline) {
|
|
|
163
163
|
warnings.push(`meta.cssBaseline is "${meta.cssBaseline}" but this SDK ships baseline "${baseline.version}" — the host may serve a different set.`);
|
|
164
164
|
}
|
|
165
165
|
const baseSet = new Set(baseline.classes || []);
|
|
166
|
+
// SDK-defined names that aren't baseline helpers but are legitimately in bundles: the Sidekick
|
|
167
|
+
// screen's scope class (mobile-admin-container.md → Styling).
|
|
168
|
+
for (const c of ['sl-mobile-admin']) baseSet.add(c);
|
|
166
169
|
// Class usage lives in compiled markup strings — present in every bundle regardless
|
|
167
170
|
// of module format, so scan both umd + esm across all surfaces.
|
|
168
171
|
const used = new Set();
|