@proveanything/smartlinks 1.15.3 → 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.
@@ -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/openapi.yaml CHANGED
@@ -24084,6 +24084,67 @@ components:
24084
24084
  $ref: "#/components/schemas/JsonValue"
24085
24085
  required:
24086
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"
24087
24148
  Proof:
24088
24149
  type: object
24089
24150
  properties:
@@ -24103,9 +24164,16 @@ components:
24103
24164
  type: boolean
24104
24165
  virtual:
24105
24166
  type: boolean
24106
- values:
24167
+ data:
24107
24168
  type: object
24108
- additionalProperties: true
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"
24109
24177
  required:
24110
24178
  - collectionId
24111
24179
  - createdAt
@@ -24118,14 +24186,30 @@ components:
24118
24186
  type: object
24119
24187
  properties:
24120
24188
  values:
24189
+ $ref: "#/components/schemas/ProofValues"
24190
+ data:
24121
24191
  type: object
24122
- additionalProperties: true
24192
+ additionalProperties:
24193
+ $ref: "#/components/schemas/JsonValue"
24194
+ admin:
24195
+ type: object
24196
+ additionalProperties:
24197
+ $ref: "#/components/schemas/JsonValue"
24123
24198
  claimable:
24124
24199
  type: boolean
24125
24200
  virtual:
24126
24201
  type: boolean
24127
24202
  required:
24128
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
24129
24213
  ProofResponse:
24130
24214
  type: object
24131
24215
  additionalProperties: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.15.3",
3
+ "version": "1.15.4",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",