@gemmein/sdk 0.1.0 → 0.2.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 +21 -12
- package/REFERENCE.md +56 -18
- package/dist/index.cjs +133 -61
- package/dist/index.d.cts +85 -35
- package/dist/index.d.ts +85 -35
- package/dist/index.js +128 -59
- package/llms.txt +52 -25
- package/package.json +6 -2
- package/reaffirm.mjs +8 -8
package/dist/index.d.cts
CHANGED
|
@@ -125,34 +125,39 @@ export declare class BrowserTokenStore implements TokenStore {
|
|
|
125
125
|
set(token: string): void;
|
|
126
126
|
clear(): void;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* The client — two layers:
|
|
130
|
+
*
|
|
131
|
+
* - `g.collection("notes")` — YOUR app's collections. Records, files,
|
|
132
|
+
* safety rules. This is where your app's own data model lives.
|
|
133
|
+
* - Business primitives Gemmein runs for you: `g.auth` (sign-in),
|
|
134
|
+
* `g.subscriptions` (who's on which plan), `g.payments` (one-off
|
|
135
|
+
* purchases), `g.account` (the user's own account). These are
|
|
136
|
+
* SELF-SERVICE surfaces for the signed-in user — reads and Stripe
|
|
137
|
+
* hand-offs, never admin powers. Managing other people's users,
|
|
138
|
+
* subscriptions, or records happens in the owner's dashboard
|
|
139
|
+
* (app.gemmein.com), on purpose.
|
|
140
|
+
*/
|
|
128
141
|
export declare class Gemmein {
|
|
129
142
|
readonly auth: AuthClient;
|
|
130
143
|
readonly storage: StorageClient;
|
|
144
|
+
readonly subscriptions: SubscriptionsClient;
|
|
145
|
+
readonly payments: PaymentsClient;
|
|
146
|
+
readonly account: AccountClient;
|
|
131
147
|
constructor(options: GemmeinOptions);
|
|
132
|
-
/** The signed-in user's subscription — `(await g.subscription())?.plan === "pro"`. */
|
|
133
|
-
subscription(): Promise<{
|
|
134
|
-
plan: string;
|
|
135
|
-
status: "active" | "cancelled";
|
|
136
|
-
} | null>;
|
|
137
|
-
/** Send the signed-in user to Stripe checkout for a plan — `g.checkout("pro")`. Redirects in browsers. */
|
|
138
|
-
checkout(plan?: string): Promise<{
|
|
139
|
-
url: string;
|
|
140
|
-
plan: string;
|
|
141
|
-
}>;
|
|
142
148
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
149
|
+
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
150
|
+
* canonical spelling; `g.storage.collection(name)` is the same client.
|
|
151
|
+
*
|
|
152
|
+
* `intent` (one sentence: what this collection is for and who should
|
|
153
|
+
* access it) travels with every call. Against a LOCAL gemmein dev
|
|
154
|
+
* runtime, an undeclared collection then reaches the human with your
|
|
155
|
+
* suggestion attached — always pass it when you aren't certain the
|
|
156
|
+
* collection exists yet. The cloud ignores it.
|
|
148
157
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}):
|
|
152
|
-
url: string;
|
|
153
|
-
product: string;
|
|
154
|
-
item?: string;
|
|
155
|
-
}>;
|
|
158
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
159
|
+
intent?: string;
|
|
160
|
+
}): CollectionClient<T>;
|
|
156
161
|
}
|
|
157
162
|
export declare function gemmein(appKeyOrOptions: string | GemmeinOptions, options?: Omit<GemmeinOptions, "appKey">): Gemmein;
|
|
158
163
|
export declare function gemmeinServer(secretKeyOrOptions: string | GemmeinServerOptions, options?: Omit<GemmeinServerOptions, "secretKey">): GemmeinServer;
|
|
@@ -170,14 +175,33 @@ export declare class AuthClient {
|
|
|
170
175
|
code: string;
|
|
171
176
|
}): Promise<AuthSession>;
|
|
172
177
|
logout(): Promise<void>;
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
currentUser(): Promise<CurrentUser>;
|
|
179
|
+
private request;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* SUBS: the subscription primitive, self-service side. Gemmein keeps
|
|
183
|
+
* exactly one subscription per customer — created by the payment itself,
|
|
184
|
+
* updated by Stripe's signed webhooks, overridable by the owner in their
|
|
185
|
+
* dashboard. The client surface is deliberately read-plus-checkout only:
|
|
186
|
+
* there is no client write path to plan or status, by design.
|
|
187
|
+
*/
|
|
188
|
+
export declare class SubscriptionsClient {
|
|
189
|
+
private readonly config;
|
|
190
|
+
constructor(config: ClientConfig);
|
|
191
|
+
/**
|
|
192
|
+
* The signed-in user's subscription — gate features with
|
|
193
|
+
* `(await g.subscriptions.mine())?.plan === "pro"`. Null when payments
|
|
194
|
+
* are off or this user has never paid; throws GemmeinError (401) when
|
|
195
|
+
* nobody is signed in — a data route, not the never-throw current-user
|
|
196
|
+
* contract.
|
|
197
|
+
*/
|
|
198
|
+
mine(): Promise<{
|
|
175
199
|
plan: string;
|
|
176
200
|
status: "active" | "cancelled";
|
|
177
201
|
} | null>;
|
|
178
202
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
203
|
+
* Start a Stripe checkout for a plan — Gemmein mints the URL with the
|
|
204
|
+
* signed-in buyer and the plan already wired in (never build checkout
|
|
181
205
|
* URLs yourself; raw emails get silently dropped by Stripe's URL rules).
|
|
182
206
|
* In a browser this redirects immediately; it also resolves with the URL
|
|
183
207
|
* (for non-browser callers or custom handling). Requires a signed-in user
|
|
@@ -188,27 +212,50 @@ export declare class AuthClient {
|
|
|
188
212
|
url: string;
|
|
189
213
|
plan: string;
|
|
190
214
|
}>;
|
|
215
|
+
}
|
|
216
|
+
/** The one-off purchase primitive — things, not plans (plans are `g.subscriptions`). */
|
|
217
|
+
export declare class PaymentsClient {
|
|
218
|
+
private readonly config;
|
|
219
|
+
constructor(config: ClientConfig);
|
|
191
220
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
221
|
+
* Buy a one-off product — `g.payments.buy("poster")`. Redirects in
|
|
222
|
+
* browsers, and resolves with the URL. The optional `item` note names
|
|
223
|
+
* WHAT is being bought when one product covers many things (e.g. a
|
|
224
|
+
* license tier across a catalog):
|
|
225
|
+
* `g.payments.buy("premium license", { item: "beat_37" })`.
|
|
226
|
+
* A completed payment writes a receipt record addressed to the buyer in
|
|
227
|
+
* the owner's receipts collection; gate downloads/fulfilment on that
|
|
228
|
+
* receipt, never on the redirect coming back.
|
|
196
229
|
*/
|
|
197
|
-
|
|
230
|
+
buy(product: string, options?: {
|
|
198
231
|
item?: string;
|
|
199
232
|
}): Promise<{
|
|
200
233
|
url: string;
|
|
201
234
|
product: string;
|
|
202
235
|
item?: string;
|
|
203
236
|
}>;
|
|
204
|
-
|
|
205
|
-
|
|
237
|
+
}
|
|
238
|
+
/** The signed-in user's own account — self-service, one deliberate power. */
|
|
239
|
+
export declare class AccountClient {
|
|
240
|
+
private readonly config;
|
|
241
|
+
constructor(config: ClientConfig);
|
|
242
|
+
/**
|
|
243
|
+
* Self-service erasure — the "delete my account" screen. Every app it
|
|
244
|
+
* APPLIES to needs one (GDPR right to erasure; Apple 5.1.1(v) requires it
|
|
245
|
+
* for any app with account creation). Server-side this is the full
|
|
246
|
+
* cascade: sessions revoked, the user's records and files deleted, their
|
|
247
|
+
* subscription row removed. Irreversible — put a real confirm in front
|
|
248
|
+
* of it.
|
|
249
|
+
*/
|
|
250
|
+
delete(): Promise<unknown>;
|
|
206
251
|
}
|
|
207
252
|
export declare class StorageClient {
|
|
208
253
|
private readonly config;
|
|
209
254
|
constructor(config: ClientConfig);
|
|
210
255
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
211
|
-
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string
|
|
256
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
257
|
+
intent?: string;
|
|
258
|
+
}): CollectionClient<T>;
|
|
212
259
|
}
|
|
213
260
|
/**
|
|
214
261
|
* Talks to one collection. Collections themselves are created by the app
|
|
@@ -219,7 +266,10 @@ export declare class StorageClient {
|
|
|
219
266
|
export declare class CollectionClient<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
220
267
|
private readonly config;
|
|
221
268
|
private readonly name;
|
|
222
|
-
|
|
269
|
+
private readonly intent?;
|
|
270
|
+
constructor(config: ClientConfig, name: string, options?: {
|
|
271
|
+
intent?: string;
|
|
272
|
+
});
|
|
223
273
|
/**
|
|
224
274
|
* Create a record from your fields. The signed-in user becomes its owner.
|
|
225
275
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -125,34 +125,39 @@ export declare class BrowserTokenStore implements TokenStore {
|
|
|
125
125
|
set(token: string): void;
|
|
126
126
|
clear(): void;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* The client — two layers:
|
|
130
|
+
*
|
|
131
|
+
* - `g.collection("notes")` — YOUR app's collections. Records, files,
|
|
132
|
+
* safety rules. This is where your app's own data model lives.
|
|
133
|
+
* - Business primitives Gemmein runs for you: `g.auth` (sign-in),
|
|
134
|
+
* `g.subscriptions` (who's on which plan), `g.payments` (one-off
|
|
135
|
+
* purchases), `g.account` (the user's own account). These are
|
|
136
|
+
* SELF-SERVICE surfaces for the signed-in user — reads and Stripe
|
|
137
|
+
* hand-offs, never admin powers. Managing other people's users,
|
|
138
|
+
* subscriptions, or records happens in the owner's dashboard
|
|
139
|
+
* (app.gemmein.com), on purpose.
|
|
140
|
+
*/
|
|
128
141
|
export declare class Gemmein {
|
|
129
142
|
readonly auth: AuthClient;
|
|
130
143
|
readonly storage: StorageClient;
|
|
144
|
+
readonly subscriptions: SubscriptionsClient;
|
|
145
|
+
readonly payments: PaymentsClient;
|
|
146
|
+
readonly account: AccountClient;
|
|
131
147
|
constructor(options: GemmeinOptions);
|
|
132
|
-
/** The signed-in user's subscription — `(await g.subscription())?.plan === "pro"`. */
|
|
133
|
-
subscription(): Promise<{
|
|
134
|
-
plan: string;
|
|
135
|
-
status: "active" | "cancelled";
|
|
136
|
-
} | null>;
|
|
137
|
-
/** Send the signed-in user to Stripe checkout for a plan — `g.checkout("pro")`. Redirects in browsers. */
|
|
138
|
-
checkout(plan?: string): Promise<{
|
|
139
|
-
url: string;
|
|
140
|
-
plan: string;
|
|
141
|
-
}>;
|
|
142
148
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
149
|
+
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
150
|
+
* canonical spelling; `g.storage.collection(name)` is the same client.
|
|
151
|
+
*
|
|
152
|
+
* `intent` (one sentence: what this collection is for and who should
|
|
153
|
+
* access it) travels with every call. Against a LOCAL gemmein dev
|
|
154
|
+
* runtime, an undeclared collection then reaches the human with your
|
|
155
|
+
* suggestion attached — always pass it when you aren't certain the
|
|
156
|
+
* collection exists yet. The cloud ignores it.
|
|
148
157
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}):
|
|
152
|
-
url: string;
|
|
153
|
-
product: string;
|
|
154
|
-
item?: string;
|
|
155
|
-
}>;
|
|
158
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
159
|
+
intent?: string;
|
|
160
|
+
}): CollectionClient<T>;
|
|
156
161
|
}
|
|
157
162
|
export declare function gemmein(appKeyOrOptions: string | GemmeinOptions, options?: Omit<GemmeinOptions, "appKey">): Gemmein;
|
|
158
163
|
export declare function gemmeinServer(secretKeyOrOptions: string | GemmeinServerOptions, options?: Omit<GemmeinServerOptions, "secretKey">): GemmeinServer;
|
|
@@ -170,14 +175,33 @@ export declare class AuthClient {
|
|
|
170
175
|
code: string;
|
|
171
176
|
}): Promise<AuthSession>;
|
|
172
177
|
logout(): Promise<void>;
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
currentUser(): Promise<CurrentUser>;
|
|
179
|
+
private request;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* SUBS: the subscription primitive, self-service side. Gemmein keeps
|
|
183
|
+
* exactly one subscription per customer — created by the payment itself,
|
|
184
|
+
* updated by Stripe's signed webhooks, overridable by the owner in their
|
|
185
|
+
* dashboard. The client surface is deliberately read-plus-checkout only:
|
|
186
|
+
* there is no client write path to plan or status, by design.
|
|
187
|
+
*/
|
|
188
|
+
export declare class SubscriptionsClient {
|
|
189
|
+
private readonly config;
|
|
190
|
+
constructor(config: ClientConfig);
|
|
191
|
+
/**
|
|
192
|
+
* The signed-in user's subscription — gate features with
|
|
193
|
+
* `(await g.subscriptions.mine())?.plan === "pro"`. Null when payments
|
|
194
|
+
* are off or this user has never paid; throws GemmeinError (401) when
|
|
195
|
+
* nobody is signed in — a data route, not the never-throw current-user
|
|
196
|
+
* contract.
|
|
197
|
+
*/
|
|
198
|
+
mine(): Promise<{
|
|
175
199
|
plan: string;
|
|
176
200
|
status: "active" | "cancelled";
|
|
177
201
|
} | null>;
|
|
178
202
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
203
|
+
* Start a Stripe checkout for a plan — Gemmein mints the URL with the
|
|
204
|
+
* signed-in buyer and the plan already wired in (never build checkout
|
|
181
205
|
* URLs yourself; raw emails get silently dropped by Stripe's URL rules).
|
|
182
206
|
* In a browser this redirects immediately; it also resolves with the URL
|
|
183
207
|
* (for non-browser callers or custom handling). Requires a signed-in user
|
|
@@ -188,27 +212,50 @@ export declare class AuthClient {
|
|
|
188
212
|
url: string;
|
|
189
213
|
plan: string;
|
|
190
214
|
}>;
|
|
215
|
+
}
|
|
216
|
+
/** The one-off purchase primitive — things, not plans (plans are `g.subscriptions`). */
|
|
217
|
+
export declare class PaymentsClient {
|
|
218
|
+
private readonly config;
|
|
219
|
+
constructor(config: ClientConfig);
|
|
191
220
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
221
|
+
* Buy a one-off product — `g.payments.buy("poster")`. Redirects in
|
|
222
|
+
* browsers, and resolves with the URL. The optional `item` note names
|
|
223
|
+
* WHAT is being bought when one product covers many things (e.g. a
|
|
224
|
+
* license tier across a catalog):
|
|
225
|
+
* `g.payments.buy("premium license", { item: "beat_37" })`.
|
|
226
|
+
* A completed payment writes a receipt record addressed to the buyer in
|
|
227
|
+
* the owner's receipts collection; gate downloads/fulfilment on that
|
|
228
|
+
* receipt, never on the redirect coming back.
|
|
196
229
|
*/
|
|
197
|
-
|
|
230
|
+
buy(product: string, options?: {
|
|
198
231
|
item?: string;
|
|
199
232
|
}): Promise<{
|
|
200
233
|
url: string;
|
|
201
234
|
product: string;
|
|
202
235
|
item?: string;
|
|
203
236
|
}>;
|
|
204
|
-
|
|
205
|
-
|
|
237
|
+
}
|
|
238
|
+
/** The signed-in user's own account — self-service, one deliberate power. */
|
|
239
|
+
export declare class AccountClient {
|
|
240
|
+
private readonly config;
|
|
241
|
+
constructor(config: ClientConfig);
|
|
242
|
+
/**
|
|
243
|
+
* Self-service erasure — the "delete my account" screen. Every app it
|
|
244
|
+
* APPLIES to needs one (GDPR right to erasure; Apple 5.1.1(v) requires it
|
|
245
|
+
* for any app with account creation). Server-side this is the full
|
|
246
|
+
* cascade: sessions revoked, the user's records and files deleted, their
|
|
247
|
+
* subscription row removed. Irreversible — put a real confirm in front
|
|
248
|
+
* of it.
|
|
249
|
+
*/
|
|
250
|
+
delete(): Promise<unknown>;
|
|
206
251
|
}
|
|
207
252
|
export declare class StorageClient {
|
|
208
253
|
private readonly config;
|
|
209
254
|
constructor(config: ClientConfig);
|
|
210
255
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
211
|
-
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string
|
|
256
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
257
|
+
intent?: string;
|
|
258
|
+
}): CollectionClient<T>;
|
|
212
259
|
}
|
|
213
260
|
/**
|
|
214
261
|
* Talks to one collection. Collections themselves are created by the app
|
|
@@ -219,7 +266,10 @@ export declare class StorageClient {
|
|
|
219
266
|
export declare class CollectionClient<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
220
267
|
private readonly config;
|
|
221
268
|
private readonly name;
|
|
222
|
-
|
|
269
|
+
private readonly intent?;
|
|
270
|
+
constructor(config: ClientConfig, name: string, options?: {
|
|
271
|
+
intent?: string;
|
|
272
|
+
});
|
|
223
273
|
/**
|
|
224
274
|
* Create a record from your fields. The signed-in user becomes its owner.
|
|
225
275
|
*
|
package/dist/index.js
CHANGED
|
@@ -56,9 +56,24 @@ function defaultTokenStore(appKey) {
|
|
|
56
56
|
catch { /* SSR / Node / storage blocked */ }
|
|
57
57
|
return new MemoryTokenStore();
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The client — two layers:
|
|
61
|
+
*
|
|
62
|
+
* - `g.collection("notes")` — YOUR app's collections. Records, files,
|
|
63
|
+
* safety rules. This is where your app's own data model lives.
|
|
64
|
+
* - Business primitives Gemmein runs for you: `g.auth` (sign-in),
|
|
65
|
+
* `g.subscriptions` (who's on which plan), `g.payments` (one-off
|
|
66
|
+
* purchases), `g.account` (the user's own account). These are
|
|
67
|
+
* SELF-SERVICE surfaces for the signed-in user — reads and Stripe
|
|
68
|
+
* hand-offs, never admin powers. Managing other people's users,
|
|
69
|
+
* subscriptions, or records happens in the owner's dashboard
|
|
70
|
+
* (app.gemmein.com), on purpose.
|
|
71
|
+
*/
|
|
59
72
|
export class Gemmein {
|
|
60
73
|
constructor(options) {
|
|
61
|
-
|
|
74
|
+
// A missing key must land on the signpost below, not a TypeError —
|
|
75
|
+
// gemmein(undefined) is exactly the "env var didn't load" case.
|
|
76
|
+
if (options?.appKey?.startsWith("sk_")) {
|
|
62
77
|
throw new GemmeinError({
|
|
63
78
|
status: 0,
|
|
64
79
|
code: "invalid_app_key",
|
|
@@ -66,7 +81,7 @@ export class Gemmein {
|
|
|
66
81
|
});
|
|
67
82
|
}
|
|
68
83
|
// The signpost: an AI building without a key gets routed, not stuck.
|
|
69
|
-
if (!options
|
|
84
|
+
if (!options?.appKey || !options.appKey.startsWith("pk_")) {
|
|
70
85
|
throw new GemmeinError({
|
|
71
86
|
status: 0,
|
|
72
87
|
code: "missing_app_key",
|
|
@@ -82,24 +97,22 @@ export class Gemmein {
|
|
|
82
97
|
};
|
|
83
98
|
this.auth = new AuthClient(config);
|
|
84
99
|
this.storage = new StorageClient(config);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return this.auth.subscription();
|
|
89
|
-
}
|
|
90
|
-
/** Send the signed-in user to Stripe checkout for a plan — `g.checkout("pro")`. Redirects in browsers. */
|
|
91
|
-
checkout(plan) {
|
|
92
|
-
return this.auth.checkout(plan);
|
|
100
|
+
this.subscriptions = new SubscriptionsClient(config);
|
|
101
|
+
this.payments = new PaymentsClient(config);
|
|
102
|
+
this.account = new AccountClient(config);
|
|
93
103
|
}
|
|
94
104
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
105
|
+
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
106
|
+
* canonical spelling; `g.storage.collection(name)` is the same client.
|
|
107
|
+
*
|
|
108
|
+
* `intent` (one sentence: what this collection is for and who should
|
|
109
|
+
* access it) travels with every call. Against a LOCAL gemmein dev
|
|
110
|
+
* runtime, an undeclared collection then reaches the human with your
|
|
111
|
+
* suggestion attached — always pass it when you aren't certain the
|
|
112
|
+
* collection exists yet. The cloud ignores it.
|
|
100
113
|
*/
|
|
101
|
-
|
|
102
|
-
return this.
|
|
114
|
+
collection(name, options = {}) {
|
|
115
|
+
return this.storage.collection(name, options);
|
|
103
116
|
}
|
|
104
117
|
}
|
|
105
118
|
// Factory forms — what the copied prompts teach. `gemmein("pk_...")` reads
|
|
@@ -150,24 +163,51 @@ export class AuthClient {
|
|
|
150
163
|
await this.config.tokenStore.clear();
|
|
151
164
|
}
|
|
152
165
|
}
|
|
153
|
-
async
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
async currentUser() {
|
|
167
|
+
// Contract: "who am I?" never throws for session state — the server
|
|
168
|
+
// answers 200 {authenticated:false} for a token it won't honour right
|
|
169
|
+
// now, same as no token. We deliberately do NOT clear the stored token
|
|
170
|
+
// here: authenticated:false also covers a SUSPENDED user, whose session
|
|
171
|
+
// is intact and restored on unsuspend — clearing it would make a
|
|
172
|
+
// reversible suspension permanently sign them out. A genuinely expired
|
|
173
|
+
// token is harmless to keep (the next data-plane call 401s and the app
|
|
174
|
+
// re-auths); logout() and g.account.delete() are the deliberate clears.
|
|
175
|
+
//
|
|
176
|
+
// Moderation note: authenticated:false is deliberately SILENT about the
|
|
177
|
+
// why — signed out, suspended, and erased all read the same here, so a
|
|
178
|
+
// moderated user's state is never leaked to the client. Build the
|
|
179
|
+
// signed-out screen for all three.
|
|
180
|
+
return this.request("/auth/current-user");
|
|
181
|
+
}
|
|
182
|
+
request(path, init = {}) {
|
|
183
|
+
return runtimeRequest(this.config, path, init);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* SUBS: the subscription primitive, self-service side. Gemmein keeps
|
|
188
|
+
* exactly one subscription per customer — created by the payment itself,
|
|
189
|
+
* updated by Stripe's signed webhooks, overridable by the owner in their
|
|
190
|
+
* dashboard. The client surface is deliberately read-plus-checkout only:
|
|
191
|
+
* there is no client write path to plan or status, by design.
|
|
192
|
+
*/
|
|
193
|
+
export class SubscriptionsClient {
|
|
194
|
+
constructor(config) {
|
|
195
|
+
this.config = config;
|
|
157
196
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
/**
|
|
198
|
+
* The signed-in user's subscription — gate features with
|
|
199
|
+
* `(await g.subscriptions.mine())?.plan === "pro"`. Null when payments
|
|
200
|
+
* are off or this user has never paid; throws GemmeinError (401) when
|
|
201
|
+
* nobody is signed in — a data route, not the never-throw current-user
|
|
202
|
+
* contract.
|
|
203
|
+
*/
|
|
204
|
+
async mine() {
|
|
205
|
+
const result = (await runtimeRequest(this.config, "/auth/subscription"));
|
|
166
206
|
return result.subscription;
|
|
167
207
|
}
|
|
168
208
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
209
|
+
* Start a Stripe checkout for a plan — Gemmein mints the URL with the
|
|
210
|
+
* signed-in buyer and the plan already wired in (never build checkout
|
|
171
211
|
* URLs yourself; raw emails get silently dropped by Stripe's URL rules).
|
|
172
212
|
* In a browser this redirects immediately; it also resolves with the URL
|
|
173
213
|
* (for non-browser callers or custom handling). Requires a signed-in user
|
|
@@ -176,45 +216,56 @@ export class AuthClient {
|
|
|
176
216
|
*/
|
|
177
217
|
async checkout(plan) {
|
|
178
218
|
const query = plan ? `?plan=${encodeURIComponent(plan)}` : "";
|
|
179
|
-
const result = (await this.
|
|
219
|
+
const result = (await runtimeRequest(this.config, `/auth/checkout${query}`));
|
|
180
220
|
if (typeof window !== "undefined" && window.location) {
|
|
181
221
|
window.location.assign(result.url);
|
|
182
222
|
}
|
|
183
223
|
return result;
|
|
184
224
|
}
|
|
225
|
+
}
|
|
226
|
+
/** The one-off purchase primitive — things, not plans (plans are `g.subscriptions`). */
|
|
227
|
+
export class PaymentsClient {
|
|
228
|
+
constructor(config) {
|
|
229
|
+
this.config = config;
|
|
230
|
+
}
|
|
185
231
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
232
|
+
* Buy a one-off product — `g.payments.buy("poster")`. Redirects in
|
|
233
|
+
* browsers, and resolves with the URL. The optional `item` note names
|
|
234
|
+
* WHAT is being bought when one product covers many things (e.g. a
|
|
235
|
+
* license tier across a catalog):
|
|
236
|
+
* `g.payments.buy("premium license", { item: "beat_37" })`.
|
|
237
|
+
* A completed payment writes a receipt record addressed to the buyer in
|
|
238
|
+
* the owner's receipts collection; gate downloads/fulfilment on that
|
|
239
|
+
* receipt, never on the redirect coming back.
|
|
190
240
|
*/
|
|
191
|
-
async
|
|
241
|
+
async buy(product, options) {
|
|
192
242
|
const params = new URLSearchParams({ product });
|
|
193
243
|
if (options?.item)
|
|
194
244
|
params.set("item", options.item);
|
|
195
|
-
const result = (await this.
|
|
245
|
+
const result = (await runtimeRequest(this.config, `/auth/pay?${params.toString()}`));
|
|
196
246
|
if (typeof window !== "undefined" && window.location) {
|
|
197
247
|
window.location.assign(result.url);
|
|
198
248
|
}
|
|
199
249
|
return result;
|
|
200
250
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// is intact and restored on unsuspend — clearing it would make a
|
|
207
|
-
// reversible suspension permanently sign them out. A genuinely expired
|
|
208
|
-
// token is harmless to keep (the next data-plane call 401s and the app
|
|
209
|
-
// re-auths); logout() and deleteAccount() are the deliberate clears.
|
|
210
|
-
return this.request("/auth/current-user");
|
|
251
|
+
}
|
|
252
|
+
/** The signed-in user's own account — self-service, one deliberate power. */
|
|
253
|
+
export class AccountClient {
|
|
254
|
+
constructor(config) {
|
|
255
|
+
this.config = config;
|
|
211
256
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Self-service erasure — the "delete my account" screen. Every app it
|
|
259
|
+
* APPLIES to needs one (GDPR right to erasure; Apple 5.1.1(v) requires it
|
|
260
|
+
* for any app with account creation). Server-side this is the full
|
|
261
|
+
* cascade: sessions revoked, the user's records and files deleted, their
|
|
262
|
+
* subscription row removed. Irreversible — put a real confirm in front
|
|
263
|
+
* of it.
|
|
264
|
+
*/
|
|
265
|
+
async delete() {
|
|
266
|
+
const result = await runtimeRequest(this.config, "/auth/delete-account", { method: "POST" });
|
|
267
|
+
await this.config.tokenStore.clear();
|
|
268
|
+
return result;
|
|
218
269
|
}
|
|
219
270
|
}
|
|
220
271
|
function isSessionResponse(value) {
|
|
@@ -228,9 +279,9 @@ export class StorageClient {
|
|
|
228
279
|
this.config = config;
|
|
229
280
|
}
|
|
230
281
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
231
|
-
collection(name) {
|
|
282
|
+
collection(name, options = {}) {
|
|
232
283
|
assertCollectionName(name);
|
|
233
|
-
return new CollectionClient(this.config, name);
|
|
284
|
+
return new CollectionClient(this.config, name, options);
|
|
234
285
|
}
|
|
235
286
|
}
|
|
236
287
|
/**
|
|
@@ -240,9 +291,10 @@ export class StorageClient {
|
|
|
240
291
|
* it there, don't retry.
|
|
241
292
|
*/
|
|
242
293
|
export class CollectionClient {
|
|
243
|
-
constructor(config, name) {
|
|
294
|
+
constructor(config, name, options = {}) {
|
|
244
295
|
this.config = config;
|
|
245
296
|
this.name = name;
|
|
297
|
+
this.intent = options.intent;
|
|
246
298
|
}
|
|
247
299
|
/**
|
|
248
300
|
* Create a record from your fields. The signed-in user becomes its owner.
|
|
@@ -374,6 +426,10 @@ export class CollectionClient {
|
|
|
374
426
|
...init,
|
|
375
427
|
headers: await runtimeHeaders(this.config, {
|
|
376
428
|
"content-type": "application/json",
|
|
429
|
+
// The intent rides every call so an undeclared collection reaches
|
|
430
|
+
// the human WITH the AI's suggestion attached (local runtime only;
|
|
431
|
+
// the cloud ignores it).
|
|
432
|
+
...(this.intent ? { "x-collection-intent": this.intent.slice(0, 200) } : {}),
|
|
377
433
|
...init.headers
|
|
378
434
|
})
|
|
379
435
|
});
|
|
@@ -382,11 +438,15 @@ export class CollectionClient {
|
|
|
382
438
|
}
|
|
383
439
|
export class GemmeinServer {
|
|
384
440
|
constructor(options) {
|
|
385
|
-
|
|
441
|
+
// Same signpost law as the client: a missing key (env var didn't load)
|
|
442
|
+
// must be a typed error, never a TypeError.
|
|
443
|
+
if (!options?.secretKey || !options.secretKey.startsWith("sk_")) {
|
|
386
444
|
throw new GemmeinError({
|
|
387
445
|
status: 0,
|
|
388
446
|
code: "invalid_secret_key",
|
|
389
|
-
message: "
|
|
447
|
+
message: options?.secretKey?.startsWith("pk_")
|
|
448
|
+
? "Public keys (pk_) must not be used with GemmeinServer — use the Gemmein client SDK instead"
|
|
449
|
+
: 'GemmeinServer needs a secret key (it starts with "sk_") — create one in the dashboard at https://app.gemmein.com and pass it from a server env var'
|
|
390
450
|
});
|
|
391
451
|
}
|
|
392
452
|
this.apiUrl = options.apiUrl ?? "https://api.gemmein.com";
|
|
@@ -533,6 +593,15 @@ async function readErrorBody(response) {
|
|
|
533
593
|
message: `Gemmein request failed: ${response.status}`
|
|
534
594
|
};
|
|
535
595
|
}
|
|
596
|
+
// One request path for every runtime client (auth, subscriptions,
|
|
597
|
+
// payments, account) — same headers, same error handling, same signposts.
|
|
598
|
+
async function runtimeRequest(config, path, init = {}) {
|
|
599
|
+
const response = await fetch(new URL(path, config.apiUrl), {
|
|
600
|
+
...init,
|
|
601
|
+
headers: await runtimeHeaders(config, init.headers)
|
|
602
|
+
});
|
|
603
|
+
return handleResponse(response, config);
|
|
604
|
+
}
|
|
536
605
|
async function runtimeHeaders(config, headers) {
|
|
537
606
|
const token = await config.tokenStore.get();
|
|
538
607
|
return {
|