@gemmein/sdk 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,11 +80,28 @@ const one = await tasks.get("rec_abc123")
80
80
  await tasks.update("rec_abc123", { done: true })
81
81
  await tasks.delete("rec_abc123")
82
82
 
83
- // Images/files: presigned upload, returns a CDN URL to store in a record
83
+ // Images/files: presigned upload, returns a REFERENCE to store in a record
84
84
  const file = await tasks.upload(imageBlob, { name: "avatar.png" })
85
- // { id, url, contentType, sizeBytes }
85
+ // { id, ref, contentType, sizeBytes } ref looks like "file:01K…"
86
+ await tasks.create({ title: "Profile", avatar: file.ref })
87
+
88
+ // To show or download it — one call, whatever the collection:
89
+ const { url } = await g.files.link(record.avatar)
90
+ // public collection → a permanent, cacheable URL
91
+ // anything else → a signed URL valid for a couple of minutes, re-checked
92
+ // against who you are and what you still hold
86
93
  ```
87
94
 
95
+ Store the reference, never the URL. A reference doesn't expire and grants
96
+ nothing on its own; `link()` is where authorization happens, every time. That
97
+ also means the same app code keeps working if a collection later changes from
98
+ public to private.
99
+
100
+ **The bound, honestly:** revoking access stops Gemmein issuing new links
101
+ immediately. A link already in someone's hands works until it expires. Nothing
102
+ can take back a file they already downloaded — this is controlled delivery, not
103
+ DRM.
104
+
88
105
  ### Collection rules
89
106
 
90
107
  Each collection has one rule, set in the dashboard. The server enforces it — your app never implements authorization:
package/REFERENCE.md CHANGED
@@ -91,7 +91,40 @@ certain the collection exists yet.
91
91
  | `get` | `(id: string, options?: { expand?: string[] })` | `Promise<GemmeinRecord<T>>` |
92
92
  | `update` | `(id: string, data: Partial<T> \| { field: { increment\|decrement, floor?, ceiling? } }, options?: { ifVersion?: number; published?: boolean })` | `Promise<GemmeinRecord<T>>` |
93
93
  | `delete` | `(id: string)` | `Promise<void>` |
94
- | `upload` | `(file: Blob \| File, options?: { name?: string })` | `Promise<{ id: string; url: string; contentType: string; sizeBytes: number }>` |
94
+ | `upload` | `(file: Blob \| File, options?: { name?: string })` | `Promise<{ id: string; ref: FileRef; contentType: string; sizeBytes: number }>` |
95
+
96
+ ### Files
97
+
98
+ `upload()` returns a **reference** (`file:01K…`), not a URL. Store the reference
99
+ in your record — it never expires and grants nothing on its own.
100
+
101
+ | Method | Signature | Returns |
102
+ |--------|-----------|---------|
103
+ | `g.files.link` | `(ref: FileRef \| string, options?: { intent?: "inline" \| "download" })` | `Promise<{ ref, url, expiresAt?, contentType, sizeBytes?, name? }>` |
104
+
105
+ ```ts
106
+ const { ref } = await g.collection("films").upload(file)
107
+ await g.collection("films").create({ title, poster: ref })
108
+
109
+ // later, to render or download:
110
+ const { url } = await g.files.link(record.poster)
111
+ ```
112
+
113
+ One call for every file. A collection anyone can read gives a permanent,
114
+ cacheable URL; anything else gives one that expires in a couple of minutes and
115
+ is re-checked against who you are, what the collection's rule says, whether the
116
+ file is yours, and any entitlement the collection requires. That is why the
117
+ same code keeps working when a collection changes from public to private.
118
+
119
+ **Don't store what `link()` returns.** Store the reference and call it again.
120
+
121
+ `403 entitlement_required` from `link()` names the key the customer is
122
+ missing. `404` means the file isn't theirs, or isn't there.
123
+
124
+ **The bound, honestly:** revoking access stops Gemmein issuing *new* links
125
+ immediately; a link already issued works until it expires, and a download that
126
+ started before expiry may finish after it. Controlled delivery, not DRM —
127
+ nothing takes back a file someone already downloaded.
95
128
 
96
129
  ```ts
