@gemmein/sdk 0.4.2 → 0.4.3
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 +57 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +57 -0
- package/llms.txt +29 -0
- 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) {
|
|
@@ -753,6 +771,45 @@ class GemmeinServer {
|
|
|
753
771
|
}
|
|
754
772
|
return response.json();
|
|
755
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
776
|
+
*
|
|
777
|
+
* await g.notify(order.ownerUserId, {
|
|
778
|
+
* subject: "Your order shipped",
|
|
779
|
+
* text: "Order #142 left the warehouse today.",
|
|
780
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
781
|
+
* });
|
|
782
|
+
*
|
|
783
|
+
* A person id, never an email address — the recipient must be a verified
|
|
784
|
+
* user of this app (their address comes from the server's own record).
|
|
785
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
786
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
787
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
788
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
789
|
+
* campaigns: event-class sends
|
|
790
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
791
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
792
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
793
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
794
|
+
* for campaigns is visible in your own audit trail.
|
|
795
|
+
*/
|
|
796
|
+
async notify(personId, input) {
|
|
797
|
+
const response = await fetch(new URL("/server/notify", this.apiUrl), {
|
|
798
|
+
method: "POST",
|
|
799
|
+
headers: { "x-app-key": this.secretKey, "content-type": "application/json" },
|
|
800
|
+
body: JSON.stringify({ personId, subject: input.subject, text: input.text, ...(input.kind ? { kind: input.kind } : {}), ...(input.key ? { key: input.key } : {}) }),
|
|
801
|
+
});
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
const body = await response.json().catch(() => ({ code: "request_failed", message: `Request failed: ${response.status}` }));
|
|
804
|
+
throw new GemmeinError({
|
|
805
|
+
status: response.status,
|
|
806
|
+
code: body.code ?? "request_failed",
|
|
807
|
+
message: body.message ?? `Request failed: ${response.status}`,
|
|
808
|
+
resetAt: typeof body.resetAt === "string" ? body.resetAt : undefined,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return response.json();
|
|
812
|
+
}
|
|
756
813
|
}
|
|
757
814
|
exports.GemmeinServer = GemmeinServer;
|
|
758
815
|
class ServerCollectionClient {
|
package/dist/index.d.cts
CHANGED
|
@@ -540,6 +540,41 @@ export declare class GemmeinServer {
|
|
|
540
540
|
role: string;
|
|
541
541
|
};
|
|
542
542
|
}>;
|
|
543
|
+
/**
|
|
544
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
545
|
+
*
|
|
546
|
+
* await g.notify(order.ownerUserId, {
|
|
547
|
+
* subject: "Your order shipped",
|
|
548
|
+
* text: "Order #142 left the warehouse today.",
|
|
549
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
550
|
+
* });
|
|
551
|
+
*
|
|
552
|
+
* A person id, never an email address — the recipient must be a verified
|
|
553
|
+
* user of this app (their address comes from the server's own record).
|
|
554
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
555
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
556
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
557
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
558
|
+
* campaigns: event-class sends
|
|
559
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
560
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
561
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
562
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
563
|
+
* for campaigns is visible in your own audit trail.
|
|
564
|
+
*/
|
|
565
|
+
notify(personId: string, input: {
|
|
566
|
+
subject: string;
|
|
567
|
+
text: string;
|
|
568
|
+
kind?: "event" | "account";
|
|
569
|
+
key?: string;
|
|
570
|
+
}): Promise<{
|
|
571
|
+
sent: boolean;
|
|
572
|
+
deduped?: boolean;
|
|
573
|
+
id: string | null;
|
|
574
|
+
threadId: string | null;
|
|
575
|
+
replyRail?: boolean;
|
|
576
|
+
recorded?: boolean;
|
|
577
|
+
}>;
|
|
543
578
|
}
|
|
544
579
|
declare class ServerCollectionClient {
|
|
545
580
|
private readonly apiUrl;
|
package/dist/index.d.ts
CHANGED
|
@@ -540,6 +540,41 @@ export declare class GemmeinServer {
|
|
|
540
540
|
role: string;
|
|
541
541
|
};
|
|
542
542
|
}>;
|
|
543
|
+
/**
|
|
544
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
545
|
+
*
|
|
546
|
+
* await g.notify(order.ownerUserId, {
|
|
547
|
+
* subject: "Your order shipped",
|
|
548
|
+
* text: "Order #142 left the warehouse today.",
|
|
549
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
550
|
+
* });
|
|
551
|
+
*
|
|
552
|
+
* A person id, never an email address — the recipient must be a verified
|
|
553
|
+
* user of this app (their address comes from the server's own record).
|
|
554
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
555
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
556
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
557
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
558
|
+
* campaigns: event-class sends
|
|
559
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
560
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
561
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
562
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
563
|
+
* for campaigns is visible in your own audit trail.
|
|
564
|
+
*/
|
|
565
|
+
notify(personId: string, input: {
|
|
566
|
+
subject: string;
|
|
567
|
+
text: string;
|
|
568
|
+
kind?: "event" | "account";
|
|
569
|
+
key?: string;
|
|
570
|
+
}): Promise<{
|
|
571
|
+
sent: boolean;
|
|
572
|
+
deduped?: boolean;
|
|
573
|
+
id: string | null;
|
|
574
|
+
threadId: string | null;
|
|
575
|
+
replyRail?: boolean;
|
|
576
|
+
recorded?: boolean;
|
|
577
|
+
}>;
|
|
543
578
|
}
|
|
544
579
|
declare class ServerCollectionClient {
|
|
545
580
|
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) {
|
|
@@ -736,6 +754,45 @@ export class GemmeinServer {
|
|
|
736
754
|
}
|
|
737
755
|
return response.json();
|
|
738
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* W7.3 — tell one of YOUR OWN people that something happened, by email:
|
|
759
|
+
*
|
|
760
|
+
* await g.notify(order.ownerUserId, {
|
|
761
|
+
* subject: "Your order shipped",
|
|
762
|
+
* text: "Order #142 left the warehouse today.",
|
|
763
|
+
* key: "order-142-shipped", // optional: retries can't double-send
|
|
764
|
+
* });
|
|
765
|
+
*
|
|
766
|
+
* A person id, never an email address — the recipient must be a verified
|
|
767
|
+
* user of this app (their address comes from the server's own record).
|
|
768
|
+
* Plain text (an account-context footer is added). The email is branded
|
|
769
|
+
* as your app; the send appears in your dashboard Inbox, and a customer's
|
|
770
|
+
* reply lands there too once your domain's receiving is verified — the
|
|
771
|
+
* response's `replyRail` says which is true. This is for EVENTS, not
|
|
772
|
+
* campaigns: event-class sends
|
|
773
|
+
* are capped at about 5 per person per day (429 notify_capped, with
|
|
774
|
+
* resetAt). Pass `kind: "account"` for account activity — a new sign-in,
|
|
775
|
+
* an access change, a billing problem — which skips the per-person cap
|
|
776
|
+
* (a security notice must never lose to five order emails); misusing it
|
|
777
|
+
* for campaigns is visible in your own audit trail.
|
|
778
|
+
*/
|
|
779
|
+
async notify(personId, input) {
|
|
780
|
+
const response = await fetch(new URL("/server/notify", this.apiUrl), {
|
|
781
|
+
method: "POST",
|
|
782
|
+
headers: { "x-app-key": this.secretKey, "content-type": "application/json" },
|
|
783
|
+
body: JSON.stringify({ personId, subject: input.subject, text: input.text, ...(input.kind ? { kind: input.kind } : {}), ...(input.key ? { key: input.key } : {}) }),
|
|
784
|
+
});
|
|
785
|
+
if (!response.ok) {
|
|
786
|
+
const body = await response.json().catch(() => ({ code: "request_failed", message: `Request failed: ${response.status}` }));
|
|
787
|
+
throw new GemmeinError({
|
|
788
|
+
status: response.status,
|
|
789
|
+
code: body.code ?? "request_failed",
|
|
790
|
+
message: body.message ?? `Request failed: ${response.status}`,
|
|
791
|
+
resetAt: typeof body.resetAt === "string" ? body.resetAt : undefined,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return response.json();
|
|
795
|
+
}
|
|
739
796
|
}
|
|
740
797
|
class ServerCollectionClient {
|
|
741
798
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemmein/sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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",
|