@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 +19 -2
- package/REFERENCE.md +34 -1
- package/dist/index.cjs +74 -1
- package/dist/index.d.cts +94 -1
- package/dist/index.d.ts +94 -1
- package/dist/index.js +71 -0
- package/llms.txt +23 -12
- package/package.json +1 -1
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
|
|
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,
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
116
|
-
field like any text
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
130
|
-
|
|
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.
|
|
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",
|