@gemmein/sdk 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +64 -1
- package/dist/index.d.cts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +64 -1
- package/llms.txt +48 -6
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -526,6 +526,24 @@ class CollectionClient {
|
|
|
526
526
|
inFlight = false;
|
|
527
527
|
return;
|
|
528
528
|
}
|
|
529
|
+
// An auth refusal is not transient: a signed-out or unentitled
|
|
530
|
+
// watcher polling forever writes a denied-audit row per attempt on
|
|
531
|
+
// the server (audit finding #8). Stop; a fresh watch() after
|
|
532
|
+
// sign-in starts clean.
|
|
533
|
+
if (err instanceof GemmeinError && (err.status === 401 || err.status === 403)) {
|
|
534
|
+
inFlight = false;
|
|
535
|
+
stopped = true;
|
|
536
|
+
if (timer !== undefined) {
|
|
537
|
+
clearTimeout(timer);
|
|
538
|
+
timer = undefined;
|
|
539
|
+
}
|
|
540
|
+
if (typeof document !== "undefined") {
|
|
541
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
542
|
+
}
|
|
543
|
+
if (typeof console !== "undefined")
|
|
544
|
+
console.warn(`gemmein watch stopped: ${err.code} — start a new watch after signing in`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
529
547
|
// Back off; a rate limit says exactly when to come back.
|
|
530
548
|
delayMs = Math.min(Math.max(delayMs * 2, every), 60000);
|
|
531
549
|
if (err instanceof GemmeinError && err.resetAt) {
|
|
@@ -653,6 +671,12 @@ class CollectionClient {
|
|
|
653
671
|
* Pass `{ name }` so the download carries a real filename, and
|
|
654
672
|
* `{ contentType }` when the Blob has no type of its own.
|
|
655
673
|
*
|
|
674
|
+
* W7.4 — `{ for: userId }` names ONE other person who may read this file
|
|
675
|
+
* (addressed and direct collections only — the rules where recipients
|
|
676
|
+
* exist). The attachment on a direct message: upload with
|
|
677
|
+
* `{ for: recipientId }`, store the ref in the record you send them, and
|
|
678
|
+
* their `g.files.link(ref)` works. Without `for`, only you can link it.
|
|
679
|
+
*
|
|
656
680
|
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
657
681
|
* bug this replaced.
|
|
658
682
|
*/
|
|
@@ -664,7 +688,7 @@ class CollectionClient {
|
|
|
664
688
|
// Step 1: Get presigned upload URL
|
|
665
689
|
const presign = await this.request("/upload", {
|
|
666
690
|
method: "POST",
|
|
667
|
-
body: JSON.stringify({ name, size: file.size, contentType }),
|
|
691
|
+
body: JSON.stringify({ name, size: file.size, contentType, ...(options?.for ? { for: options.for } : {}) }),
|
|
668
692
|
});
|
|
669
693
|
// Step 2: Upload directly to S3 via presigned POST
|
|
670
694
|
const form = new FormData();
|
|
@@ -753,6 +777,45 @@ class GemmeinServer {
|
|
|
753
777
|
}
|
|
754
778
|
return response.json();
|
|
755
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
782
|
+
*
|
|
783
|
+
* await g.notify(order.ownerUserId, {
|
|
784
|
+
* subject: "Your order shipped",
|
|
785
|
+
* text: "Order #142 left the warehouse today.",
|
|
786
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
787
|
+
* });
|
|
788
|
+
*
|
|
789
|
+
* A person id, never an email address — the recipient must be a verified
|
|
790
|
+
* user of this app (their address comes from the server's own record).
|
|
791
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
792
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
793
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
794
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
795
|
+
* campaigns: event-class sends
|
|
796
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
797
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
798
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
799
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
800
|
+
* for campaigns is visible in your own audit trail.
|
|
801
|
+
*/
|
|
802
|
+
async notify(personId, input) {
|
|
803
|
+
const response = await fetch(new URL("/server/notify", this.apiUrl), {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers: { "x-app-key": this.secretKey, "content-type": "application/json" },
|
|
806
|
+
body: JSON.stringify({ personId, subject: input.subject, text: input.text, ...(input.kind ? { kind: input.kind } : {}), ...(input.key ? { key: input.key } : {}) }),
|
|
807
|
+
});
|
|
808
|
+
if (!response.ok) {
|
|
809
|
+
const body = await response.json().catch(() => ({ code: "request_failed", message: `Request failed: ${response.status}` }));
|
|
810
|
+
throw new GemmeinError({
|
|
811
|
+
status: response.status,
|
|
812
|
+
code: body.code ?? "request_failed",
|
|
813
|
+
message: body.message ?? `Request failed: ${response.status}`,
|
|
814
|
+
resetAt: typeof body.resetAt === "string" ? body.resetAt : undefined,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
return response.json();
|
|
818
|
+
}
|
|
756
819
|
}
|
|
757
820
|
exports.GemmeinServer = GemmeinServer;
|
|
758
821
|
class ServerCollectionClient {
|
package/dist/index.d.cts
CHANGED
|
@@ -500,12 +500,19 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
|
|
|
500
500
|
* Pass `{ name }` so the download carries a real filename, and
|
|
501
501
|
* `{ contentType }` when the Blob has no type of its own.
|
|
502
502
|
*
|
|
503
|
+
* W7.4 — `{ for: userId }` names ONE other person who may read this file
|
|
504
|
+
* (addressed and direct collections only — the rules where recipients
|
|
505
|
+
* exist). The attachment on a direct message: upload with
|
|
506
|
+
* `{ for: recipientId }`, store the ref in the record you send them, and
|
|
507
|
+
* their `g.files.link(ref)` works. Without `for`, only you can link it.
|
|
508
|
+
*
|
|
503
509
|
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
504
510
|
* bug this replaced.
|
|
505
511
|
*/
|
|
506
512
|
upload(file: Blob | File, options?: {
|
|
507
513
|
name?: string;
|
|
508
514
|
contentType?: string;
|
|
515
|
+
for?: string;
|
|
509
516
|
}): Promise<{
|
|
510
517
|
id: string;
|
|
511
518
|
ref: FileRef;
|
|
@@ -540,6 +547,41 @@ export declare class GemmeinServer {
|
|
|
540
547
|
role: string;
|
|
541
548
|
};
|
|
542
549
|
}>;
|
|
550
|
+
/**
|
|
551
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
552
|
+
*
|
|
553
|
+
* await g.notify(order.ownerUserId, {
|
|
554
|
+
* subject: "Your order shipped",
|
|
555
|
+
* text: "Order #142 left the warehouse today.",
|
|
556
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
557
|
+
* });
|
|
558
|
+
*
|
|
559
|
+
* A person id, never an email address — the recipient must be a verified
|
|
560
|
+
* user of this app (their address comes from the server's own record).
|
|
561
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
562
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
563
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
564
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
565
|
+
* campaigns: event-class sends
|
|
566
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
567
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
568
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
569
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
570
|
+
* for campaigns is visible in your own audit trail.
|
|
571
|
+
*/
|
|
572
|
+
notify(personId: string, input: {
|
|
573
|
+
subject: string;
|
|
574
|
+
text: string;
|
|
575
|
+
kind?: "event" | "account";
|
|
576
|
+
key?: string;
|
|
577
|
+
}): Promise<{
|
|
578
|
+
sent: boolean;
|
|
579
|
+
deduped?: boolean;
|
|
580
|
+
id: string | null;
|
|
581
|
+
threadId: string | null;
|
|
582
|
+
replyRail?: boolean;
|
|
583
|
+
recorded?: boolean;
|
|
584
|
+
}>;
|
|
543
585
|
}
|
|
544
586
|
declare class ServerCollectionClient {
|
|
545
587
|
private readonly apiUrl;
|
package/dist/index.d.ts
CHANGED
|
@@ -500,12 +500,19 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
|
|
|
500
500
|
* Pass `{ name }` so the download carries a real filename, and
|
|
501
501
|
* `{ contentType }` when the Blob has no type of its own.
|
|
502
502
|
*
|
|
503
|
+
* W7.4 — `{ for: userId }` names ONE other person who may read this file
|
|
504
|
+
* (addressed and direct collections only — the rules where recipients
|
|
505
|
+
* exist). The attachment on a direct message: upload with
|
|
506
|
+
* `{ for: recipientId }`, store the ref in the record you send them, and
|
|
507
|
+
* their `g.files.link(ref)` works. Without `for`, only you can link it.
|
|
508
|
+
*
|
|
503
509
|
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
504
510
|
* bug this replaced.
|
|
505
511
|
*/
|
|
506
512
|
upload(file: Blob | File, options?: {
|
|
507
513
|
name?: string;
|
|
508
514
|
contentType?: string;
|
|
515
|
+
for?: string;
|
|
509
516
|
}): Promise<{
|
|
510
517
|
id: string;
|
|
511
518
|
ref: FileRef;
|
|
@@ -540,6 +547,41 @@ export declare class GemmeinServer {
|
|
|
540
547
|
role: string;
|
|
541
548
|
};
|
|
542
549
|
}>;
|
|
550
|
+
/**
|
|
551
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
552
|
+
*
|
|
553
|
+
* await g.notify(order.ownerUserId, {
|
|
554
|
+
* subject: "Your order shipped",
|
|
555
|
+
* text: "Order #142 left the warehouse today.",
|
|
556
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
557
|
+
* });
|
|
558
|
+
*
|
|
559
|
+
* A person id, never an email address — the recipient must be a verified
|
|
560
|
+
* user of this app (their address comes from the server's own record).
|
|
561
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
562
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
563
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
564
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
565
|
+
* campaigns: event-class sends
|
|
566
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
567
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
568
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
569
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
570
|
+
* for campaigns is visible in your own audit trail.
|
|
571
|
+
*/
|
|
572
|
+
notify(personId: string, input: {
|
|
573
|
+
subject: string;
|
|
574
|
+
text: string;
|
|
575
|
+
kind?: "event" | "account";
|
|
576
|
+
key?: string;
|
|
577
|
+
}): Promise<{
|
|
578
|
+
sent: boolean;
|
|
579
|
+
deduped?: boolean;
|
|
580
|
+
id: string | null;
|
|
581
|
+
threadId: string | null;
|
|
582
|
+
replyRail?: boolean;
|
|
583
|
+
recorded?: boolean;
|
|
584
|
+
}>;
|
|
543
585
|
}
|
|
544
586
|
declare class ServerCollectionClient {
|
|
545
587
|
private readonly apiUrl;
|
package/dist/index.js
CHANGED
|
@@ -510,6 +510,24 @@ export class CollectionClient {
|
|
|
510
510
|
inFlight = false;
|
|
511
511
|
return;
|
|
512
512
|
}
|
|
513
|
+
// An auth refusal is not transient: a signed-out or unentitled
|
|
514
|
+
// watcher polling forever writes a denied-audit row per attempt on
|
|
515
|
+
// the server (audit finding #8). Stop; a fresh watch() after
|
|
516
|
+
// sign-in starts clean.
|
|
517
|
+
if (err instanceof GemmeinError && (err.status === 401 || err.status === 403)) {
|
|
518
|
+
inFlight = false;
|
|
519
|
+
stopped = true;
|
|
520
|
+
if (timer !== undefined) {
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
timer = undefined;
|
|
523
|
+
}
|
|
524
|
+
if (typeof document !== "undefined") {
|
|
525
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
526
|
+
}
|
|
527
|
+
if (typeof console !== "undefined")
|
|
528
|
+
console.warn(`gemmein watch stopped: ${err.code} — start a new watch after signing in`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
513
531
|
// Back off; a rate limit says exactly when to come back.
|
|
514
532
|
delayMs = Math.min(Math.max(delayMs * 2, every), 60000);
|
|
515
533
|
if (err instanceof GemmeinError && err.resetAt) {
|
|
@@ -637,6 +655,12 @@ export class CollectionClient {
|
|
|
637
655
|
* Pass `{ name }` so the download carries a real filename, and
|
|
638
656
|
* `{ contentType }` when the Blob has no type of its own.
|
|
639
657
|
*
|
|
658
|
+
* W7.4 — `{ for: userId }` names ONE other person who may read this file
|
|
659
|
+
* (addressed and direct collections only — the rules where recipients
|
|
660
|
+
* exist). The attachment on a direct message: upload with
|
|
661
|
+
* `{ for: recipientId }`, store the ref in the record you send them, and
|
|
662
|
+
* their `g.files.link(ref)` works. Without `for`, only you can link it.
|
|
663
|
+
*
|
|
640
664
|
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
641
665
|
* bug this replaced.
|
|
642
666
|
*/
|
|
@@ -648,7 +672,7 @@ export class CollectionClient {
|
|
|
648
672
|
// Step 1: Get presigned upload URL
|
|
649
673
|
const presign = await this.request("/upload", {
|
|
650
674
|
method: "POST",
|
|
651
|
-
body: JSON.stringify({ name, size: file.size, contentType }),
|
|
675
|
+
body: JSON.stringify({ name, size: file.size, contentType, ...(options?.for ? { for: options.for } : {}) }),
|
|
652
676
|
});
|
|
653
677
|
// Step 2: Upload directly to S3 via presigned POST
|
|
654
678
|
const form = new FormData();
|
|
@@ -736,6 +760,45 @@ export class GemmeinServer {
|
|
|
736
760
|
}
|
|
737
761
|
return response.json();
|
|
738
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
765
|
+
*
|
|
766
|
+
* await g.notify(order.ownerUserId, {
|
|
767
|
+
* subject: "Your order shipped",
|
|
768
|
+
* text: "Order #142 left the warehouse today.",
|
|
769
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
770
|
+
* });
|
|
771
|
+
*
|
|
772
|
+
* A person id, never an email address — the recipient must be a verified
|
|
773
|
+
* user of this app (their address comes from the server's own record).
|
|
774
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
775
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
776
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
777
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
778
|
+
* campaigns: event-class sends
|
|
779
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
780
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
781
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
782
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
783
|
+
* for campaigns is visible in your own audit trail.
|
|
784
|
+
*/
|
|
785
|
+
async notify(personId, input) {
|
|
786
|
+
const response = await fetch(new URL("/server/notify", this.apiUrl), {
|
|
787
|
+
method: "POST",
|
|
788
|
+
headers: { "x-app-key": this.secretKey, "content-type": "application/json" },
|
|
789
|
+
body: JSON.stringify({ personId, subject: input.subject, text: input.text, ...(input.kind ? { kind: input.kind } : {}), ...(input.key ? { key: input.key } : {}) }),
|
|
790
|
+
});
|
|
791
|
+
if (!response.ok) {
|
|
792
|
+
const body = await response.json().catch(() => ({ code: "request_failed", message: `Request failed: ${response.status}` }));
|
|
793
|
+
throw new GemmeinError({
|
|
794
|
+
status: response.status,
|
|
795
|
+
code: body.code ?? "request_failed",
|
|
796
|
+
message: body.message ?? `Request failed: ${response.status}`,
|
|
797
|
+
resetAt: typeof body.resetAt === "string" ? body.resetAt : undefined,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
return response.json();
|
|
801
|
+
}
|
|
739
802
|
}
|
|
740
803
|
class ServerCollectionClient {
|
|
741
804
|
constructor(apiUrl, secretKey, name) {
|
package/llms.txt
CHANGED
|
@@ -243,6 +243,35 @@ go-live. Everything else is yours.
|
|
|
243
243
|
isn't reported (it isn't deleted) — it clears on the next tab-return
|
|
244
244
|
refresh. Watch suits lists you'd actually render (up to a few thousand
|
|
245
245
|
records); it re-reads the full list on every tab return.
|
|
246
|
+
- Telling a customer something happened ("order shipped", "booking
|
|
247
|
+
confirmed", "new reply") is built in — your SERVER calls it with a
|
|
248
|
+
secret key; there is no SMTP, no email service to wire, and it cannot
|
|
249
|
+
be called from the browser:
|
|
250
|
+
|
|
251
|
+
import { gemmeinServer } from "@gemmein/sdk";
|
|
252
|
+
const g = gemmeinServer(process.env.GEMMEIN_SECRET_KEY);
|
|
253
|
+
await g.notify(order.ownerUserId, {
|
|
254
|
+
subject: "Your order shipped",
|
|
255
|
+
text: "Order #142 left the warehouse today.",
|
|
256
|
+
key: "order-142-shipped", // retries can't double-send
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
A person id, NEVER an email address — the recipient must be a verified
|
|
260
|
+
user of your app (404 not_a_customer otherwise; the address comes from
|
|
261
|
+
the server's own record). Plain text; a short "you have an account with
|
|
262
|
+
{your app}" footer is added for you. The email is branded as your app
|
|
263
|
+
(your verified domain when you have one). Every send appears in your
|
|
264
|
+
dashboard Inbox as a conversation; a customer's REPLY lands there too
|
|
265
|
+
once your domain's receiving is verified (the Domains page) — before
|
|
266
|
+
that the mail has no reply path, so if you expect answers, say where to
|
|
267
|
+
write. The response's `replyRail: true|false` states which is true right
|
|
268
|
+
now. This is for EVENTS, not campaigns:
|
|
269
|
+
about 5 event-class sends per person per day (429 notify_capped with
|
|
270
|
+
resetAt says when). Account activity — a new sign-in, an access change,
|
|
271
|
+
a billing problem — passes `kind: "account"`, which skips the per-person
|
|
272
|
+
cap (a security notice never loses to five order emails); misusing it
|
|
273
|
+
for campaigns shows in your own audit trail. In `gemmein dev` the send
|
|
274
|
+
prints in the terminal (NOTIFY · …) instead of mailing.
|
|
246
275
|
- Linking records (author on a post, product on an order): store the other
|
|
247
276
|
record's id in a field (`authorProfileId: profile.id`) — in collections
|
|
248
277
|
users write (community, shared, direct) the server learns it's a link;
|
|
@@ -262,7 +291,7 @@ go-live. Everything else is yours.
|
|
|
262
291
|
- Images & files: NEVER base64 into record data and NEVER wire up your own
|
|
263
292
|
storage bucket — uploads are built in:
|
|
264
293
|
`const file = await g.collection("posts").upload(blob, { name })`
|
|
265
|
-
→ `{ id, ref, contentType, sizeBytes }`. Store `file.ref` (`"file:
|
|
294
|
+
→ `{ id, ref, contentType, sizeBytes }`. Store `file.ref` (`"file:9f2c…"` — a UUID form)
|
|
266
295
|
in a record field like any text — that's how a record "has" an image or
|
|
267
296
|
a document.
|
|
268
297
|
Store the REFERENCE, never a URL: a reference never expires and grants
|
|
@@ -289,14 +318,27 @@ go-live. Everything else is yours.
|
|
|
289
318
|
the console shows by name).
|
|
290
319
|
Honest bound: revoking access stops NEW links immediately; a link already
|
|
291
320
|
issued works until it expires. Gemmein controls delivery, it can't take
|
|
292
|
-
back a file someone already downloaded.
|
|
321
|
+
back a file someone already downloaded. A file can name ONE other reader:
|
|
322
|
+
`upload(blob, { for: userId })` (addressed/direct collections only) — the
|
|
323
|
+
direct message's attachment; the recipient's `files.link` works, everyone
|
|
324
|
+
else's still refuses. Without `for`, only the uploader can link it —
|
|
325
|
+
including in a client portal, where the OWNER hands a client a document
|
|
326
|
+
from the dashboard (attach it on the client's record there).
|
|
293
327
|
- Shapes: collections your users write (community, shared, direct) have a
|
|
294
328
|
SHAPE — the set of fields allowed. While your human builds, the server
|
|
295
329
|
learns it from your writes automatically (nothing to do); when they go
|
|
296
|
-
live it locks.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
330
|
+
live it locks. Field kinds the shape learns: text, number, yes/no, list,
|
|
331
|
+
link (a stored record id), json, and FILE — a field that held an
|
|
332
|
+
upload()'s ref learns as a file field — and WHICH class (an image field
|
|
333
|
+
vs a document field, from what you actually uploaded). Once locked, the
|
|
334
|
+
field accepts only a reference to a real, confirmed file of this app
|
|
335
|
+
that the writer may read, of the right class — a ZIP into a profile
|
|
336
|
+
photo field is refused with the teach, as is a made-up or foreign ref
|
|
337
|
+
("file not found"). A 400 invalid_shape means the field isn't in the locked
|
|
338
|
+
shape or the value doesn't fit its kind. A live shape is sealed and
|
|
339
|
+
cannot take new fields — stop, tell your human which field you needed,
|
|
340
|
+
and send only the fields the shape already has. Never rename fields to
|
|
341
|
+
dodge it.
|
|
300
342
|
- Contention (bookings, slugs, stock, shared edits): when two users can race
|
|
301
343
|
for the same thing, a permission model can't save you — preconditions do,
|
|
302
344
|
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.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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",
|