@proveanything/smartlinks 1.15.2 → 1.15.4
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/ai.js +4 -0
- package/dist/docs/API_SUMMARY.md +72 -3
- package/dist/docs/ai.md +94 -0
- package/dist/docs/app-data-storage.md +2 -0
- package/dist/docs/overview.md +7 -0
- package/dist/docs/proof-product-data-scoping.md +459 -0
- package/dist/openapi.yaml +113 -3
- package/dist/types/ai.d.ts +11 -0
- package/dist/types/product.d.ts +37 -0
- package/dist/types/proof.d.ts +39 -3
- package/docs/API_SUMMARY.md +72 -3
- package/docs/ai.md +94 -0
- package/docs/app-data-storage.md +2 -0
- package/docs/overview.md +7 -0
- package/docs/proof-product-data-scoping.md +459 -0
- package/openapi.yaml +113 -3
- package/package.json +1 -1
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Data Scoping for Products and Proofs
|
|
2
|
+
|
|
3
|
+
Canonical specification for how custom data is stored, who can read it, and who can write it — on both **products** and **proofs**.
|
|
4
|
+
|
|
5
|
+
Applies to the SmartLinks SDK, admin apps, public apps, and any consumer of the platform API.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Design principles
|
|
10
|
+
|
|
11
|
+
1. **Fixed named fields at the root; flexible data in JSON bags.** The root of a product or proof is reserved for typed, first-class columns (name, image, gtin, ownerId, claimable, …). Everything custom lives inside a JSON bag.
|
|
12
|
+
2. **Every bag is defined by *who can write and read* it, and lives at the top level.** Write authority and read visibility are hard, server-enforced boundaries. Rather than hide them under sub-keys, each combination gets its own top-level bag. This matches the existing `product.admin` convention and keeps read-time filtering trivial: the server either returns a bag or it doesn't.
|
|
13
|
+
3. **Public is the default; admin is opt-in.** Anything in `data` is world-readable. Anything private to the business goes in `admin`, which the server never returns to a non-admin caller.
|
|
14
|
+
4. **Same shape everywhere.** Products and proofs share the same rules for the business-writable bags (`data`, `admin`). Proofs add a third top-level bag — `values` — for owner-writable and per-user data, because proofs have an owner and products don't.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The schema
|
|
19
|
+
|
|
20
|
+
### Product
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
product.<rootColumns> // typed columns: name, description, image, gtin, …
|
|
24
|
+
|
|
25
|
+
product.data = { … } // PUBLIC → read: everyone, write: business
|
|
26
|
+
product.admin = { … } // BUSINESS-ONLY → read: business, write: business
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Proof
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
proof.<rootColumns> // typed columns: id, productId, ownerId, claimable, …
|
|
33
|
+
|
|
34
|
+
proof.data = { … } // PUBLIC → read: everyone, write: business
|
|
35
|
+
proof.admin = { … } // BUSINESS-ONLY → read: business, write: business
|
|
36
|
+
|
|
37
|
+
proof.values = { // consumer bag — owner + business-writable
|
|
38
|
+
<anyKey>: …, // PUBLIC → read: everyone,
|
|
39
|
+
// write: business + current owner
|
|
40
|
+
owner: { … }, // OWNER-SCOPED → read: business + current owner,
|
|
41
|
+
// write: business + current owner,
|
|
42
|
+
// transfers with ownership
|
|
43
|
+
personal: { // PER-USER → read/write: only the specific user,
|
|
44
|
+
[userId]: { … }, // not visible to the next owner
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Products intentionally have no `values` bag — there is no owner to write it. If a product ever needs owner-like data, that data belongs on the proof.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Reserved sub-key names
|
|
54
|
+
|
|
55
|
+
Inside `proof.values`, these keys are reserved and MUST NOT be used as free-form public field names:
|
|
56
|
+
|
|
57
|
+
| Bag | Reserved keys |
|
|
58
|
+
| ------------- | --------------------------- |
|
|
59
|
+
| `proof.values`| `owner`, `personal` |
|
|
60
|
+
|
|
61
|
+
`data` and `admin` bags have no reserved sub-keys — everything inside them is flat.
|
|
62
|
+
|
|
63
|
+
The field-config editor and SDK write helpers MUST reject attempts to create top-level public fields in `proof.values` named `owner` or `personal`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Who reads what (visibility matrix)
|
|
68
|
+
|
|
69
|
+
| Location | Public / Anonymous | Authenticated (non-owner) | Current Owner | Business Admin |
|
|
70
|
+
| --------------------------------- | :-----------------: | :------------------------: | :------------: | :--------------: |
|
|
71
|
+
| `product.<root>` | ✅ | ✅ | ✅ | ✅ |
|
|
72
|
+
| `product.data.*` | ✅ | ✅ | ✅ | ✅ |
|
|
73
|
+
| `product.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
74
|
+
| `proof.<root>` | ✅ | ✅ | ✅ | ✅ |
|
|
75
|
+
| `proof.data.*` | ✅ | ✅ | ✅ | ✅ |
|
|
76
|
+
| `proof.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
77
|
+
| `proof.values.<publicKey>` | ✅ | ✅ | ✅ | ✅ |
|
|
78
|
+
| `proof.values.owner.*` | ❌ | ❌ | ✅ | ✅ |
|
|
79
|
+
| `proof.values.personal[me].*` | ❌ | ✅ (own slot only) | ✅ (own slot) | ❌ (see note) |
|
|
80
|
+
| `proof.values.personal[other].*` | ❌ | ❌ | ❌ | ❌ (see note) |
|
|
81
|
+
|
|
82
|
+
> **Note on `personal`:** the default rule is that `personal` slots are readable *only* by the user whose `userId` matches the slot key — not even business admins. If the platform ever needs an admin-visible variant, it should be a separate mechanism, not a relaxation of this rule.
|
|
83
|
+
|
|
84
|
+
## Who writes what (authority matrix)
|
|
85
|
+
|
|
86
|
+
| Location | Public / Anonymous | Authenticated (non-owner) | Current Owner | Business Admin |
|
|
87
|
+
| --------------------------------- | :-----------------: | :------------------------: | :------------: | :--------------: |
|
|
88
|
+
| `product.<root>` | ❌ | ❌ | ❌ | ✅ |
|
|
89
|
+
| `product.data.*` | ❌ | ❌ | ❌ | ✅ |
|
|
90
|
+
| `product.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
91
|
+
| `proof.<root>` (spec) | ❌ | ❌ | ❌ | ✅ |
|
|
92
|
+
| `proof.data.*` | ❌ | ❌ | ❌ | ✅ |
|
|
93
|
+
| `proof.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
94
|
+
| `proof.values.<publicKey>` | ❌ | ❌ | ✅ | ✅ |
|
|
95
|
+
| `proof.values.owner.*` | ❌ | ❌ | ✅ | ✅ |
|
|
96
|
+
| `proof.values.personal[me].*` | ❌ | ✅ (own slot only) | ✅ (own slot) | ❌ |
|
|
97
|
+
|
|
98
|
+
All read and write rules are **server-enforced**. Clients MUST NOT rely on UI hiding alone.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Worked examples
|
|
103
|
+
|
|
104
|
+
### A bicycle
|
|
105
|
+
|
|
106
|
+
```jsonc
|
|
107
|
+
// product (the model)
|
|
108
|
+
{
|
|
109
|
+
"id": "prod_road_2024",
|
|
110
|
+
"name": "Aero Road 2024",
|
|
111
|
+
"image": "…",
|
|
112
|
+
"data": {
|
|
113
|
+
"frameMaterial": "carbon", // public
|
|
114
|
+
"wheelSize": "700c", // public
|
|
115
|
+
"purchaseLink": "https://…" // public
|
|
116
|
+
},
|
|
117
|
+
"admin": {
|
|
118
|
+
"internalSku": "AR24-CARB-M", // business-only
|
|
119
|
+
"unitCost": 1420, // business-only
|
|
120
|
+
"manufacturingSite": "Taichung" // business-only
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// proof (a specific bike)
|
|
125
|
+
{
|
|
126
|
+
"id": "proof_abc123",
|
|
127
|
+
"productId": "prod_road_2024",
|
|
128
|
+
"ownerId": "user_42",
|
|
129
|
+
"claimable": false,
|
|
130
|
+
"data": {
|
|
131
|
+
"serial": "SN-000123", // public spec, business-set
|
|
132
|
+
"colour": "matte black", // public spec, business-set
|
|
133
|
+
"year": 2024 // public spec, business-set
|
|
134
|
+
},
|
|
135
|
+
"admin": {
|
|
136
|
+
"warrantyExpiry": "2027-06-01", // business-only
|
|
137
|
+
"batchId": "B-2024-Q2-17" // business-only
|
|
138
|
+
},
|
|
139
|
+
"values": {
|
|
140
|
+
"nickname": "Midnight", // public, owner-editable
|
|
141
|
+
"displayNote": "For sale — DM me", // public, owner-editable
|
|
142
|
+
"owner": {
|
|
143
|
+
"purchasedAt": "Halfords Oxford", // business + current owner, transfers
|
|
144
|
+
"serviceHistory": [ /* … */ ] // with ownership
|
|
145
|
+
},
|
|
146
|
+
"personal": {
|
|
147
|
+
"user_42": {
|
|
148
|
+
"combinationLock": "17-04-88", // only user_42 ever sees this
|
|
149
|
+
"privateNotes": "…"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Liquid template ergonomics
|
|
157
|
+
|
|
158
|
+
```liquid
|
|
159
|
+
{{ product.name }} {# root column #}
|
|
160
|
+
{{ product.data.purchaseLink }} {# public #}
|
|
161
|
+
{{ product.admin.internalSku }} {# admin surfaces only #}
|
|
162
|
+
|
|
163
|
+
{{ proof.data.colour }} {# public spec #}
|
|
164
|
+
{{ proof.admin.warrantyExpiry }} {# admin surfaces only #}
|
|
165
|
+
{{ proof.values.nickname }} {# public, owner-editable #}
|
|
166
|
+
{{ proof.values.owner.purchasedAt }} {# owner-scoped #}
|
|
167
|
+
{{ proof.values.personal[currentUserId].combinationLock }}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Legacy compatibility
|
|
173
|
+
|
|
174
|
+
The new shape is **strictly additive**. No existing data needs to move.
|
|
175
|
+
|
|
176
|
+
- **`product.data.<key>`** — legacy flat keys continue to mean "public". No migration.
|
|
177
|
+
- **`product.admin.<key>`** — already exists today; formalised here as the canonical private-business bag.
|
|
178
|
+
- **`proof.values.<key>`** — legacy flat keys continue to mean "public, owner-editable". No migration.
|
|
179
|
+
- **`proof.data`** — new bucket, added alongside the existing `proof.values`, for public business-owned spec that isn't consumer-editable.
|
|
180
|
+
- **`proof.admin`** — new bucket, mirroring `product.admin`.
|
|
181
|
+
- **`owner` / `personal`** — reserved going forward inside `proof.values`. Any pre-existing custom field literally named `owner` or `personal` should be renamed on a case-by-case basis. New writes MUST reject these as free-form key names.
|
|
182
|
+
|
|
183
|
+
Legacy apps that only read the flat public shape continue to work unchanged. New apps that need scoped data opt in by reading and writing the new bags and reserved sub-keys.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## SDK guidance
|
|
188
|
+
|
|
189
|
+
- **Reads** return whatever bags the caller is entitled to. Callers MUST NOT assume `admin`, `values.owner`, or `values.personal` are present.
|
|
190
|
+
- **Writes** to `admin` bags, `proof.data`, and `proof.values.owner` require the appropriate role; the server rejects unauthorised writes with a permission error rather than silently dropping keys.
|
|
191
|
+
- **Field-config editors** (custom-fields UIs on collection settings) should:
|
|
192
|
+
- Offer a **scope** dropdown per field. Products: `Public` (→ `product.data`), `Admin` (→ `product.admin`). Proofs: `Public` (→ `proof.values.<key>` — owner-editable), `Owner` (→ `proof.values.owner`), `Personal` (→ `proof.values.personal[userId]`), `Admin` (→ `proof.admin`).
|
|
193
|
+
- Reject reserved names (`owner`, `personal`) as free-form keys in `proof.values`.
|
|
194
|
+
- Persist enough metadata for renderers to know which bag a field lives in and to gate its display by the current caller's role.
|
|
195
|
+
- **Server** performs both read-scoping (strip bags the caller can't see) and write authorisation (reject writes the caller isn't allowed to make). The client is a convenience layer, never the source of truth for either.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Summary
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
product.data = { … } // public business
|
|
203
|
+
product.admin = { … } // admin-only business
|
|
204
|
+
|
|
205
|
+
proof.data = { … } // public business (spec)
|
|
206
|
+
proof.admin = { … } // admin-only business
|
|
207
|
+
proof.values = { …public, owner?, personal? } // consumer
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Reserved sub-keys: **`owner`** and **`personal`** inside `proof.values`. Everything else in every bag is a free-form key.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Field-definition schemas (collection settings)
|
|
215
|
+
|
|
216
|
+
The custom-field definitions that drive the editors above are stored on the
|
|
217
|
+
**collection** in named settings groups. Both are read/written via the standard
|
|
218
|
+
settings API:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// Read (admin: true required for admin-scoped fields to be returned)
|
|
222
|
+
const productFields = await SL.collection.getSettings(collectionId, 'productFields', true);
|
|
223
|
+
const proofFields = await SL.collection.getSettings(collectionId, 'proofFields', true);
|
|
224
|
+
|
|
225
|
+
// Write
|
|
226
|
+
await SL.collection.updateSettings(collectionId, 'productFields', productFieldsConfig);
|
|
227
|
+
await SL.collection.updateSettings(collectionId, 'proofFields', proofFieldsConfig);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Third-party apps (CRM, warranty, provenance, etc.) that render a product or
|
|
231
|
+
proof for a user should fetch these once per collection to know which
|
|
232
|
+
configurable fields exist, their labels, types, and — crucially — which bag to
|
|
233
|
+
read the value from.
|
|
234
|
+
|
|
235
|
+
### Settings group names
|
|
236
|
+
|
|
237
|
+
| Group key | Shape | Applies to |
|
|
238
|
+
| ----------------- | --------------------- | ---------------------------------------------------------------- |
|
|
239
|
+
| `productFields` | `ProductFieldsConfig`| Custom fields on `product.data` / `product.admin` |
|
|
240
|
+
| `proofFields` | `ProofFieldsConfig` | Custom fields on `proof.values` / `proof.data` / `proof.admin`|
|
|
241
|
+
|
|
242
|
+
### Shared field-def shape
|
|
243
|
+
|
|
244
|
+
Product and proof field defs share the same base shape. The only difference is
|
|
245
|
+
the allowed `scope` values.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
type FieldType =
|
|
249
|
+
| 'text'
|
|
250
|
+
| 'textarea'
|
|
251
|
+
| 'number'
|
|
252
|
+
| 'date'
|
|
253
|
+
| 'select'
|
|
254
|
+
| 'switch'
|
|
255
|
+
| 'url';
|
|
256
|
+
|
|
257
|
+
interface FieldOption {
|
|
258
|
+
value: string;
|
|
259
|
+
label: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface FieldDef {
|
|
263
|
+
/** Stable key. Written into the resolved bag at this name. */
|
|
264
|
+
key: string;
|
|
265
|
+
/** Human label for editors and read-only renderers. */
|
|
266
|
+
label: string;
|
|
267
|
+
type: FieldType;
|
|
268
|
+
required?: boolean;
|
|
269
|
+
placeholder?: string;
|
|
270
|
+
help?: string;
|
|
271
|
+
/** Required when type === 'select'. */
|
|
272
|
+
options?: FieldOption[];
|
|
273
|
+
/** Default value applied when creating a new record. */
|
|
274
|
+
defaultValue?: unknown;
|
|
275
|
+
/** Show as a column in the default items table. */
|
|
276
|
+
showInTable?: boolean;
|
|
277
|
+
/** Show on the inline detail / edit view. */
|
|
278
|
+
showInDetail?: boolean;
|
|
279
|
+
/**
|
|
280
|
+
* Storage scope. When omitted the field is legacy public and written flat
|
|
281
|
+
* (product.data.<key> or proof.values.<key>). New fields opt into a scope.
|
|
282
|
+
*/
|
|
283
|
+
scope?: FieldScope;
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `productFields` — `ProductFieldsConfig`
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
type ProductFieldScope = 'public' | 'admin';
|
|
291
|
+
|
|
292
|
+
type ProductFieldDef = FieldDef & { scope?: ProductFieldScope };
|
|
293
|
+
|
|
294
|
+
interface ProductFieldsConfig {
|
|
295
|
+
fields: ProductFieldDef[];
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Value resolution** for a given `ProductFieldDef` `f`:
|
|
300
|
+
|
|
301
|
+
| `f.scope` | Read from |
|
|
302
|
+
| ---------------------- | ---------------------------------------------- |
|
|
303
|
+
| `'public'` / omitted | `product.data[f.key]` |
|
|
304
|
+
| `'admin'` | `product.admin?.[f.key]` (admin only) |
|
|
305
|
+
|
|
306
|
+
### `proofFields` — `ProofFieldsConfig`
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
type ProofFieldScope = 'public' | 'owner' | 'personal' | 'admin';
|
|
310
|
+
|
|
311
|
+
type ProofFieldDef = FieldDef & { scope?: ProofFieldScope };
|
|
312
|
+
|
|
313
|
+
interface ProofFieldsConfig {
|
|
314
|
+
fields: ProofFieldDef[];
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Value resolution** for a given `ProofFieldDef` `f`, current user id `uid`:
|
|
319
|
+
|
|
320
|
+
| `f.scope` | Read from |
|
|
321
|
+
| ---------------------- | -------------------------------------------------- |
|
|
322
|
+
| `'public'` / omitted | `proof.values[f.key]` |
|
|
323
|
+
| `'owner'` | `proof.values.owner?.[f.key]` |
|
|
324
|
+
| `'personal'` | `proof.values.personal?.[uid]?.[f.key]` |
|
|
325
|
+
| `'admin'` | `proof.admin?.[f.key]` |
|
|
326
|
+
|
|
327
|
+
If the caller isn't entitled to the scope, the server strips the bag; the
|
|
328
|
+
field def still exists but the value comes back `undefined`. Clients should
|
|
329
|
+
treat that as "not visible to me" rather than "unset".
|
|
330
|
+
|
|
331
|
+
> **Note:** there is currently no `scope` value that maps to `proof.data`
|
|
332
|
+
> (public business spec). If/when the editor UX wants to distinguish
|
|
333
|
+
> "public, business-only-writable" from "public, owner-editable" (the current
|
|
334
|
+
> unscoped `proof.values.<key>`), add a `'spec'` scope that resolves to
|
|
335
|
+
> `proof.data[f.key]`. Until then, treat `proof.data` as hand-authored /
|
|
336
|
+
> server-set and out of scope for the collection-settings field editor.
|
|
337
|
+
|
|
338
|
+
### JSON Schema — `productFields`
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
343
|
+
"$id": "https://smartlinks.app/schemas/collection-settings/productFields.json",
|
|
344
|
+
"title": "Collection Settings: productFields",
|
|
345
|
+
"type": "object",
|
|
346
|
+
"additionalProperties": false,
|
|
347
|
+
"required": ["fields"],
|
|
348
|
+
"properties": {
|
|
349
|
+
"fields": {
|
|
350
|
+
"type": "array",
|
|
351
|
+
"items": { "$ref": "#/$defs/productFieldDef" }
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
"$defs": {
|
|
355
|
+
"productFieldDef": {
|
|
356
|
+
"type": "object",
|
|
357
|
+
"additionalProperties": false,
|
|
358
|
+
"required": ["key", "label", "type"],
|
|
359
|
+
"properties": {
|
|
360
|
+
"key": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
361
|
+
"label": { "type": "string" },
|
|
362
|
+
"type": { "enum": ["text","textarea","number","date","select","switch","url"] },
|
|
363
|
+
"required": { "type": "boolean" },
|
|
364
|
+
"placeholder": { "type": "string" },
|
|
365
|
+
"help": { "type": "string" },
|
|
366
|
+
"options": {
|
|
367
|
+
"type": "array",
|
|
368
|
+
"items": {
|
|
369
|
+
"type": "object",
|
|
370
|
+
"additionalProperties": false,
|
|
371
|
+
"required": ["value", "label"],
|
|
372
|
+
"properties": {
|
|
373
|
+
"value": { "type": "string" },
|
|
374
|
+
"label": { "type": "string" }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
"defaultValue": {},
|
|
379
|
+
"showInTable": { "type": "boolean" },
|
|
380
|
+
"showInDetail": { "type": "boolean" },
|
|
381
|
+
"scope": { "enum": ["public", "admin"] }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### JSON Schema — `proofFields`
|
|
389
|
+
|
|
390
|
+
```json
|
|
391
|
+
{
|
|
392
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
393
|
+
"$id": "https://smartlinks.app/schemas/collection-settings/proofFields.json",
|
|
394
|
+
"title": "Collection Settings: proofFields",
|
|
395
|
+
"type": "object",
|
|
396
|
+
"additionalProperties": false,
|
|
397
|
+
"required": ["fields"],
|
|
398
|
+
"properties": {
|
|
399
|
+
"fields": {
|
|
400
|
+
"type": "array",
|
|
401
|
+
"items": { "$ref": "#/$defs/proofFieldDef" }
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
"$defs": {
|
|
405
|
+
"proofFieldDef": {
|
|
406
|
+
"type": "object",
|
|
407
|
+
"additionalProperties": false,
|
|
408
|
+
"required": ["key", "label", "type"],
|
|
409
|
+
"properties": {
|
|
410
|
+
"key": { "type": "string", "pattern": "^(?!owner$|personal$)[A-Za-z_][A-Za-z0-9_]*$" },
|
|
411
|
+
"label": { "type": "string" },
|
|
412
|
+
"type": { "enum": ["text","textarea","number","date","select","switch","url"] },
|
|
413
|
+
"required": { "type": "boolean" },
|
|
414
|
+
"placeholder": { "type": "string" },
|
|
415
|
+
"help": { "type": "string" },
|
|
416
|
+
"options": {
|
|
417
|
+
"type": "array",
|
|
418
|
+
"items": {
|
|
419
|
+
"type": "object",
|
|
420
|
+
"additionalProperties": false,
|
|
421
|
+
"required": ["value", "label"],
|
|
422
|
+
"properties": {
|
|
423
|
+
"value": { "type": "string" },
|
|
424
|
+
"label": { "type": "string" }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
"defaultValue": {},
|
|
429
|
+
"showInTable": { "type": "boolean" },
|
|
430
|
+
"showInDetail": { "type": "boolean" },
|
|
431
|
+
"scope": { "enum": ["public", "owner", "personal", "admin"] }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Worked example — CRM app resolving a proof
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const { fields } = await SL.collection.getSettings(collectionId, 'proofFields');
|
|
442
|
+
const uid = (await SL.auth.getAccount())?.id;
|
|
443
|
+
|
|
444
|
+
for (const f of fields) {
|
|
445
|
+
const value =
|
|
446
|
+
!f.scope || f.scope === 'public' ? proof.values?.[f.key] :
|
|
447
|
+
f.scope === 'owner' ? proof.values?.owner?.[f.key] :
|
|
448
|
+
f.scope === 'personal' ? proof.values?.personal?.[uid!]?.[f.key] :
|
|
449
|
+
f.scope === 'admin' ? proof.admin?.[f.key] :
|
|
450
|
+
undefined;
|
|
451
|
+
|
|
452
|
+
render({ label: f.label, type: f.type, value });
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The same loop, swapping `proofFields` → `productFields` and the resolution
|
|
457
|
+
table for the product one (`public` → `product.data`, `admin` → `product.admin`),
|
|
458
|
+
gives a CRM app a complete configurable read of a product. No app-specific
|
|
459
|
+
knowledge required beyond "fetch the field defs from the collection".
|
package/dist/openapi.yaml
CHANGED
|
@@ -13964,6 +13964,16 @@ components:
|
|
|
13964
13964
|
parameters:
|
|
13965
13965
|
type: object
|
|
13966
13966
|
additionalProperties: true
|
|
13967
|
+
output_schema:
|
|
13968
|
+
type: object
|
|
13969
|
+
additionalProperties: true
|
|
13970
|
+
allowed_callers:
|
|
13971
|
+
type: array
|
|
13972
|
+
items:
|
|
13973
|
+
type: string
|
|
13974
|
+
enum:
|
|
13975
|
+
- assistant
|
|
13976
|
+
- programmatic
|
|
13967
13977
|
required:
|
|
13968
13978
|
- type
|
|
13969
13979
|
ResponseInputItem:
|
|
@@ -14052,6 +14062,22 @@ components:
|
|
|
14052
14062
|
type: string
|
|
14053
14063
|
prompt_cache_key:
|
|
14054
14064
|
type: string
|
|
14065
|
+
multi_agent:
|
|
14066
|
+
type: object
|
|
14067
|
+
additionalProperties: true
|
|
14068
|
+
enabled:
|
|
14069
|
+
type: boolean
|
|
14070
|
+
max_concurrent_subagents:
|
|
14071
|
+
type: number
|
|
14072
|
+
service_tier:
|
|
14073
|
+
type: string
|
|
14074
|
+
enum:
|
|
14075
|
+
- auto
|
|
14076
|
+
- standard
|
|
14077
|
+
- flex
|
|
14078
|
+
- priority
|
|
14079
|
+
required:
|
|
14080
|
+
- enabled
|
|
14055
14081
|
ResponsesResult:
|
|
14056
14082
|
type: object
|
|
14057
14083
|
properties:
|
|
@@ -24058,6 +24084,67 @@ components:
|
|
|
24058
24084
|
$ref: "#/components/schemas/JsonValue"
|
|
24059
24085
|
required:
|
|
24060
24086
|
- claimId
|
|
24087
|
+
ScopedFieldOption:
|
|
24088
|
+
type: object
|
|
24089
|
+
properties:
|
|
24090
|
+
value:
|
|
24091
|
+
type: string
|
|
24092
|
+
label:
|
|
24093
|
+
type: string
|
|
24094
|
+
required:
|
|
24095
|
+
- value
|
|
24096
|
+
- label
|
|
24097
|
+
ScopedFieldDef:
|
|
24098
|
+
type: object
|
|
24099
|
+
properties:
|
|
24100
|
+
key:
|
|
24101
|
+
type: string
|
|
24102
|
+
label:
|
|
24103
|
+
type: string
|
|
24104
|
+
type:
|
|
24105
|
+
$ref: "#/components/schemas/ScopedFieldType"
|
|
24106
|
+
required:
|
|
24107
|
+
type: boolean
|
|
24108
|
+
placeholder:
|
|
24109
|
+
type: string
|
|
24110
|
+
help:
|
|
24111
|
+
type: string
|
|
24112
|
+
options:
|
|
24113
|
+
type: array
|
|
24114
|
+
items:
|
|
24115
|
+
$ref: "#/components/schemas/ScopedFieldOption"
|
|
24116
|
+
defaultValue:
|
|
24117
|
+
$ref: "#/components/schemas/JsonValue"
|
|
24118
|
+
showInTable:
|
|
24119
|
+
type: boolean
|
|
24120
|
+
showInDetail:
|
|
24121
|
+
type: boolean
|
|
24122
|
+
required:
|
|
24123
|
+
- key
|
|
24124
|
+
- label
|
|
24125
|
+
- type
|
|
24126
|
+
ProductFieldsConfig:
|
|
24127
|
+
type: object
|
|
24128
|
+
properties:
|
|
24129
|
+
fields:
|
|
24130
|
+
type: array
|
|
24131
|
+
items:
|
|
24132
|
+
$ref: "#/components/schemas/ProductFieldDef"
|
|
24133
|
+
required:
|
|
24134
|
+
- fields
|
|
24135
|
+
ProofValues:
|
|
24136
|
+
type: object
|
|
24137
|
+
properties:
|
|
24138
|
+
owner:
|
|
24139
|
+
type: object
|
|
24140
|
+
additionalProperties:
|
|
24141
|
+
$ref: "#/components/schemas/JsonValue"
|
|
24142
|
+
personal:
|
|
24143
|
+
type: object
|
|
24144
|
+
additionalProperties:
|
|
24145
|
+
type: object
|
|
24146
|
+
additionalProperties:
|
|
24147
|
+
$ref: "#/components/schemas/JsonValue"
|
|
24061
24148
|
Proof:
|
|
24062
24149
|
type: object
|
|
24063
24150
|
properties:
|
|
@@ -24077,9 +24164,16 @@ components:
|
|
|
24077
24164
|
type: boolean
|
|
24078
24165
|
virtual:
|
|
24079
24166
|
type: boolean
|
|
24080
|
-
|
|
24167
|
+
data:
|
|
24081
24168
|
type: object
|
|
24082
|
-
additionalProperties:
|
|
24169
|
+
additionalProperties:
|
|
24170
|
+
$ref: "#/components/schemas/JsonValue"
|
|
24171
|
+
admin:
|
|
24172
|
+
type: object
|
|
24173
|
+
additionalProperties:
|
|
24174
|
+
$ref: "#/components/schemas/JsonValue"
|
|
24175
|
+
values:
|
|
24176
|
+
$ref: "#/components/schemas/ProofValues"
|
|
24083
24177
|
required:
|
|
24084
24178
|
- collectionId
|
|
24085
24179
|
- createdAt
|
|
@@ -24092,14 +24186,30 @@ components:
|
|
|
24092
24186
|
type: object
|
|
24093
24187
|
properties:
|
|
24094
24188
|
values:
|
|
24189
|
+
$ref: "#/components/schemas/ProofValues"
|
|
24190
|
+
data:
|
|
24095
24191
|
type: object
|
|
24096
|
-
additionalProperties:
|
|
24192
|
+
additionalProperties:
|
|
24193
|
+
$ref: "#/components/schemas/JsonValue"
|
|
24194
|
+
admin:
|
|
24195
|
+
type: object
|
|
24196
|
+
additionalProperties:
|
|
24197
|
+
$ref: "#/components/schemas/JsonValue"
|
|
24097
24198
|
claimable:
|
|
24098
24199
|
type: boolean
|
|
24099
24200
|
virtual:
|
|
24100
24201
|
type: boolean
|
|
24101
24202
|
required:
|
|
24102
24203
|
- values
|
|
24204
|
+
ProofFieldsConfig:
|
|
24205
|
+
type: object
|
|
24206
|
+
properties:
|
|
24207
|
+
fields:
|
|
24208
|
+
type: array
|
|
24209
|
+
items:
|
|
24210
|
+
$ref: "#/components/schemas/ProofFieldDef"
|
|
24211
|
+
required:
|
|
24212
|
+
- fields
|
|
24103
24213
|
ProofResponse:
|
|
24104
24214
|
type: object
|
|
24105
24215
|
additionalProperties: true
|
package/dist/types/ai.d.ts
CHANGED
|
@@ -56,6 +56,10 @@ export interface ResponseTool {
|
|
|
56
56
|
name?: string;
|
|
57
57
|
description?: string;
|
|
58
58
|
parameters?: Record<string, any>;
|
|
59
|
+
/** Expected shape of this tool's output, used by Programmatic Tool Calling. */
|
|
60
|
+
output_schema?: Record<string, any>;
|
|
61
|
+
/** Restricts which callers may invoke this tool (e.g. `['programmatic']` for Programmatic Tool Calling). */
|
|
62
|
+
allowed_callers?: Array<'assistant' | 'programmatic'>;
|
|
59
63
|
[key: string]: any;
|
|
60
64
|
}
|
|
61
65
|
/** Structured input item accepted by the Responses API. */
|
|
@@ -98,6 +102,13 @@ export interface ResponsesRequest {
|
|
|
98
102
|
text?: Record<string, any>;
|
|
99
103
|
metadata?: Record<string, string>;
|
|
100
104
|
prompt_cache_key?: string;
|
|
105
|
+
/** Enables multi-agent (subagent) orchestration. Not supported together with `stream: true`. */
|
|
106
|
+
multi_agent?: {
|
|
107
|
+
enabled: boolean;
|
|
108
|
+
max_concurrent_subagents?: number;
|
|
109
|
+
};
|
|
110
|
+
/** `'flex'` is ~50% cheaper at Batch-API rates but slower; reserve for non-interactive/background work. */
|
|
111
|
+
service_tier?: 'auto' | 'standard' | 'flex' | 'priority';
|
|
101
112
|
}
|
|
102
113
|
/** Response from the Responses API. */
|
|
103
114
|
export interface ResponsesResult {
|
package/dist/types/product.d.ts
CHANGED
|
@@ -85,7 +85,9 @@ export interface Product extends ProductKey {
|
|
|
85
85
|
facets?: ProductFacetMap;
|
|
86
86
|
/** Tag keys where value is true */
|
|
87
87
|
tags?: Record<string, boolean>;
|
|
88
|
+
/** Public custom data — readable by everyone, writable by business admins only. */
|
|
88
89
|
data?: Record<string, JsonValue>;
|
|
90
|
+
/** Business-only custom data — stripped from public/non-admin reads. See docs/proof-product-data-scoping.md. */
|
|
89
91
|
admin?: Record<string, JsonValue>;
|
|
90
92
|
extra?: Record<string, JsonValue>;
|
|
91
93
|
validCollections?: string[] | null;
|
|
@@ -161,3 +163,38 @@ export type ProductClaimCreateRequestBody = Omit<ProductClaimCreateInput, 'colle
|
|
|
161
163
|
export type ProductResponse = Product;
|
|
162
164
|
export type ProductCreateRequest = ProductWriteInput;
|
|
163
165
|
export type ProductUpdateRequest = Partial<Omit<ProductWriteInput, 'id'>>;
|
|
166
|
+
export type ScopedFieldType = 'text' | 'textarea' | 'number' | 'date' | 'select' | 'switch' | 'url';
|
|
167
|
+
export interface ScopedFieldOption {
|
|
168
|
+
value: string;
|
|
169
|
+
label: string;
|
|
170
|
+
}
|
|
171
|
+
export interface ScopedFieldDef {
|
|
172
|
+
/** Stable key. Written into the resolved bag at this name. */
|
|
173
|
+
key: string;
|
|
174
|
+
/** Human label for editors and read-only renderers. */
|
|
175
|
+
label: string;
|
|
176
|
+
type: ScopedFieldType;
|
|
177
|
+
required?: boolean;
|
|
178
|
+
placeholder?: string;
|
|
179
|
+
help?: string;
|
|
180
|
+
/** Required when type === 'select'. */
|
|
181
|
+
options?: ScopedFieldOption[];
|
|
182
|
+
/** Default value applied when creating a new record. */
|
|
183
|
+
defaultValue?: JsonValue;
|
|
184
|
+
/** Show as a column in the default items table. */
|
|
185
|
+
showInTable?: boolean;
|
|
186
|
+
/** Show on the inline detail / edit view. */
|
|
187
|
+
showInDetail?: boolean;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* `'public'` (default, omitted) reads/writes `product.data[key]`.
|
|
191
|
+
* `'admin'` reads/writes `product.admin[key]` (admin only).
|
|
192
|
+
*/
|
|
193
|
+
export type ProductFieldScope = 'public' | 'admin';
|
|
194
|
+
export type ProductFieldDef = ScopedFieldDef & {
|
|
195
|
+
scope?: ProductFieldScope;
|
|
196
|
+
};
|
|
197
|
+
/** Shape of the `productFields` collection settings group. */
|
|
198
|
+
export interface ProductFieldsConfig {
|
|
199
|
+
fields: ProductFieldDef[];
|
|
200
|
+
}
|