97
130
  type GemmeinRecord<T> = {
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.AuthClient = exports.Gemmein = exports.BrowserTokenStore = exports.MemoryTokenStore = exports.GemmeinError = void 0;
3
+ exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.FilesClient = exports.PurchasesClient = exports.AuthClient = exports.Gemmein = exports.BrowserTokenStore = exports.MemoryTokenStore = exports.GemmeinError = void 0;
4
4
  exports.gemmein = gemmein;
5
5
  exports.gemmeinServer = gemmeinServer;
6
6
  class GemmeinError extends Error {
@@ -107,7 +107,9 @@ class Gemmein {
107
107
  this.storage = new StorageClient(config);
108
108
  this.subscriptions = new SubscriptionsClient(config);
109
109
  this.payments = new PaymentsClient(config);
110
+ this.purchases = new PurchasesClient(config);
110
111
  this.account = new AccountClient(config);
112
+ this.files = new FilesClient(config);
111
113
  }
112
114
  /**
113
115
  * Your app's data — `g.collection<{ title: string }>("notes")`. The
@@ -200,6 +202,61 @@ exports.AuthClient = AuthClient;
200
202
  * dashboard. The client surface is deliberately read-plus-checkout only:
201
203
  * there is no client write path to plan or status, by design.
202
204
  */
205
+ /**
206
+ * The signed-in customer's own purchase history, read from Gemmein's
207
+ * immutable commercial record rather than from your data.
208
+ *
209
+ * This exists so a buyer keeps proof of what they paid for even when you keep
210
+ * no receipts collection at all — and it stays true if you later rename or
211
+ * delete one, because financial truth does not live in application data.
212
+ *
213
+ * Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
214
+ * as the payment provider reported them. Formatting is yours; rounding here
215
+ * would quietly lose money.
216
+ */
217
+ class PurchasesClient {
218
+ constructor(config) {
219
+ this.config = config;
220
+ }
221
+ /**
222
+ * Everything this customer has paid for, newest first, with refunds already
223
+ * applied — `status` is "paid", "part_refunded" or "refunded", and
224
+ * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
225
+ * nobody is signed in.
226
+ */
227
+ async mine() {
228
+ const result = (await runtimeRequest(this.config, "/auth/purchases"));
229
+ return result.purchases;
230
+ }
231
+ }
232
+ exports.PurchasesClient = PurchasesClient;
233
+ /**
234
+ * Turn a stored file reference into a URL you can actually use.
235
+ *
236
+ * const { url } = await g.files.link(record.poster)
237
+ * img.src = url
238
+ *
239
+ * One call for every file, whichever collection it lives in. If the collection
240
+ * is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
241
+ * get one that works for a couple of minutes and is re-checked against who you
242
+ * are, what the collection's rule says, whether the file is yours, and whether
243
+ * you still hold whatever the collection requires.
244
+ *
245
+ * That is why the same app code keeps working when a collection later changes
246
+ * from public to private — and why a refund actually takes a download away.
247
+ *
248
+ * Don't store what this returns. Store the reference and call this again.
249
+ */
250
+ class FilesClient {
251
+ constructor(config) {
252
+ this.config = config;
253
+ }
254
+ async link(ref, options = {}) {
255
+ const query = options.intent === "download" ? "?intent=download" : "";
256
+ return (await runtimeRequest(this.config, `/files/${encodeURIComponent(String(ref))}/link${query}`));
257
+ }
258
+ }
259
+ exports.FilesClient = FilesClient;
203
260
  class SubscriptionsClient {
204
261
  constructor(config) {
205
262
  this.config = config;
@@ -408,6 +465,22 @@ class CollectionClient {
408
465
  async delete(id) {
409
466
  await this.request(`/${encodeURIComponent(id)}`, { method: "DELETE" });
410
467
  }
468
+ /**
469
+ * Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
470
+ *
471
+ * Store the reference. It never expires, it is safe to log and export, and
472
+ * it grants nothing on its own. To show or download the file, call
473
+ * `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
474
+ * what makes revoking access actually take a download away.
475
+ *
476
+ * const { ref } = await g.collection("films").upload(file)
477
+ * await g.collection("films").create({ title, poster: ref })
478
+ * // later, to render:
479
+ * const { url } = await g.files.link(record.poster)
480
+ *
481
+ * There is deliberately no `url` here. A URL that outlives a refund is the
482
+ * bug this replaced.
483
+ */
411
484
  async upload(file, options) {
412
485
  const name = options?.name ?? (file instanceof File ? file.name : "upload");
413
486
  // Step 1: Get presigned upload URL
package/dist/index.d.cts CHANGED
@@ -143,7 +143,9 @@ export declare class Gemmein {
143
143
  readonly storage: StorageClient;
144
144
  readonly subscriptions: SubscriptionsClient;
145
145
  readonly payments: PaymentsClient;
146
+ readonly purchases: PurchasesClient;
146
147
  readonly account: AccountClient;
148
+ readonly files: FilesClient;
147
149
  constructor(options: GemmeinOptions);
148
150
  /**
149
151
  * Your app's data — `g.collection<{ title: string }>("notes")`. The
@@ -185,6 +187,81 @@ export declare class AuthClient {
185
187
  * dashboard. The client surface is deliberately read-plus-checkout only:
186
188
  * there is no client write path to plan or status, by design.
187
189
  */
190
+ /**
191
+ * The signed-in customer's own purchase history, read from Gemmein's
192
+ * immutable commercial record rather than from your data.
193
+ *
194
+ * This exists so a buyer keeps proof of what they paid for even when you keep
195
+ * no receipts collection at all — and it stays true if you later rename or
196
+ * delete one, because financial truth does not live in application data.
197
+ *
198
+ * Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
199
+ * as the payment provider reported them. Formatting is yours; rounding here
200
+ * would quietly lose money.
201
+ */
202
+ export declare class PurchasesClient {
203
+ private readonly config;
204
+ constructor(config: ClientConfig);
205
+ /**
206
+ * Everything this customer has paid for, newest first, with refunds already
207
+ * applied — `status` is "paid", "part_refunded" or "refunded", and
208
+ * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
209
+ * nobody is signed in.
210
+ */
211
+ mine(): Promise<Array<{
212
+ item: string;
213
+ kind: "purchase" | "subscription";
214
+ amountMinor: number | null;
215
+ currency: string | null;
216
+ refundedMinor: number;
217
+ status: "paid" | "part_refunded" | "refunded";
218
+ grants: string[];
219
+ paidAt: string;
220
+ }>>;
221
+ }
222
+ /**
223
+ * A file reference — `file:01K…`. What `upload()` gives you and what your
224
+ * record should store.
225
+ *
226
+ * Branded so it cannot be mistaken for a URL: `<img src={record.poster}>` is a
227
+ * type error, which is the cheapest possible moment to learn that a reference
228
+ * is a name and not a location.
229
+ */
230
+ export type FileRef = string & {
231
+ readonly __gemmeinFileRef: unique symbol;
232
+ };
233
+ /**
234
+ * Turn a stored file reference into a URL you can actually use.
235
+ *
236
+ * const { url } = await g.files.link(record.poster)
237
+ * img.src = url
238
+ *
239
+ * One call for every file, whichever collection it lives in. If the collection
240
+ * is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
241
+ * get one that works for a couple of minutes and is re-checked against who you
242
+ * are, what the collection's rule says, whether the file is yours, and whether
243
+ * you still hold whatever the collection requires.
244
+ *
245
+ * That is why the same app code keeps working when a collection later changes
246
+ * from public to private — and why a refund actually takes a download away.
247
+ *
248
+ * Don't store what this returns. Store the reference and call this again.
249
+ */
250
+ export declare class FilesClient {
251
+ private readonly config;
252
+ constructor(config: ClientConfig);
253
+ link(ref: FileRef | string, options?: {
254
+ intent?: "inline" | "download";
255
+ }): Promise<{
256
+ ref: FileRef;
257
+ url: string;
258
+ /** Absent when the collection is public — those links don't expire. */
259
+ expiresAt?: string;
260
+ contentType: string;
261
+ sizeBytes?: number;
262
+ name?: string;
263
+ }>;
264
+ }
188
265
  export declare class SubscriptionsClient {
189
266
  private readonly config;
190
267
  constructor(config: ClientConfig);
@@ -330,11 +407,27 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
330
407
  published?: boolean;
331
408
  }): Promise<GemmeinRecord<T>>;
332
409
  delete(id: string): Promise<void>;
410
+ /**
411
+ * Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
412
+ *
413
+ * Store the reference. It never expires, it is safe to log and export, and
414
+ * it grants nothing on its own. To show or download the file, call
415
+ * `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
416
+ * what makes revoking access actually take a download away.
417
+ *
418
+ * const { ref } = await g.collection("films").upload(file)
419
+ * await g.collection("films").create({ title, poster: ref })
420
+ * // later, to render:
421
+ * const { url } = await g.files.link(record.poster)
422
+ *
423
+ * There is deliberately no `url` here. A URL that outlives a refund is the
424
+ * bug this replaced.
425
+ */
333
426
  upload(file: Blob | File, options?: {
334
427
  name?: string;
335
428
  }): Promise<{
336
429
  id: string;
337
- url: string;
430
+ ref: FileRef;
338
431
  contentType: string;
339
432
  sizeBytes: number;
340
433
  }>;
package/dist/index.d.ts CHANGED
@@ -143,7 +143,9 @@ export declare class Gemmein {
143
143
  readonly storage: StorageClient;
144
144
  readonly subscriptions: SubscriptionsClient;
145
145
  readonly payments: PaymentsClient;
146
+ readonly purchases: PurchasesClient;
146
147
  readonly account: AccountClient;
148
+ readonly files: FilesClient;
147
149
  constructor(options: GemmeinOptions);
148
150
  /**
149
151
  * Your app's data — `g.collection<{ title: string }>("notes")`. The
@@ -185,6 +187,81 @@ export declare class AuthClient {
185
187
  * dashboard. The client surface is deliberately read-plus-checkout only:
186
188
  * there is no client write path to plan or status, by design.
187
189
  */
190
+ /**
191
+ * The signed-in customer's own purchase history, read from Gemmein's
192
+ * immutable commercial record rather than from your data.
193
+ *
194
+ * This exists so a buyer keeps proof of what they paid for even when you keep
195
+ * no receipts collection at all — and it stays true if you later rename or
196
+ * delete one, because financial truth does not live in application data.
197
+ *
198
+ * Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
199
+ * as the payment provider reported them. Formatting is yours; rounding here
200
+ * would quietly lose money.
201
+ */
202
+ export declare class PurchasesClient {
203
+ private readonly config;
204
+ constructor(config: ClientConfig);
205
+ /**
206
+ * Everything this customer has paid for, newest first, with refunds already
207
+ * applied — `status` is "paid", "part_refunded" or "refunded", and
208
+ * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
209
+ * nobody is signed in.
210
+ */
211
+ mine(): Promise<Array<{
212
+ item: string;
213
+ kind: "purchase" | "subscription";
214
+ amountMinor: number | null;
215
+ currency: string | null;
216
+ refundedMinor: number;
217
+ status: "paid" | "part_refunded" | "refunded";
218
+ grants: string[];
219
+ paidAt: string;
220
+ }>>;
221
+ }
222
+ /**
223
+ * A file reference — `file:01K…`. What `upload()` gives you and what your
224
+ * record should store.
225
+ *
226
+ * Branded so it cannot be mistaken for a URL: `<img src={record.poster}>` is a
227
+ * type error, which is the cheapest possible moment to learn that a reference
228
+ * is a name and not a location.
229
+ */
230
+ export type FileRef = string & {
231
+ readonly __gemmeinFileRef: unique symbol;
232
+ };
233
+ /**
234
+ * Turn a stored file reference into a URL you can actually use.
235
+ *
236
+ * const { url } = await g.files.link(record.poster)
237
+ * img.src = url
238
+ *
239
+ * One call for every file, whichever collection it lives in. If the collection
240
+ * is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
241
+ * get one that works for a couple of minutes and is re-checked against who you
242
+ * are, what the collection's rule says, whether the file is yours, and whether
243
+ * you still hold whatever the collection requires.
244
+ *
245
+ * That is why the same app code keeps working when a collection later changes
246
+ * from public to private — and why a refund actually takes a download away.
247
+ *
248
+ * Don't store what this returns. Store the reference and call this again.
249
+ */
250
+ export declare class FilesClient {
251
+ private readonly config;
252
+ constructor(config: ClientConfig);
253
+ link(ref: FileRef | string, options?: {
254
+ intent?: "inline" | "download";
255
+ }): Promise<{
256
+ ref: FileRef;
257
+ url: string;
258
+ /** Absent when the collection is public — those links don't expire. */
259
+ expiresAt?: string;
260
+ contentType: string;
261
+ sizeBytes?: number;
262
+ name?: string;
263
+ }>;
264
+ }
188
265
  export declare class SubscriptionsClient {
189
266
  private readonly config;
190
267
  constructor(config: ClientConfig);
@@ -330,11 +407,27 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
330
407
  published?: boolean;
331
408
  }): Promise<GemmeinRecord<T>>;
332
409
  delete(id: string): Promise<void>;
410
+ /**
411
+ * Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
412
+ *
413
+ * Store the reference. It never expires, it is safe to log and export, and
414
+ * it grants nothing on its own. To show or download the file, call
415
+ * `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
416
+ * what makes revoking access actually take a download away.
417
+ *
418
+ * const { ref } = await g.collection("films").upload(file)
419
+ * await g.collection("films").create({ title, poster: ref })
420
+ * // later, to render:
421
+ * const { url } = await g.files.link(record.poster)
422
+ *
423
+ * There is deliberately no `url` here. A URL that outlives a refund is the
424
+ * bug this replaced.
425
+ */
333
426
  upload(file: Blob | File, options?: {
334
427
  name?: string;
335
428
  }): Promise<{
336
429
  id: string;
337
- url: string;
430
+ ref: FileRef;
338
431
  contentType: string;
339
432
  sizeBytes: number;
340
433
  }>;
package/dist/index.js CHANGED
@@ -99,7 +99,9 @@ export class Gemmein {
99
99
  this.storage = new StorageClient(config);
100
100
  this.subscriptions = new SubscriptionsClient(config);
101
101
  this.payments = new PaymentsClient(config);
102
+ this.purchases = new PurchasesClient(config);
102
103
  this.account = new AccountClient(config);
104
+ this.files = new FilesClient(config);
103
105
  }
104
106
  /**
105
107
  * Your app's data — `g.collection<{ title: string }>("notes")`. The
@@ -190,6 +192,59 @@ export class AuthClient {
190
192
  * dashboard. The client surface is deliberately read-plus-checkout only:
191
193
  * there is no client write path to plan or status, by design.
192
194
  */
195
+ /**
196
+ * The signed-in customer's own purchase history, read from Gemmein's
197
+ * immutable commercial record rather than from your data.
198
+ *
199
+ * This exists so a buyer keeps proof of what they paid for even when you keep
200
+ * no receipts collection at all — and it stays true if you later rename or
201
+ * delete one, because financial truth does not live in application data.
202
+ *
203
+ * Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
204
+ * as the payment provider reported them. Formatting is yours; rounding here
205
+ * would quietly lose money.
206
+ */
207
+ export class PurchasesClient {
208
+ constructor(config) {
209
+ this.config = config;
210
+ }
211
+ /**
212
+ * Everything this customer has paid for, newest first, with refunds already
213
+ * applied — `status` is "paid", "part_refunded" or "refunded", and
214
+ * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
215
+ * nobody is signed in.
216
+ */
217
+ async mine() {
218
+ const result = (await runtimeRequest(this.config, "/auth/purchases"));
219
+ return result.purchases;
220
+ }
221
+ }
222
+ /**
223
+ * Turn a stored file reference into a URL you can actually use.
224
+ *
225
+ * const { url } = await g.files.link(record.poster)
226
+ * img.src = url
227
+ *
228
+ * One call for every file, whichever collection it lives in. If the collection
229
+ * is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
230
+ * get one that works for a couple of minutes and is re-checked against who you
231
+ * are, what the collection's rule says, whether the file is yours, and whether
232
+ * you still hold whatever the collection requires.
233
+ *
234
+ * That is why the same app code keeps working when a collection later changes
235
+ * from public to private — and why a refund actually takes a download away.
236
+ *
237
+ * Don't store what this returns. Store the reference and call this again.
238
+ */
239
+ export class FilesClient {
240
+ constructor(config) {
241
+ this.config = config;
242
+ }
243
+ async link(ref, options = {}) {
244
+ const query = options.intent === "download" ? "?intent=download" : "";
245
+ return (await runtimeRequest(this.config, `/files/${encodeURIComponent(String(ref))}/link${query}`));
246
+ }
247
+ }
193
248
  export class SubscriptionsClient {
194
249
  constructor(config) {
195
250
  this.config = config;
@@ -394,6 +449,22 @@ export class CollectionClient {
394
449
  async delete(id) {
395
450
  await this.request(`/${encodeURIComponent(id)}`, { method: "DELETE" });
396
451
  }
452
+ /**
453
+ * Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
454
+ *
455
+ * Store the reference. It never expires, it is safe to log and export, and
456
+ * it grants nothing on its own. To show or download the file, call
457
+ * `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
458
+ * what makes revoking access actually take a download away.
459
+ *
460
+ * const { ref } = await g.collection("films").upload(file)
461
+ * await g.collection("films").create({ title, poster: ref })
462
+ * // later, to render:
463
+ * const { url } = await g.files.link(record.poster)
464
+ *
465
+ * There is deliberately no `url` here. A URL that outlives a refund is the
466
+ * bug this replaced.
467
+ */
397
468
  async upload(file, options) {
398
469
  const name = options?.name ?? (file instanceof File ? file.name : "upload");
399
470
  // Step 1: Get presigned upload URL
package/llms.txt CHANGED
@@ -112,22 +112,33 @@
112
112
  - Images & files: NEVER base64 into record data and NEVER wire up your own
113
113
  storage bucket — uploads are built in:
114
114
  `const file = await g.collection("posts").upload(blob, { name })`
115
- → `{ id, url, contentType, sizeBytes }`. Store `file.url` in a record
116
- field like any text (that's also how a record "has" an image — the
117
- reference pattern, same as links). Upload permission follows the
118
- collection's WRITE rule; images only (JPEG/PNG/WebP/GIF/HEIC). Oversized
119
- files are refused loudly (413 file_too_large the message says the cap).
120
- The server checks the actual bytes at confirm a 400
121
- invalid_file_content means the file isn't really the image type it
122
- claimed (usually a renamed file); send the real image, don't retry.
123
- File URLs are unguessable but not revocable per-reader never put
124
- secrets in files.
115
+ → `{ id, ref, contentType, sizeBytes }`. Store `file.ref` (`"file:01K…"`)
116
+ in a record field like any text that's how a record "has" an image.
117
+ Store the REFERENCE, never a URL: a reference never expires and grants
118
+ nothing on its own.
119
+ To show or download it: `const { url } = await g.files.link(record.photo)`
120
+ one call for every file. Pass `{ intent: "download" }` for a download
121
+ rather than a preview. Files in a collection anyone can read get a
122
+ permanent link; every other file gets one that expires in a couple of
123
+ minutes, so call `link()` when you render, don't store what it returns.
124
+ Upload permission follows the collection's WRITE rule; images only
125
+ (JPEG/PNG/WebP/GIF/HEIC). Oversized files are refused loudly (413
126
+ file_too_large — the message says the cap). The server checks the actual
127
+ bytes at confirm — a 400 invalid_file_content means the file isn't really
128
+ the image type it claimed (usually a renamed file); send the real image,
129
+ don't retry. A 403 from `link()` means the customer isn't allowed this
130
+ file right now — signed out, not theirs, or an entitlement they no longer
131
+ hold (`entitlement_required` names the key).
132
+ Honest bound: revoking access stops NEW links immediately; a link already
133
+ issued works until it expires. Gemmein controls delivery, it can't take
134
+ back a file someone already downloaded.
125
135
  - Shapes: collections your users write (community, shared, direct) have a
126
136
  SHAPE — the set of fields allowed. While your human builds, the server
127
137
  learns it from your writes automatically (nothing to do); when they go
128
138
  live it locks. A 400 invalid_shape means the field isn't in the locked
129
- shape stop and tell your human to add it in their dashboard (one
130
- click), exactly like unknown_collection. Never rename fields to dodge it.
139
+ shape. A live shape is sealed and cannot take new fields stop, tell
140
+ your human which field you needed, and send only the fields the shape
141
+ already has. Never rename fields to dodge it.
131
142
  - Contention (bookings, slugs, stock, shared edits): when two users can race
132
143
  for the same thing, a permission model can't save you — preconditions do,
133
144
  and they're just arguments on calls you already make. A 409 `conflict` from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemmein/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Gemmein SDK \u2014 passwordless auth, safe storage, and Stripe-driven record flips for AI-built apps. Small enough that one prompt teaches the whole API.",
5
5
  "license": "MIT",
6
6
  "type": "module",