@gemmein/sdk 0.2.1 → 0.3.1
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 +59 -33
- package/package.json +2 -2
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
|
|
@@ -174,28 +185,43 @@
|
|
|
174
185
|
arriving out of order resolve to the newest. The app reads
|
|
175
186
|
`await g.subscriptions.mine()` → `{ plan, status }` or null, and gates features
|
|
176
187
|
with `sub?.plan === "pro"`.
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
`
|
|
188
|
+
- Paid ACCESS (entitlements): a collection can require a key — your human
|
|
189
|
+
sets `requires: "access:pro"` in its settings — and the engine refuses
|
|
190
|
+
anyone without it, on every operation, under all seven rules. Keys are
|
|
191
|
+
granted by money: a plan or product lists what it unlocks (e.g.
|
|
192
|
+
`access:pro, access:exports`), the paid webhook grants those keys, and a
|
|
193
|
+
refund or cancellation revokes exactly what it granted — nothing else.
|
|
194
|
+
Owners also grant and revoke by hand (trials, comps, support). Effective
|
|
195
|
+
access is the UNION of a customer's live grants. A signed-in customer
|
|
196
|
+
without the key gets `403 entitlement_required` naming it — show your
|
|
197
|
+
upgrade screen and send them to checkout; never retry. Proof surfaces:
|
|
198
|
+
`await g.purchases.mine()` (everything they paid for, refunds applied,
|
|
199
|
+
with the `grants` each purchase carries) and `await g.subscriptions.mine()`.
|
|
200
|
+
NO credits, NO usage limits, NO seats — access is yes-or-no by design.
|
|
201
|
+
- Selling THINGS (one-off purchases — a beat, an ebook, a course; DIGITAL
|
|
202
|
+
access only — physical goods, shipping, inventory and carts are out of
|
|
203
|
+
scope, said out loud): plans are for subscriptions; products are for
|
|
204
|
+
things. The builder adds products (name + Stripe Payment Link) on the
|
|
205
|
+
same Payments page. The app calls
|
|
206
|
+
`await g.payments.buy("beat")` — or, when one product covers many items (license
|
|
182
207
|
tiers over a catalog), names the item:
|
|
183
|
-
`await g.payments.buy("premium license", { item: "beat_37" })` (display text
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
`await g.payments.buy("premium license", { item: "beat_37" })` (display text
|
|
209
|
+
only; the PRICE always comes from the product's Payment Link, so the
|
|
210
|
+
item note can never change what's paid). Gemmein records every completed
|
|
211
|
+
payment itself — `await g.purchases.mine()` is the buyer's proof:
|
|
212
|
+
{ item, kind, status: "paid"|"part_refunded"|"refunded", amountMinor,
|
|
213
|
+
currency, refundedMinor, grants, paidAt }. A receipts collection (rule
|
|
214
|
+
`addressed`) is OPTIONAL — add one when you want receipts as records the
|
|
215
|
+
app renders; only the buyer and the owner read them. Delivery: attach
|
|
216
|
+
the thing to the product — a sealed Gemmein file (the receipt carries a
|
|
217
|
+
`file:` ref; resolve it per reader with `g.files.link`, which re-checks
|
|
218
|
+
access on every mint) or an external `deliveryUrl` (a plain handover:
|
|
219
|
+
Gemmein controls who is TOLD, not who can use it). Gate fulfilment on
|
|
220
|
+
the purchase or the entitlement it granted, never on the redirect coming
|
|
221
|
+
back — redirects can be faked; the record comes from Stripe's signed
|
|
222
|
+
webhook. NO carts, NO quantities — one product per checkout by design; a
|
|
223
|
+
cart is N checkouts or one bundled product. 404 unknown_product lists
|
|
224
|
+
what the app actually sells — use those names.
|
|
199
225
|
- Drafts on PUBLIC collections (public_read, community): create with the
|
|
200
226
|
OPTION `{ published: false }` → hidden from every reader except its
|
|
201
227
|
author and the owner, server-enforced; publish with
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemmein/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Gemmein SDK
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Gemmein SDK — 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",
|
|
7
7
|
"main": "./dist/index.cjs",
|