@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/README.md
CHANGED
|
@@ -110,6 +110,78 @@ console.log(webhook.secret); // save this!
|
|
|
110
110
|
const valid = await omen.webhooks.verify(rawBody, signature, secret);
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
### Sparks (partner awards)
|
|
114
|
+
|
|
115
|
+
Award money-backed Sparks and catalog items to your users from a prepaid pool
|
|
116
|
+
(buy/manage it in the Developer Portal → Sparks Pool).
|
|
117
|
+
|
|
118
|
+
> **Approved Partner Apps only.** Awarding mints real Sparks, so `award`,
|
|
119
|
+
> `awardBatch`, and `items.awardCatalog` require the app to be an approved
|
|
120
|
+
> Partner App (Developer Portal → Partner Program). Unapproved apps get
|
|
121
|
+
> `403 partner_required`. Funding a pool does not require approval.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// Award Sparks (pass idempotencyKey to make retries safe)
|
|
125
|
+
await omen.sparks.award({
|
|
126
|
+
userId, amount: 250, reason: 'Beat level 10',
|
|
127
|
+
idempotencyKey: `level10:${userId}`,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Batch (≤100, best-effort per item)
|
|
131
|
+
await omen.sparks.awardBatch([
|
|
132
|
+
{ userId: 'u1', amount: 100 },
|
|
133
|
+
{ userId: 'u2', amount: 100 },
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// Award an open-shop item or avatar trait — paid from your pool at list price
|
|
137
|
+
await omen.items.awardCatalog({ userId, catalogType: 'store', catalogItemId: 'theme_lava' });
|
|
138
|
+
|
|
139
|
+
// Pool status
|
|
140
|
+
const pool = await omen.sparks.pool();
|
|
141
|
+
|
|
142
|
+
// Mint a short-lived display token for the UI kit (backend only — needs your
|
|
143
|
+
// app's OAuth client secret). Pass it to <omen-sparks-balance>/<omen-inventory>.
|
|
144
|
+
const token = omen.sparks.displayToken({ userId, secret: process.env.OMEN_CLIENT_SECRET! });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### UI kit (embeddable widgets)
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<script src="https://sdk.omen.dog/v1/ui-kit.js"></script>
|
|
151
|
+
<omen-sparks-balance token="DISPLAY_TOKEN"></omen-sparks-balance>
|
|
152
|
+
<omen-inventory token="DISPLAY_TOKEN" limit="24"></omen-inventory>
|
|
153
|
+
<script>OmenUI.toast({ amount: 250, message: 'Level cleared!' })</script>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Restyle via CSS variables (`--omen-bg`, `--omen-accent`, `--omen-spark`, …),
|
|
157
|
+
`::part()` selectors, or `theme="bare"` to ship your own CSS entirely.
|
|
158
|
+
|
|
159
|
+
### Shared avatar (Embedded Avatar Kit)
|
|
160
|
+
|
|
161
|
+
Put the shared Omen avatar inside your app — display it, let users edit it
|
|
162
|
+
in-place, browse the trait catalog, and gift traits from your Sparks pool.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// Public render URL (pass version from avatar:saved / user.avatar_updated to bust caches)
|
|
166
|
+
const url = omen.avatar.renderUrl(userId, { version });
|
|
167
|
+
|
|
168
|
+
// Mint a short-lived editor token for <omen-avatar-editor> (backend only)
|
|
169
|
+
const token = omen.avatar.editorToken({ userId, secret: process.env.OMEN_CLIENT_SECRET! });
|
|
170
|
+
|
|
171
|
+
// Enumerate the shared catalog — trait ids, names, rarities, ✦ prices (public, no auth).
|
|
172
|
+
// Pass { editorToken } and each trait's `locked` reflects that user's ownership.
|
|
173
|
+
const { catalog, rarityPricing } = await omen.avatar.catalog();
|
|
174
|
+
|
|
175
|
+
// Gift a trait app-funded — your pool pays list price, the user pays nothing
|
|
176
|
+
await omen.items.awardCatalog({
|
|
177
|
+
userId, catalogType: 'avatar_trait', catalogItemId: 'hair/export-01.svg',
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Trait ids are `category/file.svg` paths and double as public preview SVGs at
|
|
182
|
+
`https://omen.dog/avatar/{traitId}`. Common traits are free for everyone and
|
|
183
|
+
can't be awarded.
|
|
184
|
+
|
|
113
185
|
## Creation Runtime Types
|
|
114
186
|
|
|
115
187
|
For creation developers — get autocomplete for the `omen.*` global:
|
|
@@ -121,6 +193,42 @@ const save = await omen.load();
|
|
|
121
193
|
omen.score(100);
|
|
122
194
|
```
|
|
123
195
|
|
|
196
|
+
## Child Re-Login (`ChildLogin`)
|
|
197
|
+
|
|
198
|
+
A safe, thin wrapper over the child device-grant flow for Tier-3 apps that sign
|
|
199
|
+
in Omen child accounts (F263). It's a separate export — it uses your OAuth
|
|
200
|
+
`clientId`/`clientSecret`, not a `dev_` token — and is also available from the
|
|
201
|
+
server-only subpath `@omen.dog/sdk/child-login`. `scope:'child'` is always sent,
|
|
202
|
+
RFC 8628 errors map to a calm status union instead of throwing, and scope is
|
|
203
|
+
preserved across refresh rotation. Tokens never leave your server.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { ChildLogin } from '@omen.dog/sdk/child-login';
|
|
207
|
+
|
|
208
|
+
const child = new ChildLogin({
|
|
209
|
+
clientId: process.env.OMEN_CLIENT_ID!,
|
|
210
|
+
clientSecret: process.env.OMEN_CLIENT_SECRET!, // omit for public/PKCE clients
|
|
211
|
+
webhookSecret: process.env.OMEN_WEBHOOK_SECRET!,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const grant = await child.deviceStart({ deviceName: "Astrid's iPad" });
|
|
215
|
+
await child.notify({ device_code: grant.device_code, username: 'astrid' }); // cold start
|
|
216
|
+
const res = await child.pollUntil(grant.device_code); // honors interval + slow_down
|
|
217
|
+
if (res.status === 'approved') {
|
|
218
|
+
const session = child.seal(res.tokens!); // store in your httpOnly session; res.rebind = card data
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// flip the child's screen the instant a parent approves:
|
|
222
|
+
if (child.verifyWebhook(rawBody, req.headers['x-omen-signature'])) { /* notify the device */ }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Pair it with the in-context `<omen-child-login>` web component (zero-dep, themeable,
|
|
226
|
+
talks only to your own routes). See [omen.dog/docs#child-relogin-kit](https://omen.dog/docs).
|
|
227
|
+
|
|
228
|
+
The canonical child-side state machine is exported too (`deriveInitialState`,
|
|
229
|
+
`reduce`, `displayGroup`, `CHILD_LOGIN_STATES`) for apps building custom UI on the
|
|
230
|
+
same kindness.
|
|
231
|
+
|
|
124
232
|
## Error Handling
|
|
125
233
|
|
|
126
234
|
```ts
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
type ChildPollStatus =
|
|
2
|
+
| 'approved'
|
|
3
|
+
| 'pending'
|
|
4
|
+
| 'slow_down'
|
|
5
|
+
| 'denied'
|
|
6
|
+
| 'expired'
|
|
7
|
+
| 'error';
|
|
8
|
+
|
|
9
|
+
interface ChildTokens {
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
token_type: string;
|
|
13
|
+
expires_in?: number;
|
|
14
|
+
scope?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ChildRebind {
|
|
18
|
+
child_id: string;
|
|
19
|
+
display_name: string;
|
|
20
|
+
avatar_url: string | null;
|
|
21
|
+
suggested_device_name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PollResult {
|
|
25
|
+
status: ChildPollStatus;
|
|
26
|
+
tokens?: ChildTokens;
|
|
27
|
+
rebind?: ChildRebind | null;
|
|
28
|
+
deviceToken?: string | null;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SealedSession {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
refreshToken: string | null;
|
|
35
|
+
scope: string;
|
|
36
|
+
accessExpiresAt: number;
|
|
37
|
+
deviceToken?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ChildLoginApprovedPayload {
|
|
41
|
+
child_id: string;
|
|
42
|
+
device_id: string | null;
|
|
43
|
+
device_name: string | null;
|
|
44
|
+
approved_at: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ChildLoginState =
|
|
48
|
+
| 'trusted'
|
|
49
|
+
| 'known'
|
|
50
|
+
| 'cold'
|
|
51
|
+
| 'pending'
|
|
52
|
+
| 'approved'
|
|
53
|
+
| 'denied'
|
|
54
|
+
| 'blocked'
|
|
55
|
+
| 'paused'
|
|
56
|
+
| 'expired'
|
|
57
|
+
| 'offline';
|
|
58
|
+
|
|
59
|
+
type ChildLoginSignal =
|
|
60
|
+
| 'ask'
|
|
61
|
+
| 'pending'
|
|
62
|
+
| 'slow_down'
|
|
63
|
+
| 'approved'
|
|
64
|
+
| 'denied'
|
|
65
|
+
| 'blocked'
|
|
66
|
+
| 'paused'
|
|
67
|
+
| 'expired'
|
|
68
|
+
| 'offline'
|
|
69
|
+
| 'online'
|
|
70
|
+
| 'reset';
|
|
71
|
+
|
|
72
|
+
type ChildLoginDisplayGroup =
|
|
73
|
+
| 'ready'
|
|
74
|
+
| 'waiting'
|
|
75
|
+
| 'approved'
|
|
76
|
+
| 'ask-grown-up'
|
|
77
|
+
| 'expired'
|
|
78
|
+
| 'offline';
|
|
79
|
+
|
|
80
|
+
interface ChildLoginContext {
|
|
81
|
+
online?: boolean;
|
|
82
|
+
trustedDevice?: boolean;
|
|
83
|
+
identity?: unknown | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declare const CHILD_LOGIN_STATES: ChildLoginState[];
|
|
87
|
+
declare const ASK_GROWN_UP_STATES: ChildLoginState[];
|
|
88
|
+
declare const ENTRY_STATES: ChildLoginState[];
|
|
89
|
+
|
|
90
|
+
declare function deriveInitialState(ctx?: ChildLoginContext): ChildLoginState;
|
|
91
|
+
declare function reduce(state: ChildLoginState, signal: ChildLoginSignal, ctx?: ChildLoginContext): ChildLoginState;
|
|
92
|
+
declare function displayGroup(state: ChildLoginState): ChildLoginDisplayGroup;
|
|
93
|
+
|
|
94
|
+
interface ChildLoginOptions {
|
|
95
|
+
/** Your OAuth client_id (from the Developer Portal). Required. */
|
|
96
|
+
clientId: string;
|
|
97
|
+
/** Your client_secret. Omit for public (PKCE) clients. */
|
|
98
|
+
clientSecret?: string;
|
|
99
|
+
/** Base URL for the Omen API. Defaults to `https://omen.dog`. */
|
|
100
|
+
baseUrl?: string;
|
|
101
|
+
/** The endpoint secret for your `child.login.approved` webhook (enables `verifyWebhook`). */
|
|
102
|
+
webhookSecret?: string;
|
|
103
|
+
}
|
|
104
|
+
interface DeviceStartOptions {
|
|
105
|
+
/** The child's Omen userId from a previous session. Omit for a cold start (parent picks). */
|
|
106
|
+
loginHint?: string;
|
|
107
|
+
/** Stable per-device id you generate and persist (enables trusted-device auto-approval). */
|
|
108
|
+
deviceId?: string;
|
|
109
|
+
/** Human-readable device name shown to the parent ("Astrid's iPad"). */
|
|
110
|
+
deviceName?: string;
|
|
111
|
+
/** A trusted-device token from a prior approval — auto-approves on first poll. */
|
|
112
|
+
deviceToken?: string;
|
|
113
|
+
}
|
|
114
|
+
interface DeviceStartResult {
|
|
115
|
+
device_code: string;
|
|
116
|
+
user_code: string;
|
|
117
|
+
verification_uri: string;
|
|
118
|
+
verification_uri_complete: string;
|
|
119
|
+
expires_in: number;
|
|
120
|
+
interval: number;
|
|
121
|
+
}
|
|
122
|
+
interface ChildIdentity {
|
|
123
|
+
child: {
|
|
124
|
+
display_name: string;
|
|
125
|
+
avatar_url: string | null;
|
|
126
|
+
};
|
|
127
|
+
guardians: Array<{
|
|
128
|
+
display_name: string;
|
|
129
|
+
avatar_url: string | null;
|
|
130
|
+
}>;
|
|
131
|
+
}
|
|
132
|
+
interface PollUntilOptions {
|
|
133
|
+
/** Abort the wait (e.g. the child navigated away, or your webhook already flipped the screen). */
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
/** Called after every non-terminal poll with the current status — useful for logging. */
|
|
136
|
+
onStatus?: (status: PollResult['status']) => void;
|
|
137
|
+
/** Override the starting interval (seconds). Defaults to the grant's `interval`, or 5. */
|
|
138
|
+
intervalSeconds?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* F263 Child Re-Login — the official server-side primitive for third-party apps
|
|
143
|
+
* that sign in child accounts.
|
|
144
|
+
*
|
|
145
|
+
* It is a thin, safe wrapper over the Phase-1 device-grant API with the right
|
|
146
|
+
* defaults made non-optional: `scope:'child'` is always sent, RFC 8628 errors
|
|
147
|
+
* are mapped to a calm status union instead of thrown, and `scope` is preserved
|
|
148
|
+
* across refresh. Token custody stays on YOUR server — nothing here ever returns
|
|
149
|
+
* a token to the browser.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { ChildLogin } from '@omen.dog/sdk';
|
|
154
|
+
*
|
|
155
|
+
* const child = new ChildLogin({
|
|
156
|
+
* clientId: process.env.OMEN_CLIENT_ID!,
|
|
157
|
+
* clientSecret: process.env.OMEN_CLIENT_SECRET!,
|
|
158
|
+
* webhookSecret: process.env.OMEN_WEBHOOK_SECRET!,
|
|
159
|
+
* });
|
|
160
|
+
*
|
|
161
|
+
* // 1. start a grant on a fresh device
|
|
162
|
+
* const grant = await child.deviceStart({ deviceName: "Astrid's iPad" });
|
|
163
|
+
* // 2. she types her username → ask the family
|
|
164
|
+
* await child.notify({ device_code: grant.device_code, username: 'astrid' });
|
|
165
|
+
* // 3. poll until a grown-up approves (calm status union, never throws on pending)
|
|
166
|
+
* const result = await child.pollUntil(grant.device_code);
|
|
167
|
+
* if (result.status === 'approved') {
|
|
168
|
+
* const session = child.seal(result.tokens!); // store in your httpOnly session
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare class ChildLogin {
|
|
173
|
+
private readonly clientId;
|
|
174
|
+
private readonly clientSecret?;
|
|
175
|
+
private readonly webhookSecret?;
|
|
176
|
+
readonly baseUrl: string;
|
|
177
|
+
constructor(options: ChildLoginOptions);
|
|
178
|
+
/** Start a child device-authorization grant. `scope:'child'` is always sent. */
|
|
179
|
+
deviceStart(options?: DeviceStartOptions): Promise<DeviceStartResult>;
|
|
180
|
+
/**
|
|
181
|
+
* Tell Omen which child is logging in. With `username` this is the cold-start
|
|
182
|
+
* path (no prior `login_hint`); without it, it re-pings the family for a known
|
|
183
|
+
* child. Always resolves `{ ok: true }` (enumeration-safe) on the cold path.
|
|
184
|
+
*/
|
|
185
|
+
notify(args: {
|
|
186
|
+
device_code: string;
|
|
187
|
+
username?: string;
|
|
188
|
+
}): Promise<{
|
|
189
|
+
ok: boolean;
|
|
190
|
+
}>;
|
|
191
|
+
/**
|
|
192
|
+
* Poll the token endpoint once. Returns a calm status union — `pending`,
|
|
193
|
+
* `slow_down`, `denied`, `expired`, `error`, or `approved` (with tokens,
|
|
194
|
+
* rebind blob and one-time device_token). Never throws on a normal RFC 8628
|
|
195
|
+
* polling response; only network failures reject.
|
|
196
|
+
*/
|
|
197
|
+
poll(deviceCode: string): Promise<PollResult>;
|
|
198
|
+
/**
|
|
199
|
+
* Poll until the grant reaches a terminal state, honouring `interval` and
|
|
200
|
+
* widening it by 5s on every `slow_down` (RFC 8628 §3.5). Prefer driving the
|
|
201
|
+
* flip from the `child.login.approved` webhook; use this as the fallback.
|
|
202
|
+
*/
|
|
203
|
+
pollUntil(deviceCode: string, options?: PollUntilOptions): Promise<PollResult>;
|
|
204
|
+
/**
|
|
205
|
+
* Refresh a child access token. `scope:'child'` is preserved across rotation,
|
|
206
|
+
* and the old refresh token is single-use (Omen rotates it).
|
|
207
|
+
*/
|
|
208
|
+
refresh(refreshToken: string): Promise<ChildTokens>;
|
|
209
|
+
/** Fetch the consent-gated identity bundle with the child's access token. */
|
|
210
|
+
childIdentity(accessToken: string): Promise<ChildIdentity>;
|
|
211
|
+
/** Normalise a token grant into a record for your httpOnly session store. */
|
|
212
|
+
seal(tokens: ChildTokens & {
|
|
213
|
+
device_token?: string;
|
|
214
|
+
}, opts?: {
|
|
215
|
+
now?: number;
|
|
216
|
+
deviceToken?: string | null;
|
|
217
|
+
}): SealedSession;
|
|
218
|
+
/** Whether a sealed session's access token needs a refresh before use. */
|
|
219
|
+
accessExpired(record: SealedSession, opts?: {
|
|
220
|
+
now?: number;
|
|
221
|
+
skewMs?: number;
|
|
222
|
+
}): boolean;
|
|
223
|
+
/** Verify a `child.login.approved` webhook signature (uses your `webhookSecret`). */
|
|
224
|
+
verifyWebhook(payload: string, signature: string, secret?: string | undefined): boolean;
|
|
225
|
+
/** Verify + parse a `child.login.approved` webhook. Returns null if the signature is invalid. */
|
|
226
|
+
parseApproval(payload: string, signature: string, secret?: string | undefined): ChildLoginApprovedPayload | null;
|
|
227
|
+
private post;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export { ASK_GROWN_UP_STATES, CHILD_LOGIN_STATES, type ChildIdentity, ChildLogin, type ChildLoginApprovedPayload, type ChildLoginContext, type ChildLoginDisplayGroup, type ChildLoginOptions, type ChildLoginSignal, type ChildLoginState, type ChildRebind, type ChildTokens, type DeviceStartOptions, type DeviceStartResult, ENTRY_STATES, type PollResult, type PollUntilOptions, type SealedSession, deriveInitialState, displayGroup, reduce };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
type ChildPollStatus =
|
|
2
|
+
| 'approved'
|
|
3
|
+
| 'pending'
|
|
4
|
+
| 'slow_down'
|
|
5
|
+
| 'denied'
|
|
6
|
+
| 'expired'
|
|
7
|
+
| 'error';
|
|
8
|
+
|
|
9
|
+
interface ChildTokens {
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
token_type: string;
|
|
13
|
+
expires_in?: number;
|
|
14
|
+
scope?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ChildRebind {
|
|
18
|
+
child_id: string;
|
|
19
|
+
display_name: string;
|
|
20
|
+
avatar_url: string | null;
|
|
21
|
+
suggested_device_name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PollResult {
|
|
25
|
+
status: ChildPollStatus;
|
|
26
|
+
tokens?: ChildTokens;
|
|
27
|
+
rebind?: ChildRebind | null;
|
|
28
|
+
deviceToken?: string | null;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SealedSession {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
refreshToken: string | null;
|
|
35
|
+
scope: string;
|
|
36
|
+
accessExpiresAt: number;
|
|
37
|
+
deviceToken?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ChildLoginApprovedPayload {
|
|
41
|
+
child_id: string;
|
|
42
|
+
device_id: string | null;
|
|
43
|
+
device_name: string | null;
|
|
44
|
+
approved_at: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ChildLoginState =
|
|
48
|
+
| 'trusted'
|
|
49
|
+
| 'known'
|
|
50
|
+
| 'cold'
|
|
51
|
+
| 'pending'
|
|
52
|
+
| 'approved'
|
|
53
|
+
| 'denied'
|
|
54
|
+
| 'blocked'
|
|
55
|
+
| 'paused'
|
|
56
|
+
| 'expired'
|
|
57
|
+
| 'offline';
|
|
58
|
+
|
|
59
|
+
type ChildLoginSignal =
|
|
60
|
+
| 'ask'
|
|
61
|
+
| 'pending'
|
|
62
|
+
| 'slow_down'
|
|
63
|
+
| 'approved'
|
|
64
|
+
| 'denied'
|
|
65
|
+
| 'blocked'
|
|
66
|
+
| 'paused'
|
|
67
|
+
| 'expired'
|
|
68
|
+
| 'offline'
|
|
69
|
+
| 'online'
|
|
70
|
+
| 'reset';
|
|
71
|
+
|
|
72
|
+
type ChildLoginDisplayGroup =
|
|
73
|
+
| 'ready'
|
|
74
|
+
| 'waiting'
|
|
75
|
+
| 'approved'
|
|
76
|
+
| 'ask-grown-up'
|
|
77
|
+
| 'expired'
|
|
78
|
+
| 'offline';
|
|
79
|
+
|
|
80
|
+
interface ChildLoginContext {
|
|
81
|
+
online?: boolean;
|
|
82
|
+
trustedDevice?: boolean;
|
|
83
|
+
identity?: unknown | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declare const CHILD_LOGIN_STATES: ChildLoginState[];
|
|
87
|
+
declare const ASK_GROWN_UP_STATES: ChildLoginState[];
|
|
88
|
+
declare const ENTRY_STATES: ChildLoginState[];
|
|
89
|
+
|
|
90
|
+
declare function deriveInitialState(ctx?: ChildLoginContext): ChildLoginState;
|
|
91
|
+
declare function reduce(state: ChildLoginState, signal: ChildLoginSignal, ctx?: ChildLoginContext): ChildLoginState;
|
|
92
|
+
declare function displayGroup(state: ChildLoginState): ChildLoginDisplayGroup;
|
|
93
|
+
|
|
94
|
+
interface ChildLoginOptions {
|
|
95
|
+
/** Your OAuth client_id (from the Developer Portal). Required. */
|
|
96
|
+
clientId: string;
|
|
97
|
+
/** Your client_secret. Omit for public (PKCE) clients. */
|
|
98
|
+
clientSecret?: string;
|
|
99
|
+
/** Base URL for the Omen API. Defaults to `https://omen.dog`. */
|
|
100
|
+
baseUrl?: string;
|
|
101
|
+
/** The endpoint secret for your `child.login.approved` webhook (enables `verifyWebhook`). */
|
|
102
|
+
webhookSecret?: string;
|
|
103
|
+
}
|
|
104
|
+
interface DeviceStartOptions {
|
|
105
|
+
/** The child's Omen userId from a previous session. Omit for a cold start (parent picks). */
|
|
106
|
+
loginHint?: string;
|
|
107
|
+
/** Stable per-device id you generate and persist (enables trusted-device auto-approval). */
|
|
108
|
+
deviceId?: string;
|
|
109
|
+
/** Human-readable device name shown to the parent ("Astrid's iPad"). */
|
|
110
|
+
deviceName?: string;
|
|
111
|
+
/** A trusted-device token from a prior approval — auto-approves on first poll. */
|
|
112
|
+
deviceToken?: string;
|
|
113
|
+
}
|
|
114
|
+
interface DeviceStartResult {
|
|
115
|
+
device_code: string;
|
|
116
|
+
user_code: string;
|
|
117
|
+
verification_uri: string;
|
|
118
|
+
verification_uri_complete: string;
|
|
119
|
+
expires_in: number;
|
|
120
|
+
interval: number;
|
|
121
|
+
}
|
|
122
|
+
interface ChildIdentity {
|
|
123
|
+
child: {
|
|
124
|
+
display_name: string;
|
|
125
|
+
avatar_url: string | null;
|
|
126
|
+
};
|
|
127
|
+
guardians: Array<{
|
|
128
|
+
display_name: string;
|
|
129
|
+
avatar_url: string | null;
|
|
130
|
+
}>;
|
|
131
|
+
}
|
|
132
|
+
interface PollUntilOptions {
|
|
133
|
+
/** Abort the wait (e.g. the child navigated away, or your webhook already flipped the screen). */
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
/** Called after every non-terminal poll with the current status — useful for logging. */
|
|
136
|
+
onStatus?: (status: PollResult['status']) => void;
|
|
137
|
+
/** Override the starting interval (seconds). Defaults to the grant's `interval`, or 5. */
|
|
138
|
+
intervalSeconds?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* F263 Child Re-Login — the official server-side primitive for third-party apps
|
|
143
|
+
* that sign in child accounts.
|
|
144
|
+
*
|
|
145
|
+
* It is a thin, safe wrapper over the Phase-1 device-grant API with the right
|
|
146
|
+
* defaults made non-optional: `scope:'child'` is always sent, RFC 8628 errors
|
|
147
|
+
* are mapped to a calm status union instead of thrown, and `scope` is preserved
|
|
148
|
+
* across refresh. Token custody stays on YOUR server — nothing here ever returns
|
|
149
|
+
* a token to the browser.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { ChildLogin } from '@omen.dog/sdk';
|
|
154
|
+
*
|
|
155
|
+
* const child = new ChildLogin({
|
|
156
|
+
* clientId: process.env.OMEN_CLIENT_ID!,
|
|
157
|
+
* clientSecret: process.env.OMEN_CLIENT_SECRET!,
|
|
158
|
+
* webhookSecret: process.env.OMEN_WEBHOOK_SECRET!,
|
|
159
|
+
* });
|
|
160
|
+
*
|
|
161
|
+
* // 1. start a grant on a fresh device
|
|
162
|
+
* const grant = await child.deviceStart({ deviceName: "Astrid's iPad" });
|
|
163
|
+
* // 2. she types her username → ask the family
|
|
164
|
+
* await child.notify({ device_code: grant.device_code, username: 'astrid' });
|
|
165
|
+
* // 3. poll until a grown-up approves (calm status union, never throws on pending)
|
|
166
|
+
* const result = await child.pollUntil(grant.device_code);
|
|
167
|
+
* if (result.status === 'approved') {
|
|
168
|
+
* const session = child.seal(result.tokens!); // store in your httpOnly session
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare class ChildLogin {
|
|
173
|
+
private readonly clientId;
|
|
174
|
+
private readonly clientSecret?;
|
|
175
|
+
private readonly webhookSecret?;
|
|
176
|
+
readonly baseUrl: string;
|
|
177
|
+
constructor(options: ChildLoginOptions);
|
|
178
|
+
/** Start a child device-authorization grant. `scope:'child'` is always sent. */
|
|
179
|
+
deviceStart(options?: DeviceStartOptions): Promise<DeviceStartResult>;
|
|
180
|
+
/**
|
|
181
|
+
* Tell Omen which child is logging in. With `username` this is the cold-start
|
|
182
|
+
* path (no prior `login_hint`); without it, it re-pings the family for a known
|
|
183
|
+
* child. Always resolves `{ ok: true }` (enumeration-safe) on the cold path.
|
|
184
|
+
*/
|
|
185
|
+
notify(args: {
|
|
186
|
+
device_code: string;
|
|
187
|
+
username?: string;
|
|
188
|
+
}): Promise<{
|
|
189
|
+
ok: boolean;
|
|
190
|
+
}>;
|
|
191
|
+
/**
|
|
192
|
+
* Poll the token endpoint once. Returns a calm status union — `pending`,
|
|
193
|
+
* `slow_down`, `denied`, `expired`, `error`, or `approved` (with tokens,
|
|
194
|
+
* rebind blob and one-time device_token). Never throws on a normal RFC 8628
|
|
195
|
+
* polling response; only network failures reject.
|
|
196
|
+
*/
|
|
197
|
+
poll(deviceCode: string): Promise<PollResult>;
|
|
198
|
+
/**
|
|
199
|
+
* Poll until the grant reaches a terminal state, honouring `interval` and
|
|
200
|
+
* widening it by 5s on every `slow_down` (RFC 8628 §3.5). Prefer driving the
|
|
201
|
+
* flip from the `child.login.approved` webhook; use this as the fallback.
|
|
202
|
+
*/
|
|
203
|
+
pollUntil(deviceCode: string, options?: PollUntilOptions): Promise<PollResult>;
|
|
204
|
+
/**
|
|
205
|
+
* Refresh a child access token. `scope:'child'` is preserved across rotation,
|
|
206
|
+
* and the old refresh token is single-use (Omen rotates it).
|
|
207
|
+
*/
|
|
208
|
+
refresh(refreshToken: string): Promise<ChildTokens>;
|
|
209
|
+
/** Fetch the consent-gated identity bundle with the child's access token. */
|
|
210
|
+
childIdentity(accessToken: string): Promise<ChildIdentity>;
|
|
211
|
+
/** Normalise a token grant into a record for your httpOnly session store. */
|
|
212
|
+
seal(tokens: ChildTokens & {
|
|
213
|
+
device_token?: string;
|
|
214
|
+
}, opts?: {
|
|
215
|
+
now?: number;
|
|
216
|
+
deviceToken?: string | null;
|
|
217
|
+
}): SealedSession;
|
|
218
|
+
/** Whether a sealed session's access token needs a refresh before use. */
|
|
219
|
+
accessExpired(record: SealedSession, opts?: {
|
|
220
|
+
now?: number;
|
|
221
|
+
skewMs?: number;
|
|
222
|
+
}): boolean;
|
|
223
|
+
/** Verify a `child.login.approved` webhook signature (uses your `webhookSecret`). */
|
|
224
|
+
verifyWebhook(payload: string, signature: string, secret?: string | undefined): boolean;
|
|
225
|
+
/** Verify + parse a `child.login.approved` webhook. Returns null if the signature is invalid. */
|
|
226
|
+
parseApproval(payload: string, signature: string, secret?: string | undefined): ChildLoginApprovedPayload | null;
|
|
227
|
+
private post;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export { ASK_GROWN_UP_STATES, CHILD_LOGIN_STATES, type ChildIdentity, ChildLogin, type ChildLoginApprovedPayload, type ChildLoginContext, type ChildLoginDisplayGroup, type ChildLoginOptions, type ChildLoginSignal, type ChildLoginState, type ChildRebind, type ChildTokens, type DeviceStartOptions, type DeviceStartResult, ENTRY_STATES, type PollResult, type PollUntilOptions, type SealedSession, deriveInitialState, displayGroup, reduce };
|