@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/README.md +108 -0
- package/dist/child-login.d.mts +230 -0
- package/dist/child-login.d.ts +230 -0
- package/dist/child-login.js +335 -0
- package/dist/child-login.mjs +18 -0
- package/dist/chunk-7L3ANE2V.mjs +303 -0
- package/dist/creation.d.ts +3 -3
- package/dist/index.d.mts +506 -1
- package/dist/index.d.ts +506 -1
- package/dist/index.js +659 -2
- package/dist/index.mjs +367 -1
- package/package.json +7 -2
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
|
});
|