@flow-industries/id 0.6.0 → 0.7.1
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/sdk/client/create-flow.js +59 -20
- package/dist/sdk/client/methods.d.ts +0 -1
- package/dist/sdk/client/methods.js +0 -1
- package/dist/sdk/client/refresh-store.d.ts +13 -0
- package/dist/sdk/client/refresh-store.js +25 -0
- package/dist/sdk/client/session.js +2 -1
- package/dist/sdk/types/auth.d.ts +1 -0
- package/dist/sdk/types/events.d.ts +2 -2
- package/dist/sdk/types/index.d.ts +2 -1
- package/dist/sdk/types/protocol.d.ts +7 -6
- package/dist/sdk/types/sdk.d.ts +1 -0
- package/dist/sdk/types/xp.d.ts +72 -0
- package/dist/sdk/types/xp.js +1 -0
- package/package.json +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createDialogHost } from "./dialog-host";
|
|
2
2
|
import { idb } from "./idb";
|
|
3
3
|
import { METHODS } from "./methods";
|
|
4
|
+
import { makeRefreshStore } from "./refresh-store";
|
|
4
5
|
import { credentialToAddress, restoreCredential, runLogin, runLogout, } from "./session";
|
|
5
6
|
import { createStore, initialFlowState } from "./store";
|
|
6
7
|
const DEFAULT_HOST = "https://id.flow.industries";
|
|
@@ -91,6 +92,7 @@ export function createFlow(options = {}) {
|
|
|
91
92
|
return currentFlow;
|
|
92
93
|
const host = (options.host ?? DEFAULT_HOST).replace(/\/+$/, "");
|
|
93
94
|
const dialogUrl = `${host}/dialog/`;
|
|
95
|
+
const refreshStore = makeRefreshStore(host);
|
|
94
96
|
const chains = options.chains ?? [];
|
|
95
97
|
const getChain = (chainId) => {
|
|
96
98
|
if (chainId == null)
|
|
@@ -120,19 +122,39 @@ export function createFlow(options = {}) {
|
|
|
120
122
|
* user has no active session (in which case the caller should fall back to
|
|
121
123
|
* `flow.login()`). Never throws — restore failures are treated as "no session".
|
|
122
124
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Mints a fresh session from the first-party refresh token: POSTs it as a
|
|
127
|
+
* bearer to /api/session/refresh (no cookie, so iOS ITP can't block it),
|
|
128
|
+
* commits the returned session, and persists the rotated token. Returns false
|
|
129
|
+
* (never throws) when there's no token or the server rejects it — the caller
|
|
130
|
+
* then falls back to ensureGuest()/login(). A 401 means the token is dead
|
|
131
|
+
* (expired, revoked, or reuse-detected), so it's dropped.
|
|
132
|
+
*/
|
|
133
|
+
async function refreshViaToken() {
|
|
134
|
+
const token = refreshStore.get();
|
|
135
|
+
if (!token)
|
|
136
|
+
return false;
|
|
125
137
|
try {
|
|
126
|
-
const
|
|
138
|
+
const res = await fetch(`${host}/api/session/refresh`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
if (res.status === 401)
|
|
144
|
+
refreshStore.clear();
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const result = (await res.json());
|
|
148
|
+
refreshStore.set(result.refreshToken);
|
|
127
149
|
store.setState({
|
|
128
150
|
user: result.user,
|
|
129
151
|
jwt: result.jwt,
|
|
130
152
|
credential: result.credential,
|
|
131
153
|
address: result.address,
|
|
132
154
|
});
|
|
133
|
-
// A guest
|
|
134
|
-
//
|
|
135
|
-
//
|
|
155
|
+
// A guest carries a null credential; never persist that — the IDB store is
|
|
156
|
+
// reserved for a real passkey credential and writing null would erase a
|
|
157
|
+
// previously stored one.
|
|
136
158
|
if (result.credential) {
|
|
137
159
|
await idb.set("flow.activeCredential", result.credential);
|
|
138
160
|
}
|
|
@@ -142,10 +164,24 @@ export function createFlow(options = {}) {
|
|
|
142
164
|
return false;
|
|
143
165
|
}
|
|
144
166
|
}
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
167
|
+
// Every refresh-token POST — boot, ensureGuest, and getToken — funnels through
|
|
168
|
+
// this one lock so two callers can't present the same stored token at once.
|
|
169
|
+
// Rotation is single-use: a token replayed after it rotated looks like theft,
|
|
170
|
+
// tripping server-side reuse detection and revoking the whole lineage (which
|
|
171
|
+
// would then 401 → clear → re-guest). The lock serializes refreshes across
|
|
172
|
+
// tabs of the same origin, and the guard short-circuits a caller that arrives
|
|
173
|
+
// after a concurrent holder already minted a fresh JWT, so no one re-presents
|
|
174
|
+
// an already-rotated token. refreshViaToken re-reads the token fresh from
|
|
175
|
+
// storage inside the lock, so a genuinely-second refresh uses the rotated one.
|
|
176
|
+
async function doRefresh() {
|
|
177
|
+
return withGuestLock(`flow.id.refresh:${host}`, async () => {
|
|
178
|
+
const current = store.getSnapshot().jwt;
|
|
179
|
+
if (current && !isExpiring(current))
|
|
180
|
+
return true;
|
|
181
|
+
return refreshViaToken();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const refreshJwt = doRefresh;
|
|
149
185
|
// Dedupe concurrent refreshes: a burst of getToken() calls that all find the
|
|
150
186
|
// cached token expired should trigger one mint, not one per call.
|
|
151
187
|
let refreshInFlight = null;
|
|
@@ -192,17 +228,18 @@ export function createFlow(options = {}) {
|
|
|
192
228
|
return true;
|
|
193
229
|
if (!guestInFlight) {
|
|
194
230
|
guestInFlight = withGuestLock(`flow.id.guest:${host}`, async () => {
|
|
195
|
-
// Re-check under the lock: another tab may have minted the guest
|
|
196
|
-
//
|
|
197
|
-
//
|
|
231
|
+
// Re-check under the lock: another tab may have minted the guest while
|
|
232
|
+
// we waited. Refresh-first (from this origin's stored token) recovers an
|
|
233
|
+
// existing session instead of minting a duplicate guest.
|
|
198
234
|
if (store.getSnapshot().user)
|
|
199
235
|
return true;
|
|
200
|
-
if (await
|
|
236
|
+
if (await doRefresh())
|
|
201
237
|
return true;
|
|
202
238
|
try {
|
|
203
239
|
const result = await getDialog().requestSilent(METHODS.guest, []);
|
|
204
240
|
if (!result.user)
|
|
205
241
|
return false;
|
|
242
|
+
refreshStore.set(result.refreshToken);
|
|
206
243
|
store.setState({
|
|
207
244
|
user: result.user,
|
|
208
245
|
jwt: result.jwt,
|
|
@@ -225,10 +262,10 @@ export function createFlow(options = {}) {
|
|
|
225
262
|
void (async () => {
|
|
226
263
|
await restoreCredential(store);
|
|
227
264
|
if (options.autoRestore !== false) {
|
|
228
|
-
const restored = await
|
|
229
|
-
// ensureGuest re-checks under a cross-tab lock
|
|
230
|
-
// minting, so calling it after a failed
|
|
231
|
-
// duplicate guests across tabs — the extra
|
|
265
|
+
const restored = await doRefresh();
|
|
266
|
+
// ensureGuest re-checks (refresh-first) under a cross-tab lock before
|
|
267
|
+
// minting, so calling it after a failed refresh can't fork a visitor into
|
|
268
|
+
// duplicate guests across tabs — the extra refresh is the dedup.
|
|
232
269
|
if (!restored && options.autoGuest)
|
|
233
270
|
await ensureGuest();
|
|
234
271
|
}
|
|
@@ -270,12 +307,13 @@ export function createFlow(options = {}) {
|
|
|
270
307
|
accessKeyPrep = await accessKeyModule.prepareAccessKey(accessKeyOptions, chain.id);
|
|
271
308
|
extraCapabilities = { accessKeyHash: accessKeyPrep.accessKeyHash };
|
|
272
309
|
}
|
|
273
|
-
const { session, webauthn } = await runLogin({
|
|
310
|
+
const { session, webauthn, refreshToken } = await runLogin({
|
|
274
311
|
dialog: dialogHost,
|
|
275
312
|
store,
|
|
276
313
|
options: loginOpts,
|
|
277
314
|
...(extraCapabilities ? { extraCapabilities } : {}),
|
|
278
315
|
});
|
|
316
|
+
refreshStore.set(refreshToken);
|
|
279
317
|
if (accessKeyModule && accessKeyPrep && webauthn) {
|
|
280
318
|
await accessKeyModule.finalizeAccessKey({
|
|
281
319
|
address: session.address,
|
|
@@ -310,6 +348,7 @@ export function createFlow(options = {}) {
|
|
|
310
348
|
// Server-side sign-out failed (offline?) — still clear local state.
|
|
311
349
|
}
|
|
312
350
|
await runLogout(store);
|
|
351
|
+
refreshStore.clear();
|
|
313
352
|
dialog?.close();
|
|
314
353
|
// An autoGuest app is never truly "signed out" — it always wants at least a
|
|
315
354
|
// guest session. Re-mint one so the UI (e.g. the profile widget pill) keeps
|
|
@@ -338,7 +377,7 @@ export function createFlow(options = {}) {
|
|
|
338
377
|
},
|
|
339
378
|
login,
|
|
340
379
|
logout,
|
|
341
|
-
restore,
|
|
380
|
+
restore: refreshJwt,
|
|
342
381
|
ensureGuest,
|
|
343
382
|
refreshJwt,
|
|
344
383
|
getToken,
|
|
@@ -4,7 +4,6 @@ export declare const METHODS: {
|
|
|
4
4
|
readonly signTypedData: "eth_signTypedData";
|
|
5
5
|
readonly sendTransaction: "eth_sendTransaction";
|
|
6
6
|
readonly sendCalls: "wallet_sendCalls";
|
|
7
|
-
readonly restore: "wallet_restore";
|
|
8
7
|
readonly guest: "wallet_guest";
|
|
9
8
|
readonly signOut: "wallet_signout";
|
|
10
9
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party persistence for the rotating refresh token. It lives in the
|
|
3
|
+
* consumer app's OWN localStorage (origin-scoped), so it survives reloads and
|
|
4
|
+
* navigations and — unlike the old id.flow.industries cookie — is never a
|
|
5
|
+
* third-party cookie, so iOS ITP can't block it. Keyed by `host` so pointing
|
|
6
|
+
* the SDK at a different Flow ID origin (dev vs prod) doesn't collide.
|
|
7
|
+
*/
|
|
8
|
+
export interface RefreshStore {
|
|
9
|
+
get(): string | null;
|
|
10
|
+
set(token: string): void;
|
|
11
|
+
clear(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function makeRefreshStore(host: string): RefreshStore;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function makeRefreshStore(host) {
|
|
2
|
+
const key = `flow.id.refresh:${host}`;
|
|
3
|
+
return {
|
|
4
|
+
get() {
|
|
5
|
+
try {
|
|
6
|
+
return localStorage.getItem(key);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
set(token) {
|
|
13
|
+
try {
|
|
14
|
+
localStorage.setItem(key, token);
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
},
|
|
18
|
+
clear() {
|
|
19
|
+
try {
|
|
20
|
+
localStorage.removeItem(key);
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -40,12 +40,13 @@ export async function runLogin(params) {
|
|
|
40
40
|
const result = (await dialog.request(METHODS.connect, [
|
|
41
41
|
{ capabilities },
|
|
42
42
|
]));
|
|
43
|
-
const { jwt, user, credential, webauthn } = result;
|
|
43
|
+
const { jwt, refreshToken, user, credential, webauthn } = result;
|
|
44
44
|
const address = credentialToAddress(credential);
|
|
45
45
|
await idb.set(CREDENTIAL_KEY, credential);
|
|
46
46
|
store.setState({ user, jwt, credential, address });
|
|
47
47
|
return {
|
|
48
48
|
session: { user, jwt, credential, address },
|
|
49
|
+
refreshToken,
|
|
49
50
|
webauthn,
|
|
50
51
|
};
|
|
51
52
|
}
|
package/dist/sdk/types/auth.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export type AuthOutcome = "success" | "failure" | "info";
|
|
|
8
8
|
export type AuthMode = "sign-up" | "sign-in";
|
|
9
9
|
/** Client-only funnel steps reported via the `/api/events` beacon. */
|
|
10
10
|
export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "done_shown";
|
|
11
|
-
export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.audience.rejected" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.done_shown";
|
|
12
|
-
export type AuthErrorCode = "username_taken" | "credential_taken" | "email_taken" | "email_not_verified" | "no_email" | "image_upload_forbidden" | "image_upload_rate_limited" | "invalid_image_type" | "image_too_large" | "image_upload_failed" | "otp_invalid" | "otp_expired" | "otp_attempts_exceeded" | "otp_resend_cooldown" | "otp_resend_limit" | "otp_global_limit" | "otp_send_failed" | "challenge_expired" | "unknown_credential" | "invalid_assertion_type" | "invalid_assertion_origin" | "user_verification_required" | "invalid_signature" | "user_not_found" | "missing_audience" | "audience_not_allowed" | "audience_mismatch" | "no_session" | "no_passkey" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "internal_error";
|
|
11
|
+
export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.audience.rejected" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.done_shown";
|
|
12
|
+
export type AuthErrorCode = "username_taken" | "credential_taken" | "email_taken" | "email_not_verified" | "no_email" | "image_upload_forbidden" | "image_upload_rate_limited" | "invalid_image_type" | "image_too_large" | "image_upload_failed" | "otp_invalid" | "otp_expired" | "otp_attempts_exceeded" | "otp_resend_cooldown" | "otp_resend_limit" | "otp_global_limit" | "otp_send_failed" | "challenge_expired" | "unknown_credential" | "invalid_assertion_type" | "invalid_assertion_origin" | "user_verification_required" | "invalid_signature" | "user_not_found" | "missing_audience" | "audience_not_allowed" | "audience_mismatch" | "no_session" | "no_passkey" | "refresh_token_invalid" | "refresh_token_expired" | "refresh_reuse_detected" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "internal_error";
|
|
13
13
|
/** One flat record per event = one row in the `auth_events` stream. */
|
|
14
14
|
export interface AuthEventRecord {
|
|
15
15
|
service: "auth";
|
|
@@ -2,7 +2,8 @@ export type { AuthConfig, AuthResponse, AuthResponseWithWebAuthn, FlowCredential
|
|
|
2
2
|
export type { BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, ProfileData, ProfileIdentity, } from "./dialog";
|
|
3
3
|
export type { AuthErrorCode, AuthEventName, AuthEventRecord, AuthMode, AuthOutcome, BeaconBody, FunnelStep, } from "./events";
|
|
4
4
|
export type { Bridge, BridgeParameters, FlowAccount, FlowRemote, FlowRemoteConfig, FromWindowOptions, MessageResponse, Messenger, OneOf, Payload, QueuedRequest, ReadyOptions, RemoteFlowState, RemoteState, Schema, Storage, Topic, WithReady, } from "./messenger";
|
|
5
|
-
export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, GuestRequest, GuestResponse, MethodName, MethodParams, MethodResult,
|
|
5
|
+
export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, GuestRequest, GuestResponse, MethodName, MethodParams, MethodResult, RestoreResponse, RpcRequest, SendCallsParams, SendCallsRequest, SendCallsResponse, SendTransactionParams, SendTransactionRequest, SendTransactionResponse, SignMessageRequest, SignMessageResponse, SignOutRequest, SignOutResponse, SignTypedDataRequest, SignTypedDataResponse, TransactionArgs, TypedData, TypedDataDomain, TypedDataField, } from "./protocol";
|
|
6
6
|
export { isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
|
|
7
7
|
export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowIdProviderProps, FlowState, Listener, LoginOptions, MountProfileOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ProfileButtonHandle, ProfileButtonProps, ProfilePosition, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
|
|
8
8
|
export type { CoinAsset, IdentifiedTx, TxApprove, TxConvert, TxSend, TxSwap, } from "./tx";
|
|
9
|
+
export type { ActionDayContext, ActionEventKind, ActionSessionState, ActiveActionResponse, LevelProgress, PublicProfile, XpGrantResult, XpRecentGrant, XpSummary, } from "./xp";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
|
|
2
2
|
import type { Address } from "./sdk";
|
|
3
|
-
export type MethodName = "wallet_connect" | "personal_sign" | "eth_signTypedData" | "eth_sendTransaction" | "wallet_sendCalls" | "
|
|
3
|
+
export type MethodName = "wallet_connect" | "personal_sign" | "eth_signTypedData" | "eth_sendTransaction" | "wallet_sendCalls" | "wallet_guest" | "wallet_signout";
|
|
4
4
|
export type ConnectCapabilities = {
|
|
5
5
|
createAccount?: boolean;
|
|
6
6
|
signIn?: boolean;
|
|
@@ -13,6 +13,8 @@ export type ConnectRequest = [{
|
|
|
13
13
|
}];
|
|
14
14
|
export type ConnectResponse = {
|
|
15
15
|
jwt: string;
|
|
16
|
+
/** First-party rotating refresh token; the SDK persists it in the app origin. */
|
|
17
|
+
refreshToken: string;
|
|
16
18
|
user: FlowUser;
|
|
17
19
|
credential: FlowCredential;
|
|
18
20
|
webauthn?: WebAuthnSignature;
|
|
@@ -58,12 +60,12 @@ export type SendCallsRequest = [
|
|
|
58
60
|
export type SendCallsResponse = {
|
|
59
61
|
id: string;
|
|
60
62
|
};
|
|
61
|
-
export type RestoreRequest = [];
|
|
62
63
|
export type RestoreResponse = {
|
|
63
64
|
jwt: string;
|
|
65
|
+
refreshToken: string;
|
|
64
66
|
user: FlowUser;
|
|
65
|
-
credential: FlowCredential;
|
|
66
|
-
address: Address;
|
|
67
|
+
credential: FlowCredential | null;
|
|
68
|
+
address: Address | null;
|
|
67
69
|
};
|
|
68
70
|
export type GuestRequest = [];
|
|
69
71
|
/**
|
|
@@ -73,6 +75,7 @@ export type GuestRequest = [];
|
|
|
73
75
|
*/
|
|
74
76
|
export type GuestResponse = {
|
|
75
77
|
jwt: string;
|
|
78
|
+
refreshToken: string;
|
|
76
79
|
user: FlowUser;
|
|
77
80
|
credential: null;
|
|
78
81
|
address: null;
|
|
@@ -87,7 +90,6 @@ export type MethodParams = {
|
|
|
87
90
|
eth_signTypedData: SignTypedDataRequest;
|
|
88
91
|
eth_sendTransaction: SendTransactionRequest;
|
|
89
92
|
wallet_sendCalls: SendCallsRequest;
|
|
90
|
-
wallet_restore: RestoreRequest;
|
|
91
93
|
wallet_guest: GuestRequest;
|
|
92
94
|
wallet_signout: SignOutRequest;
|
|
93
95
|
};
|
|
@@ -97,7 +99,6 @@ export type MethodResult = {
|
|
|
97
99
|
eth_signTypedData: SignTypedDataResponse;
|
|
98
100
|
eth_sendTransaction: SendTransactionResponse;
|
|
99
101
|
wallet_sendCalls: SendCallsResponse;
|
|
100
|
-
wallet_restore: RestoreResponse;
|
|
101
102
|
wallet_guest: GuestResponse;
|
|
102
103
|
wallet_signout: SignOutResponse;
|
|
103
104
|
};
|
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** XP / leveling shapes that cross the server/dialog boundary. */
|
|
2
|
+
export interface LevelProgress {
|
|
3
|
+
/** Level within the current tier, 1..100 — it resets to 1 on each tier-up. */
|
|
4
|
+
level: number;
|
|
5
|
+
/** Prestige tier, 1..5. Reaching level 100 rolls into the next tier. */
|
|
6
|
+
tier: number;
|
|
7
|
+
/** XP earned since reaching the current level. */
|
|
8
|
+
xpIntoLevel: number;
|
|
9
|
+
/** XP between the current level's floor and the next level's floor. */
|
|
10
|
+
xpForNextLevel: number;
|
|
11
|
+
}
|
|
12
|
+
/** One recent ledger grant. Carries only the static source id — the display
|
|
13
|
+
* label resolves client-side from the registry, so renames never touch stored
|
|
14
|
+
* rows or the wire shape. */
|
|
15
|
+
export interface XpRecentGrant {
|
|
16
|
+
source: string;
|
|
17
|
+
amount: number;
|
|
18
|
+
/** ISO timestamp of the grant, for newer-than-last-seen diffing. */
|
|
19
|
+
at: string;
|
|
20
|
+
}
|
|
21
|
+
/** Response shape of GET /api/xp/me. */
|
|
22
|
+
export interface XpSummary extends LevelProgress {
|
|
23
|
+
totalXp: number;
|
|
24
|
+
flowScore: number;
|
|
25
|
+
/** Last few positive grants, newest first. Optional so client-synthesized
|
|
26
|
+
* summaries (the widget's per-frame lerp) stay valid without it. */
|
|
27
|
+
recentGrants?: XpRecentGrant[];
|
|
28
|
+
}
|
|
29
|
+
/** Outcome of one grant attempt. `granted: false` covers idempotent replays,
|
|
30
|
+
* sub-minimum durations and exhausted daily caps — all non-errors. Carries the
|
|
31
|
+
* post-grant standing so reporters (the game server) can refresh a player's
|
|
32
|
+
* nametag level without a second request. */
|
|
33
|
+
export interface XpGrantResult extends LevelProgress {
|
|
34
|
+
granted: boolean;
|
|
35
|
+
amount: number;
|
|
36
|
+
totalXp: number;
|
|
37
|
+
flowScore: number;
|
|
38
|
+
}
|
|
39
|
+
/** Response of GET /api/xp/day/:source — what the in-game action timer needs to
|
|
40
|
+
* estimate a live session's XP: today's prior minutes for the source (so the
|
|
41
|
+
* day-cumulative curve starts from the right point) and the flow multiplier. */
|
|
42
|
+
export interface ActionDayContext {
|
|
43
|
+
priorMinutes: number;
|
|
44
|
+
flowScore: number;
|
|
45
|
+
}
|
|
46
|
+
/** Lifecycle state of a durable action session. `abandoned` is set by the
|
|
47
|
+
* orphan reaper when the game server stops heartbeating a still-active
|
|
48
|
+
* session (e.g. a server crash). */
|
|
49
|
+
export type ActionSessionState = "active" | "finished" | "abandoned";
|
|
50
|
+
/** An action-session transition kind, appended to the action_event log.
|
|
51
|
+
* `focus_lost`/`focus_gained` mark the tab going background/foreground — accrual
|
|
52
|
+
* never stops (they only split focused vs unfocused time for stats). */
|
|
53
|
+
export type ActionEventKind = "start" | "focus_lost" | "focus_gained" | "finish" | "abandon";
|
|
54
|
+
/** Response of GET /api/actions/active — the caller's current in-progress
|
|
55
|
+
* action (or null when idle), plus the day context so the timer can render the
|
|
56
|
+
* live XP estimate from the server-authoritative accrued duration. (Always an
|
|
57
|
+
* `active` session by definition, so the lifecycle state isn't carried.) */
|
|
58
|
+
export interface ActiveActionResponse extends ActionDayContext {
|
|
59
|
+
source: string;
|
|
60
|
+
label: string | null;
|
|
61
|
+
accruedSeconds: number;
|
|
62
|
+
}
|
|
63
|
+
/** Non-sensitive profile facts any player may see about another — nested into
|
|
64
|
+
* the /api/session/verify payload beside `appearance` so game servers can
|
|
65
|
+
* broadcast them (avatar + level badge on nametags) without extra fetches.
|
|
66
|
+
* snake_case keys to match the appearance object in the same payload; the
|
|
67
|
+
* Godot consumer parses both symmetrically. */
|
|
68
|
+
export interface PublicProfile {
|
|
69
|
+
avatar_url: string | null;
|
|
70
|
+
tier: number;
|
|
71
|
+
level: number;
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/** XP / leveling shapes that cross the server/dialog boundary. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow-industries/id",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"main": "./dist/sdk/client/index.js",
|
|
5
5
|
"module": "./dist/sdk/client/index.js",
|
|
6
6
|
"types": "./dist/sdk/client/index.d.ts",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"db:push": "bunx drizzle-kit push",
|
|
54
54
|
"db:studio": "bunx drizzle-kit studio",
|
|
55
55
|
"lint": "biome check",
|
|
56
|
+
"test": "bun test src",
|
|
56
57
|
"format": "biome format --write",
|
|
57
58
|
"check": "biome check --write",
|
|
58
59
|
"typecheck": "tsr generate && tsc --noEmit"
|