@omen.dog/sdk 1.0.0 → 1.1.0

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.js CHANGED
@@ -20,12 +20,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ASK_GROWN_UP_STATES: () => ASK_GROWN_UP_STATES,
24
+ CHILD_LOGIN_STATES: () => CHILD_LOGIN_STATES,
25
+ ChildLogin: () => ChildLogin,
26
+ ENTRY_STATES: () => ENTRY_STATES,
23
27
  OmenAuthError: () => OmenAuthError,
24
28
  OmenClient: () => OmenClient,
25
29
  OmenError: () => OmenError,
26
30
  OmenNotFoundError: () => OmenNotFoundError,
27
31
  OmenRateLimitError: () => OmenRateLimitError,
28
- OmenValidationError: () => OmenValidationError
32
+ OmenValidationError: () => OmenValidationError,
33
+ deriveInitialState: () => deriveInitialState,
34
+ displayGroup: () => displayGroup,
35
+ reduce: () => reduce
29
36
  });
30
37
  module.exports = __toCommonJS(index_exports);
31
38
 
@@ -285,6 +292,27 @@ var ItemsNamespace = class {
285
292
  body: { reason }
286
293
  });
287
294
  }
295
+ /**
296
+ * Award a catalog item (open-shop cosmetic or avatar-editor trait) to a user,
297
+ * paid for from your Sparks pool at the item's list price. Custom items use
298
+ * `issue()` and are free; catalog items draw the pool down. Re-awarding an item
299
+ * the user already owns is a no-op (no charge).
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * await omen.items.awardCatalog({
304
+ * userId, catalogType: 'store', catalogItemId: 'theme_lava',
305
+ * });
306
+ * ```
307
+ */
308
+ async awardCatalog(options) {
309
+ const { idempotencyKey, ...body } = options;
310
+ return this.http.request(`/api/v1/apps/${this.appId}/items/award-catalog`, {
311
+ method: "POST",
312
+ body,
313
+ headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0
314
+ });
315
+ }
288
316
  };
289
317
 
290
318
  // src/namespaces/collections.ts
@@ -507,6 +535,610 @@ function timingSafeEqual(a, b) {
507
535
  return result === 0;
508
536
  }
509
537
 
