@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 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.2",
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",