538
+ // src/namespaces/products.ts
539
+ var ProductsNamespace = class {
540
+ constructor(http, appId) {
541
+ this.http = http;
542
+ this.appId = appId;
543
+ }
544
+ /**
545
+ * Create a new product.
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * const product = await omen.products.create({
550
+ * name: 'Premium Sword',
551
+ * type: 'one_time',
552
+ * priceCents: 499,
553
+ * });
554
+ * ```
555
+ */
556
+ async create(options) {
557
+ return this.http.request(`/api/v1/apps/${this.appId}/products`, {
558
+ method: "POST",
559
+ body: options
560
+ });
561
+ }
562
+ /**
563
+ * List all products for this app.
564
+ *
565
+ * @param activeOnly - If true, only return active products. Defaults to false.
566
+ */
567
+ async list(activeOnly) {
568
+ return this.http.request(`/api/v1/apps/${this.appId}/products`, {
569
+ query: activeOnly ? { active: "true" } : void 0
570
+ });
571
+ }
572
+ /**
573
+ * Update an existing product.
574
+ *
575
+ * @param productId - The product ID to update.
576
+ * @param options - Fields to update.
577
+ */
578
+ async update(productId, options) {
579
+ return this.http.request(`/api/v1/apps/${this.appId}/products`, {
580
+ method: "PATCH",
581
+ query: { productId },
582
+ body: options
583
+ });
584
+ }
585
+ /**
586
+ * Deactivate a product. Deactivated products cannot be purchased but existing
587
+ * purchases remain valid.
588
+ *
589
+ * @param productId - The product ID to deactivate.
590
+ */
591
+ async deactivate(productId) {
592
+ await this.http.request(`/api/v1/apps/${this.appId}/products`, {
593
+ method: "DELETE",
594
+ query: { productId }
595
+ });
596
+ }
597
+ };
598
+
599
+ // src/namespaces/multiplayer.ts
600
+ var MultiplayerNamespace = class {
601
+ constructor(http, appId) {
602
+ this.http = http;
603
+ this.appId = appId;
604
+ }
605
+ /**
606
+ * Get the current multiplayer configuration for this app.
607
+ */
608
+ async getConfig() {
609
+ const res = await this.http.request(
610
+ `/api/v1/apps/${this.appId}/multiplayer-config`
611
+ );
612
+ return res.multiplayerConfig;
613
+ }
614
+ /**
615
+ * Enable or update managed multiplayer configuration.
616
+ *
617
+ * @example
618
+ * ```ts
619
+ * await omen.multiplayer.updateConfig({
620
+ * managed: true,
621
+ * maxPlayers: 8,
622
+ * voiceEnabled: true,
623
+ * teamMode: 'manual',
624
+ * });
625
+ * ```
626
+ */
627
+ async updateConfig(options) {
628
+ const res = await this.http.request(
629
+ `/api/v1/apps/${this.appId}/multiplayer-config`,
630
+ { method: "PUT", body: options }
631
+ );
632
+ return res.multiplayerConfig;
633
+ }
634
+ /**
635
+ * Deploy authoritative room logic (Pro tier required).
636
+ * The bundle is a JavaScript file that exports room lifecycle hooks.
637
+ *
638
+ * @param bundle - The JavaScript source code as a string.
639
+ */
640
+ async deployLogic(bundle) {
641
+ return this.http.request(`/api/v1/apps/${this.appId}/room-logic`, {
642
+ method: "PUT",
643
+ body: { bundle }
644
+ });
645
+ }
646
+ /**
647
+ * List deployed room logic versions.
648
+ */
649
+ async listLogicVersions() {
650
+ const res = await this.http.request(
651
+ `/api/v1/apps/${this.appId}/room-logic/versions`
652
+ );
653
+ return res.versions;
654
+ }
655
+ /**
656
+ * Roll back to a previous room logic version.
657
+ *
658
+ * @param version - The version number to roll back to.
659
+ */
660
+ async rollbackLogic(version) {
661
+ return this.http.request(`/api/v1/apps/${this.appId}/room-logic/rollback`, {
662
+ method: "POST",
663
+ body: { version }
664
+ });
665
+ }
666
+ };
667
+
668
+ // src/namespaces/notifications.ts
669
+ var NotificationsNamespace = class {
670
+ constructor(http, appId) {
671
+ this.http = http;
672
+ this.appId = appId;
673
+ }
674
+ /**
675
+ * Send a notification to a user.
676
+ *
677
+ * @example
678
+ * ```ts
679
+ * await omen.notifications.send({
680
+ * userId: 'cmm0tzco8...',
681
+ * title: 'New high score!',
682
+ * body: 'Someone beat your record on Pixel Duel.',
683
+ * category: 'app',
684
+ * actionUrl: '/creations/pixel-duel',
685
+ * });
686
+ * ```
687
+ */
688
+ async send(options) {
689
+ return this.http.request(`/api/v1/apps/${this.appId}/notifications/send`, {
690
+ method: "POST",
691
+ body: options
692
+ });
693
+ }
694
+ };
695
+
696
+ // src/namespaces/sparks.ts
697
+ var import_node_crypto = require("crypto");
698
+ var SparksNamespace = class {
699
+ constructor(http, appId) {
700
+ this.http = http;
701
+ this.appId = appId;
702
+ }
703
+ /**
704
+ * Award Sparks to a single user. Pass `idempotencyKey` to make retries safe.
705
+ *
706
+ * @example
707
+ * ```ts
708
+ * await omen.sparks.award({
709
+ * userId: 'cmm0tzco8...',
710
+ * amount: 250,
711
+ * reason: 'Beat level 10',
712
+ * idempotencyKey: `level10:${userId}`,
713
+ * });
714
+ * ```
715
+ */
716
+ async award(options) {
717
+ const { idempotencyKey, ...body } = options;
718
+ return this.http.request(`/api/v1/apps/${this.appId}/sparks/award`, {
719
+ method: "POST",
720
+ body,
721
+ headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0
722
+ });
723
+ }
724
+ /**
725
+ * Award Sparks to many users in one request (max 100). Best-effort per item:
726
+ * a single bad/underfunded entry does not fail the rest. Inspect `results`.
727
+ */
728
+ async awardBatch(awards) {
729
+ return this.http.request(`/api/v1/apps/${this.appId}/sparks/award-batch`, {
730
+ method: "POST",
731
+ body: { awards }
732
+ });
733
+ }
734
+ /** Current pool status: balance, lifetime stats, and per-app budgets. */
735
+ async pool() {
736
+ return this.http.request(`/api/v1/apps/${this.appId}/sparks/pool`);
737
+ }
738
+ /**
739
+ * Mint a short-lived display token for the UI kit, scoped to one user. Sign
740
+ * with your app's OAuth client SECRET (from the Developer Portal) — never ship
741
+ * the secret to the browser; call this on your backend and pass the token to
742
+ * `<omen-sparks-balance token="...">` / `<omen-inventory token="...">`.
743
+ *
744
+ * Synchronous (no network). Default TTL 10 min, max 1 hour.
745
+ *
746
+ * @example
747
+ * ```ts
748
+ * const token = omen.sparks.displayToken({ userId, secret: process.env.OMEN_CLIENT_SECRET! });
749
+ * ```
750
+ */
751
+ displayToken(options) {
752
+ if (!options.userId || !options.secret) throw new Error("userId and secret are required");
753
+ const ttl = Math.min(Math.max(1, Math.floor(options.ttlSeconds ?? 600)), 3600);
754
+ const exp = Math.floor(Date.now() / 1e3) + ttl;
755
+ const payloadB64 = Buffer.from(JSON.stringify({ a: this.appId, u: options.userId, e: exp })).toString("base64url");
756
+ const sig = (0, import_node_crypto.createHmac)("sha256", options.secret).update(payloadB64).digest("base64url");
757
+ return `${payloadB64}.${sig}`;
758
+ }
759
+ };
760
+
761
+ // src/namespaces/emails.ts
762
+ var EmailsNamespace = class {
763
+ constructor(http, appId) {
764
+ this.http = http;
765
+ this.appId = appId;
766
+ }
767
+ /**
768
+ * Send one email (up to 10 recipients). No attachments; 256 KB body max.
769
+ *
770
+ * @example
771
+ * ```ts
772
+ * await omen.emails.send({
773
+ * from: 'hello@yourdomain.com',
774
+ * fromName: 'My App',
775
+ * to: player.email,
776
+ * subject: 'Your weekly recap',
777
+ * html: '<h1>Nice run!</h1>',
778
+ * });
779
+ * ```
780
+ */
781
+ async send(options) {
782
+ return this.http.request(`/api/v1/apps/${this.appId}/emails/send`, {
783
+ method: "POST",
784
+ body: options
785
+ });
786
+ }
787
+ /** Recent sends for this app plus the rolling 24h quota. */
788
+ async log(limit = 50) {
789
+ return this.http.request(`/api/v1/apps/${this.appId}/emails/log?limit=${limit}`);
790
+ }
791
+ };
792
+
793
+ // src/namespaces/avatar.ts
794
+ var import_node_crypto2 = require("crypto");
795
+ var AvatarNamespace = class {
796
+ constructor(appId, baseUrl) {
797
+ this.appId = appId;
798
+ this.baseUrl = baseUrl;
799
+ }
800
+ /**
801
+ * Mint a short-lived WRITE-scoped editor token for one user (backend only —
802
+ * never expose your client secret to the browser). Pass it to
803
+ * `<omen-avatar-editor token="...">`.
804
+ *
805
+ * @example
806
+ * ```ts
807
+ * const token = omen.avatar.editorToken({ userId, secret: process.env.OMEN_CLIENT_SECRET! });
808
+ * ```
809
+ */
810
+ editorToken(options) {
811
+ if (!options.userId || !options.secret) throw new Error("userId and secret are required");
812
+ const ttl = Math.min(Math.max(1, Math.floor(options.ttlSeconds ?? 600)), 3600);
813
+ const exp = Math.floor(Date.now() / 1e3) + ttl;
814
+ const payloadB64 = Buffer.from(
815
+ JSON.stringify({ a: this.appId, u: options.userId, e: exp, t: "editor" })
816
+ ).toString("base64url");
817
+ const sig = (0, import_node_crypto2.createHmac)("sha256", options.secret).update(payloadB64).digest("base64url");
818
+ return `${payloadB64}.${sig}`;
819
+ }
820
+ /**
821
+ * Public render URL for a user's shared avatar (SVG; append `&format=png`
822
+ * where supported). Pass `version` (from the `avatar:saved` event or the
823
+ * `user.avatar_updated` webhook) to bust caches after an edit.
824
+ */
825
+ renderUrl(userId, options) {
826
+ const v = options?.version ? `&v=${encodeURIComponent(options.version)}` : "";
827
+ return `${this.baseUrl}/api/v1/avatar/render?userId=${encodeURIComponent(userId)}${v}`;
828
+ }
829
+ /**
830
+ * Enumerate the shared avatar catalog — every trait id, name, rarity and
831
+ * Sparks list price. Public, no auth. Trait ids (`category/file.svg` paths)
832
+ * are also public SVGs at `{baseUrl}/avatar/{id}` for previews, and feed
833
+ * `omen.items.awardCatalog({ catalogType: 'avatar_trait', catalogItemId })`
834
+ * for pool-funded gifts.
835
+ *
836
+ * Pass a user's editor token and each trait's `locked` flag reflects that
837
+ * user's ownership instead of the anonymous default.
838
+ */
839
+ async catalog(options) {
840
+ const res = await fetch(`${this.baseUrl}/api/v1/avatar/catalog`, {
841
+ headers: options?.editorToken ? { Authorization: `Bearer ${options.editorToken}` } : void 0
842
+ });
843
+ if (!res.ok) throw new Error(`Failed to load avatar catalog (HTTP ${res.status})`);
844
+ return await res.json();
845
+ }
846
+ };
847
+
848
+ // src/childLogin/rfc8628.js
849
+ function mapPollResponse(httpStatus, body) {
850
+ body = body || {};
851
+ if (httpStatus >= 200 && httpStatus < 300 && body.access_token) {
852
+ return {
853
+ status: "approved",
854
+ tokens: {
855
+ access_token: body.access_token,
856
+ refresh_token: body.refresh_token,
857
+ token_type: body.token_type || "Bearer",
858
+ expires_in: typeof body.expires_in === "number" ? body.expires_in : void 0,
859
+ scope: body.scope
860
+ },
861
+ rebind: body.rebind || null,
862
+ deviceToken: body.device_token || null
863
+ };
864
+ }
865
+ switch (body.error) {
866
+ case "authorization_pending":
867
+ return { status: "pending" };
868
+ case "slow_down":
869
+ return { status: "slow_down" };
870
+ case "access_denied":
871
+ return { status: "denied" };
872
+ case "expired_token":
873
+ return { status: "expired" };
874
+ default:
875
+ return { status: "error", error: body.error || `http_${httpStatus}` };
876
+ }
877
+ }
878
+ var SLOW_DOWN_INCREMENT_SECONDS = 5;
879
+ var MIN_POLL_INTERVAL_SECONDS = 1;
880
+ function nextInterval(currentInterval, status) {
881
+ const base = Number.isFinite(currentInterval) && currentInterval >= MIN_POLL_INTERVAL_SECONDS ? currentInterval : MIN_POLL_INTERVAL_SECONDS;
882
+ if (status === "slow_down") return base + SLOW_DOWN_INCREMENT_SECONDS;
883
+ return base;
884
+ }
885
+ function isTerminalStatus(status) {
886
+ return status === "approved" || status === "denied" || status === "expired" || status === "error";
887
+ }
888
+
889
+ // src/childLogin/session.js
890
+ var DEFAULT_ACCESS_TTL_SECONDS = 86400;
891
+ var DEFAULT_SKEW_MS = 3e4;
892
+ function seal(tokens, opts) {
893
+ if (!tokens || !tokens.access_token) {
894
+ throw new Error("seal(): tokens.access_token is required");
895
+ }
896
+ const now = opts && typeof opts.now === "number" ? opts.now : Date.now();
897
+ const ttlSeconds = typeof tokens.expires_in === "number" ? tokens.expires_in : DEFAULT_ACCESS_TTL_SECONDS;
898
+ const record = {
899
+ accessToken: tokens.access_token,
900
+ refreshToken: tokens.refresh_token || null,
901
+ scope: tokens.scope || "child",
902
+ accessExpiresAt: now + ttlSeconds * 1e3
903
+ };
904
+ const deviceToken = opts && opts.deviceToken || tokens.device_token || null;
905
+ if (deviceToken) record.deviceToken = deviceToken;
906
+ return record;
907
+ }
908
+ function accessExpired(record, opts) {
909
+ if (!record || typeof record.accessExpiresAt !== "number") return true;
910
+ const now = opts && typeof opts.now === "number" ? opts.now : Date.now();
911
+ const skew = opts && typeof opts.skewMs === "number" ? opts.skewMs : DEFAULT_SKEW_MS;
912
+ return now >= record.accessExpiresAt - skew;
913
+ }
914
+
915
+ // src/childLogin/webhook.js
916
+ var import_node_crypto3 = require("crypto");
917
+ function verifyWebhook(payload, signature, secret) {
918
+ if (typeof payload !== "string" || !signature || !secret) return false;
919
+ const expected = (0, import_node_crypto3.createHmac)("sha256", secret).update(payload).digest("hex");
920
+ const a = Buffer.from(expected, "utf8");
921
+ const b = Buffer.from(String(signature), "utf8");
922
+ if (a.length !== b.length) return false;
923
+ return (0, import_node_crypto3.timingSafeEqual)(a, b);
924
+ }
925
+ function parseChildLoginApproved(payload, signature, secret) {
926
+ if (!verifyWebhook(payload, signature, secret)) return null;
927
+ try {
928
+ return JSON.parse(payload);
929
+ } catch {
930
+ return null;
931
+ }
932
+ }
933
+
934
+ // src/childLogin/stateMachine.js
935
+ var CHILD_LOGIN_STATES = [
936
+ "trusted",
937
+ // this device is pre-authorised → one tap
938
+ "known",
939
+ // we know who she is (cached identity) → "ask a grown-up"
940
+ "cold",
941
+ // no hint → type a username
942
+ "pending",
943
+ // a grown-up has been asked; waiting (calm, no timer)
944
+ "approved",
945
+ // she's in
946
+ "denied",
947
+ // a grown-up said "not now"
948
+ "blocked",
949
+ // a grown-up blocked this app
950
+ "paused",
951
+ // her account is paused everywhere
952
+ "expired",
953
+ // the request timed out; let's try again
954
+ "offline"
955
+ // no connection; render her card from cache, can't log in
956
+ ];
957
+ var ASK_GROWN_UP_STATES = ["denied", "blocked", "paused"];
958
+ var ENTRY_STATES = ["trusted", "known", "cold"];
959
+ function deriveInitialState(ctx) {
960
+ ctx = ctx || {};
961
+ if (ctx.online === false) return "offline";
962
+ if (ctx.trustedDevice) return "trusted";
963
+ if (ctx.identity) return "known";
964
+ return "cold";
965
+ }
966
+ var TERMINAL = {
967
+ approved: "approved",
968
+ denied: "denied",
969
+ blocked: "blocked",
970
+ paused: "paused",
971
+ expired: "expired",
972
+ offline: "offline"
973
+ };
974
+ function reduce(state, signal, ctx) {
975
+ if (signal in TERMINAL) return TERMINAL[signal];
976
+ if (signal === "reset") return deriveInitialState(ctx);
977
+ if (signal === "online") return state === "offline" ? deriveInitialState(ctx) : state;
978
+ if (ENTRY_STATES.includes(state) && (signal === "ask" || signal === "pending")) {
979
+ return "pending";
980
+ }
981
+ if (state === "pending" && (signal === "pending" || signal === "slow_down")) {
982
+ return "pending";
983
+ }
984
+ return state;
985
+ }
986
+ function displayGroup(state) {
987
+ if (ENTRY_STATES.includes(state)) return "ready";
988
+ if (state === "pending") return "waiting";
989
+ if (state === "approved") return "approved";
990
+ if (ASK_GROWN_UP_STATES.includes(state)) return "ask-grown-up";
991
+ if (state === "expired") return "expired";
992
+ return "offline";
993
+ }
994
+
995
+ // src/childLogin/index.ts
996
+ var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
997
+ var ChildLogin = class {
998
+ clientId;
999
+ clientSecret;
1000
+ webhookSecret;
1001
+ baseUrl;
1002
+ constructor(options) {
1003
+ if (!options?.clientId) throw new Error("ChildLogin: clientId is required");
1004
+ this.clientId = options.clientId;
1005
+ this.clientSecret = options.clientSecret;
1006
+ this.webhookSecret = options.webhookSecret;
1007
+ this.baseUrl = (options.baseUrl ?? "https://omen.dog").replace(/\/$/, "");
1008
+ }
1009
+ /** Start a child device-authorization grant. `scope:'child'` is always sent. */
1010
+ async deviceStart(options = {}) {
1011
+ const { status, body } = await this.post("/api/oauth/device", {
1012
+ client_id: this.clientId,
1013
+ scope: "child",
1014
+ login_hint: options.loginHint,
1015
+ device_id: options.deviceId,
1016
+ device_name: options.deviceName,
1017
+ device_token: options.deviceToken
1018
+ });
1019
+ if (status >= 400) throw oauthError(status, body);
1020
+ return body;
1021
+ }
1022
+ /**
1023
+ * Tell Omen which child is logging in. With `username` this is the cold-start
1024
+ * path (no prior `login_hint`); without it, it re-pings the family for a known
1025
+ * child. Always resolves `{ ok: true }` (enumeration-safe) on the cold path.
1026
+ */
1027
+ async notify(args) {
1028
+ const { status, body } = await this.post("/api/oauth/device/notify", {
1029
+ device_code: args.device_code,
1030
+ username: args.username
1031
+ });
1032
+ if (status >= 400) throw oauthError(status, body);
1033
+ return body;
1034
+ }
1035
+ /**
1036
+ * Poll the token endpoint once. Returns a calm status union — `pending`,
1037
+ * `slow_down`, `denied`, `expired`, `error`, or `approved` (with tokens,
1038
+ * rebind blob and one-time device_token). Never throws on a normal RFC 8628
1039
+ * polling response; only network failures reject.
1040
+ */
1041
+ async poll(deviceCode) {
1042
+ const { status, body } = await this.post("/api/oauth/token", {
1043
+ grant_type: DEVICE_GRANT_TYPE,
1044
+ device_code: deviceCode,
1045
+ client_id: this.clientId
1046
+ });
1047
+ return mapPollResponse(status, body);
1048
+ }
1049
+ /**
1050
+ * Poll until the grant reaches a terminal state, honouring `interval` and
1051
+ * widening it by 5s on every `slow_down` (RFC 8628 §3.5). Prefer driving the
1052
+ * flip from the `child.login.approved` webhook; use this as the fallback.
1053
+ */
1054
+ async pollUntil(deviceCode, options = {}) {
1055
+ let interval = options.intervalSeconds ?? 5;
1056
+ for (; ; ) {
1057
+ if (options.signal?.aborted) throw new DOMException("Aborted", "AbortError");
1058
+ const result = await this.poll(deviceCode);
1059
+ if (isTerminalStatus(result.status)) return result;
1060
+ options.onStatus?.(result.status);
1061
+ interval = nextInterval(interval, result.status);
1062
+ await sleep(interval * 1e3, options.signal);
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Refresh a child access token. `scope:'child'` is preserved across rotation,
1067
+ * and the old refresh token is single-use (Omen rotates it).
1068
+ */
1069
+ async refresh(refreshToken) {
1070
+ const { status, body } = await this.post("/api/oauth/token", {
1071
+ grant_type: "refresh_token",
1072
+ refresh_token: refreshToken,
1073
+ client_id: this.clientId,
1074
+ client_secret: this.clientSecret
1075
+ });
1076
+ if (status >= 400) throw oauthError(status, body);
1077
+ return body;
1078
+ }
1079
+ /** Fetch the consent-gated identity bundle with the child's access token. */
1080
+ async childIdentity(accessToken) {
1081
+ const res = await fetch(`${this.baseUrl}/api/oauth/child-identity`, {
1082
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" }
1083
+ });
1084
+ const body = await res.json().catch(() => ({}));
1085
+ if (!res.ok) throw oauthError(res.status, body);
1086
+ return body;
1087
+ }
1088
+ /** Normalise a token grant into a record for your httpOnly session store. */
1089
+ seal(tokens, opts) {
1090
+ return seal(tokens, opts);
1091
+ }
1092
+ /** Whether a sealed session's access token needs a refresh before use. */
1093
+ accessExpired(record, opts) {
1094
+ return accessExpired(record, opts);
1095
+ }
1096
+ /** Verify a `child.login.approved` webhook signature (uses your `webhookSecret`). */
1097
+ verifyWebhook(payload, signature, secret = this.webhookSecret) {
1098
+ if (!secret) throw new Error("ChildLogin.verifyWebhook: no webhookSecret configured");
1099
+ return verifyWebhook(payload, signature, secret);
1100
+ }
1101
+ /** Verify + parse a `child.login.approved` webhook. Returns null if the signature is invalid. */
1102
+ parseApproval(payload, signature, secret = this.webhookSecret) {
1103
+ if (!secret) throw new Error("ChildLogin.parseApproval: no webhookSecret configured");
1104
+ return parseChildLoginApproved(payload, signature, secret);
1105
+ }
1106
+ async post(path, body) {
1107
+ const clean = {};
1108
+ for (const [k, v] of Object.entries(body)) if (v !== void 0) clean[k] = v;
1109
+ const res = await fetch(`${this.baseUrl}${path}`, {
1110
+ method: "POST",
1111
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
1112
+ body: JSON.stringify(clean)
1113
+ });
1114
+ const text = await res.text();
1115
+ let json = {};
1116
+ try {
1117
+ json = text ? JSON.parse(text) : {};
1118
+ } catch {
1119
+ json = { error: "invalid_response", raw: text };
1120
+ }
1121
+ return { status: res.status, body: json };
1122
+ }
1123
+ };
1124
+ function oauthError(status, body) {
1125
+ const code = body?.error || `http_${status}`;
1126
+ const err = new Error(body?.error_description || code);
1127
+ err.status = status;
1128
+ err.code = code;
1129
+ return err;
1130
+ }
1131
+ function sleep(ms, signal) {
1132
+ return new Promise((resolve, reject) => {
1133
+ if (signal?.aborted) return reject(new DOMException("Aborted", "AbortError"));
1134
+ const t = setTimeout(resolve, ms);
1135
+ signal?.addEventListener("abort", () => {
1136
+ clearTimeout(t);
1137
+ reject(new DOMException("Aborted", "AbortError"));
1138
+ }, { once: true });
1139
+ });
1140
+ }
1141
+
510
1142
  // src/index.ts
511
1143
  var OmenClient = class {
512
1144
  /** User profiles and friend lists. */
@@ -519,6 +1151,18 @@ var OmenClient = class {
519
1151
  collections;
520
1152
  /** Webhook endpoint management and signature verification. */
521
1153
  webhooks;
1154
+ /** In-app products (one-time purchases and subscriptions). */
1155
+ products;
1156
+ /** Managed multiplayer configuration and room logic deployment. */
1157
+ multiplayer;
1158
+ /** Send notifications to app users. */
1159
+ notifications;
1160
+ /** Award Sparks from your prepaid pool + mint UI-kit display tokens. */
1161
+ sparks;
1162
+ /** Send transactional email from your verified domains (Partner Apps). */
1163
+ emails;
1164
+ /** Shared Omen avatar in your app: editor tokens + render URLs (F262). */
1165
+ avatar;
522
1166
  constructor(options) {
523
1167
  if (!options.token) throw new Error("OmenClient: token is required");
524
1168
  if (!options.appId) throw new Error("OmenClient: appId is required");
@@ -529,14 +1173,27 @@ var OmenClient = class {
529
1173
  this.items = new ItemsNamespace(http, options.appId);
530
1174
  this.collections = new CollectionsNamespace(http, options.appId);
531
1175
  this.webhooks = new WebhooksNamespace(http, options.appId);
1176
+ this.products = new ProductsNamespace(http, options.appId);
1177
+ this.multiplayer = new MultiplayerNamespace(http, options.appId);
1178
+ this.notifications = new NotificationsNamespace(http, options.appId);
1179
+ this.sparks = new SparksNamespace(http, options.appId);
1180
+ this.emails = new EmailsNamespace(http, options.appId);
1181
+ this.avatar = new AvatarNamespace(options.appId, baseUrl);
532
1182
  }
533
1183
  };
534
1184
  // Annotate the CommonJS export names for ESM import in node:
535
1185
  0 && (module.exports = {
1186
+ ASK_GROWN_UP_STATES,
1187
+ CHILD_LOGIN_STATES,
1188
+ ChildLogin,
1189
+ ENTRY_STATES,
536
1190
  OmenAuthError,
537
1191
  OmenClient,
538
1192
  OmenError,
539
1193
  OmenNotFoundError,
540
1194
  OmenRateLimitError,
541
- OmenValidationError
1195
+ OmenValidationError,
1196
+ deriveInitialState,
1197
+ displayGroup,
1198
+ reduce
542
1199
  });