@fytkit/sdk 3.4.0 → 4.0.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/CHANGELOG.md +30 -0
- package/README.md +125 -7
- package/dist/index.d.ts +4076 -807
- package/dist/index.js +477 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ORPCError, isORPCErrorJson, createORPCErrorFromJson, createORPCClient } from '@orpc/client';
|
|
2
2
|
import { RPCLink } from '@orpc/client/fetch';
|
|
3
3
|
|
|
4
|
+
// src/error.ts
|
|
5
|
+
|
|
6
|
+
// src/is-record.ts
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/refusal.ts
|
|
12
|
+
function refusalData(error, reason) {
|
|
13
|
+
if (!isRecord(error) || typeof error.kind !== "string") return null;
|
|
14
|
+
const refusal = error.refusal;
|
|
15
|
+
if (!isRecord(refusal) || refusal.reason !== reason || !isRecord(refusal.data)) return null;
|
|
16
|
+
return refusal.data;
|
|
17
|
+
}
|
|
18
|
+
var MEMBER_CAP_QUOTA = "max_clients";
|
|
19
|
+
function pickRefusal(data) {
|
|
20
|
+
if (!isRecord(data)) return {};
|
|
21
|
+
const { reason, ...rest } = data;
|
|
22
|
+
if (typeof reason === "string" && reason.length > 0) return { refusal: { reason, data: rest } };
|
|
23
|
+
if (data.quotaCode === MEMBER_CAP_QUOTA)
|
|
24
|
+
return { refusal: { reason: MEMBER_CAP_QUOTA, data: {} } };
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
// src/error.ts
|
|
5
29
|
function pickIssues(data) {
|
|
6
30
|
if (!data || typeof data !== "object") return null;
|
|
@@ -31,7 +55,8 @@ function normalizeError(err) {
|
|
|
31
55
|
return {
|
|
32
56
|
kind: "forbidden",
|
|
33
57
|
reason: message || "Forbidden",
|
|
34
|
-
...pickStringField(data, "code")
|
|
58
|
+
...pickStringField(data, "code"),
|
|
59
|
+
...pickRefusal(data)
|
|
35
60
|
};
|
|
36
61
|
case "PAYMENT_REQUIRED":
|
|
37
62
|
return { kind: "payment_required", message: message || "Payment Required" };
|
|
@@ -39,25 +64,60 @@ function normalizeError(err) {
|
|
|
39
64
|
return {
|
|
40
65
|
kind: "conflict",
|
|
41
66
|
message: message || "Conflict",
|
|
42
|
-
...pickStringField(data, "enrollmentId")
|
|
67
|
+
...pickStringField(data, "enrollmentId"),
|
|
68
|
+
...pickRefusal(data)
|
|
43
69
|
};
|
|
44
70
|
case "NOT_FOUND":
|
|
45
|
-
return { kind: "not_found" };
|
|
71
|
+
return { kind: "not_found", ...pickRefusal(data) };
|
|
46
72
|
case "TOO_MANY_REQUESTS":
|
|
47
|
-
return { kind: "rate_limited", retryAfter: pickRetryAfter(data) };
|
|
73
|
+
return { kind: "rate_limited", retryAfter: pickRetryAfter(data), ...pickRefusal(data) };
|
|
48
74
|
case "BAD_REQUEST":
|
|
49
75
|
case "UNPROCESSABLE_CONTENT": {
|
|
50
76
|
const issues = pickIssues(data);
|
|
51
|
-
if (issues) return { kind: "validation", issues };
|
|
52
|
-
return {
|
|
77
|
+
if (issues) return { kind: "validation", issues, ...pickRefusal(data) };
|
|
78
|
+
return {
|
|
79
|
+
kind: "server",
|
|
80
|
+
status,
|
|
81
|
+
message,
|
|
82
|
+
...pickStringField(data, "code"),
|
|
83
|
+
...pickRefusal(data)
|
|
84
|
+
};
|
|
53
85
|
}
|
|
54
86
|
default:
|
|
55
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
kind: "server",
|
|
89
|
+
status,
|
|
90
|
+
message,
|
|
91
|
+
...pickStringField(data, "code"),
|
|
92
|
+
...pickRefusal(data)
|
|
93
|
+
};
|
|
56
94
|
}
|
|
57
95
|
}
|
|
58
96
|
return { kind: "server", status: 0 };
|
|
59
97
|
}
|
|
60
98
|
|
|
99
|
+
// src/app.ts
|
|
100
|
+
var UNEXPECTED_NOT_MODIFIED = { kind: "server", status: 304 };
|
|
101
|
+
function createAppNamespace(transport) {
|
|
102
|
+
const bootstrap = async (options = {}) => {
|
|
103
|
+
const etag = options.etag ?? null;
|
|
104
|
+
let output;
|
|
105
|
+
try {
|
|
106
|
+
output = await transport.client.app.bootstrap(void 0, {
|
|
107
|
+
context: etag === null ? {} : { ifNoneMatch: etag }
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw normalizeError(error);
|
|
111
|
+
}
|
|
112
|
+
if (output === void 0 || output === null) {
|
|
113
|
+
if (etag === null) throw UNEXPECTED_NOT_MODIFIED;
|
|
114
|
+
return { status: "unchanged", etag };
|
|
115
|
+
}
|
|
116
|
+
return { status: "ok", data: output.data, etag: output.meta.etag };
|
|
117
|
+
};
|
|
118
|
+
return { bootstrap };
|
|
119
|
+
}
|
|
120
|
+
|
|
61
121
|
// src/events.ts
|
|
62
122
|
function createEventBus() {
|
|
63
123
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -198,6 +258,10 @@ function createAuthNamespace(opts) {
|
|
|
198
258
|
const confirmEmail = async (input) => {
|
|
199
259
|
await call(() => opts.transport.client.auth.confirmEmail(input));
|
|
200
260
|
};
|
|
261
|
+
const resendVerification = async () => {
|
|
262
|
+
const result = await call(() => opts.transport.client.auth.resendVerification({}));
|
|
263
|
+
return result.data;
|
|
264
|
+
};
|
|
201
265
|
const verifyTotp = async (input) => {
|
|
202
266
|
const result = await call(
|
|
203
267
|
() => opts.transport.client.auth.verifyTotp({ token: input.challenge.token, code: input.code })
|
|
@@ -271,6 +335,9 @@ function createAuthNamespace(opts) {
|
|
|
271
335
|
const changeEmail = async (input) => {
|
|
272
336
|
await call(() => opts.transport.client.auth.changeEmail(input));
|
|
273
337
|
};
|
|
338
|
+
const confirmEmailChange = async (input) => {
|
|
339
|
+
await call(() => opts.transport.client.auth.confirmEmailChange(input));
|
|
340
|
+
};
|
|
274
341
|
const listSessions = async () => {
|
|
275
342
|
const result = await call(() => opts.transport.client.auth.listSessions());
|
|
276
343
|
return result.data;
|
|
@@ -327,6 +394,7 @@ function createAuthNamespace(opts) {
|
|
|
327
394
|
requestPasswordReset,
|
|
328
395
|
confirmPasswordReset,
|
|
329
396
|
confirmEmail,
|
|
397
|
+
resendVerification,
|
|
330
398
|
verifyTotp,
|
|
331
399
|
verifyBackupCode,
|
|
332
400
|
requestEmailOtp,
|
|
@@ -338,6 +406,7 @@ function createAuthNamespace(opts) {
|
|
|
338
406
|
regenerateBackupCodes,
|
|
339
407
|
changePassword,
|
|
340
408
|
changeEmail,
|
|
409
|
+
confirmEmailChange,
|
|
341
410
|
listSessions,
|
|
342
411
|
revokeSession,
|
|
343
412
|
revokeOtherSessions,
|
|
@@ -347,6 +416,9 @@ function createAuthNamespace(opts) {
|
|
|
347
416
|
};
|
|
348
417
|
}
|
|
349
418
|
|
|
419
|
+
// src/base-url.ts
|
|
420
|
+
var DEFAULT_BASE_URL = "https://api.fytkit.com";
|
|
421
|
+
|
|
350
422
|
// src/data.ts
|
|
351
423
|
function wrapSlice(slice) {
|
|
352
424
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -385,9 +457,15 @@ function createDataNamespaces(transport) {
|
|
|
385
457
|
sessionLogs: wrapSlice(c.sessionLog),
|
|
386
458
|
progression: wrapSlice(c.progression),
|
|
387
459
|
bodyMetrics: wrapSlice(c.bodyMetric),
|
|
460
|
+
bodyMeasurements: wrapSlice(c.bodyMeasurement),
|
|
461
|
+
progressPhotos: wrapSlice(c.progressPhoto),
|
|
462
|
+
checkIns: wrapSlice(c.checkin),
|
|
463
|
+
messages: wrapSlice(c.message),
|
|
464
|
+
timeline: wrapSlice(c.timeline),
|
|
465
|
+
health: wrapSlice(c.health),
|
|
466
|
+
habits: wrapSlice(c.habit),
|
|
388
467
|
progress: wrapSlice(c.progress),
|
|
389
468
|
media: wrapSlice(c.media),
|
|
390
|
-
appConfig: wrapSlice(c.appConfig),
|
|
391
469
|
subscription: wrapSlice(c.subscription),
|
|
392
470
|
profile: wrapSlice(c.profile),
|
|
393
471
|
nutritionLog: wrapSlice(c.nutritionLog),
|
|
@@ -445,18 +523,71 @@ function createMemoryStorageAdapter() {
|
|
|
445
523
|
}
|
|
446
524
|
};
|
|
447
525
|
}
|
|
526
|
+
|
|
527
|
+
// src/retry-after.ts
|
|
528
|
+
var DELAY_SECONDS = /^\d+$/;
|
|
529
|
+
function retryAfterHeaderMs(value, now) {
|
|
530
|
+
if (value === null) return null;
|
|
531
|
+
const trimmed = value.trim();
|
|
532
|
+
if (trimmed.length === 0) return null;
|
|
533
|
+
if (DELAY_SECONDS.test(trimmed)) return Number(trimmed) * 1e3;
|
|
534
|
+
if (!/[a-z]/i.test(trimmed)) return null;
|
|
535
|
+
const at = Date.parse(trimmed);
|
|
536
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
537
|
+
}
|
|
538
|
+
function parseJson(text) {
|
|
539
|
+
try {
|
|
540
|
+
return JSON.parse(text);
|
|
541
|
+
} catch {
|
|
542
|
+
return void 0;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function hintedBody(body, waitMs) {
|
|
546
|
+
if (isRecord(body) && isRecord(body.json) && typeof body.json.code === "string") {
|
|
547
|
+
const error = body.json;
|
|
548
|
+
const data = isRecord(error.data) ? error.data : {};
|
|
549
|
+
if (typeof data.retryAfterMs === "number") return null;
|
|
550
|
+
return { ...body, json: { ...error, data: { ...data, retryAfterMs: waitMs } } };
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
json: {
|
|
554
|
+
defined: false,
|
|
555
|
+
code: "TOO_MANY_REQUESTS",
|
|
556
|
+
status: 429,
|
|
557
|
+
message: "Too Many Requests",
|
|
558
|
+
data: { retryAfterMs: waitMs }
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async function withRetryAfterHint(response, now) {
|
|
563
|
+
if (response.status !== 429) return response;
|
|
564
|
+
const waitMs = retryAfterHeaderMs(response.headers.get("retry-after"), now);
|
|
565
|
+
if (waitMs === null) return response;
|
|
566
|
+
const hinted = hintedBody(parseJson(await response.clone().text()), waitMs);
|
|
567
|
+
if (hinted === null) return response;
|
|
568
|
+
const headers = new Headers(response.headers);
|
|
569
|
+
headers.set("content-type", "application/json");
|
|
570
|
+
headers.delete("content-length");
|
|
571
|
+
return new Response(JSON.stringify(hinted), {
|
|
572
|
+
status: response.status,
|
|
573
|
+
statusText: response.statusText,
|
|
574
|
+
headers
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/transport.ts
|
|
448
579
|
function createTransport(options) {
|
|
449
580
|
let interceptor = null;
|
|
450
581
|
const baseFetch = options.fetch ?? fetch.bind(globalThis);
|
|
451
|
-
const wrappedFetch = (input, init) => {
|
|
582
|
+
const wrappedFetch = async (input, init) => {
|
|
452
583
|
const request = input instanceof Request ? input : new Request(input, init);
|
|
453
584
|
const next = (req) => baseFetch(req);
|
|
454
|
-
|
|
455
|
-
return
|
|
585
|
+
const response = interceptor ? await interceptor(request, next) : await next(request);
|
|
586
|
+
return withRetryAfterHint(response, Date.now());
|
|
456
587
|
};
|
|
457
588
|
const link = new RPCLink({
|
|
458
589
|
url: () => `${options.baseUrl}/rpc`,
|
|
459
|
-
headers: () => {
|
|
590
|
+
headers: ({ context }) => {
|
|
460
591
|
const session = options.getSession();
|
|
461
592
|
const headers = {
|
|
462
593
|
"X-Fytkit-Client-Id": options.clientId
|
|
@@ -464,6 +595,9 @@ function createTransport(options) {
|
|
|
464
595
|
if (session) {
|
|
465
596
|
headers.Authorization = `Bearer ${session.accessToken}`;
|
|
466
597
|
}
|
|
598
|
+
if (context.ifNoneMatch !== void 0) {
|
|
599
|
+
headers["If-None-Match"] = context.ifNoneMatch;
|
|
600
|
+
}
|
|
467
601
|
return headers;
|
|
468
602
|
},
|
|
469
603
|
fetch: wrappedFetch
|
|
@@ -483,6 +617,7 @@ var CREDENTIAL_PROCEDURES = [
|
|
|
483
617
|
"auth/requestPasswordReset",
|
|
484
618
|
"auth/confirmPasswordReset",
|
|
485
619
|
"auth/confirmEmail",
|
|
620
|
+
"auth/confirmEmailChange",
|
|
486
621
|
"auth/verifyTotp",
|
|
487
622
|
"auth/verifyBackupCode",
|
|
488
623
|
"auth/requestEmailOtp",
|
|
@@ -550,9 +685,331 @@ function attachRefreshInterceptor(opts) {
|
|
|
550
685
|
return retried;
|
|
551
686
|
});
|
|
552
687
|
}
|
|
688
|
+
var INACTIVE = {
|
|
689
|
+
kind: "not_found",
|
|
690
|
+
refusal: { reason: "join_code_inactive", data: {} }
|
|
691
|
+
};
|
|
692
|
+
var IGNORED_SEPARATORS = /[\s\-\u2010-\u2015\u2212]/g;
|
|
693
|
+
var PATH_SAFE_CODE = /^[A-Za-z0-9]{1,64}$/;
|
|
694
|
+
function pathSafeJoinCode(input) {
|
|
695
|
+
const candidate = input.replace(IGNORED_SEPARATORS, "");
|
|
696
|
+
return PATH_SAFE_CODE.test(candidate) ? candidate.toUpperCase() : null;
|
|
697
|
+
}
|
|
698
|
+
async function readJson(response) {
|
|
699
|
+
try {
|
|
700
|
+
return await response.json();
|
|
701
|
+
} catch {
|
|
702
|
+
return void 0;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function refusalOf(response, body) {
|
|
706
|
+
const wait = () => retryAfterHeaderMs(response.headers.get("retry-after"), Date.now()) ?? 0;
|
|
707
|
+
if (isORPCErrorJson(body)) {
|
|
708
|
+
const error = normalizeError(createORPCErrorFromJson(body));
|
|
709
|
+
if (error.kind === "rate_limited" && error.retryAfter === 0) {
|
|
710
|
+
return { ...error, retryAfter: wait() };
|
|
711
|
+
}
|
|
712
|
+
return error;
|
|
713
|
+
}
|
|
714
|
+
if (response.status === 429) return { kind: "rate_limited", retryAfter: wait() };
|
|
715
|
+
return { kind: "server", status: response.status };
|
|
716
|
+
}
|
|
717
|
+
async function resolveJoinCode(code, options = {}) {
|
|
718
|
+
const canonical = pathSafeJoinCode(code);
|
|
719
|
+
if (canonical === null) throw INACTIVE;
|
|
720
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
721
|
+
const doFetch = options.fetch ?? fetch.bind(globalThis);
|
|
722
|
+
let response;
|
|
723
|
+
try {
|
|
724
|
+
response = await doFetch(`${baseUrl}/api/v1/join/${canonical}`, {
|
|
725
|
+
method: "GET",
|
|
726
|
+
headers: { Accept: "application/json" }
|
|
727
|
+
});
|
|
728
|
+
} catch (error) {
|
|
729
|
+
throw normalizeError(error);
|
|
730
|
+
}
|
|
731
|
+
const body = await readJson(response);
|
|
732
|
+
if (response.ok) {
|
|
733
|
+
if (isRecord(body) && isRecord(body.data)) {
|
|
734
|
+
return body.data;
|
|
735
|
+
}
|
|
736
|
+
throw { kind: "server", status: response.status };
|
|
737
|
+
}
|
|
738
|
+
throw refusalOf(response, body);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/payment-policy.ts
|
|
742
|
+
var PLAN_INFO = { mode: "plan_info" };
|
|
743
|
+
function resolvePaymentMode(policy, options) {
|
|
744
|
+
if (!policy || !Array.isArray(policy.linkOut)) return PLAN_INFO;
|
|
745
|
+
const storefront = options.storefront?.trim().toUpperCase() ?? "";
|
|
746
|
+
if (storefront === "") return PLAN_INFO;
|
|
747
|
+
const rule = policy.linkOut.find(
|
|
748
|
+
(entry) => entry?.platform === options.platform && typeof entry.storefront === "string" && entry.storefront.toUpperCase() === storefront
|
|
749
|
+
);
|
|
750
|
+
if (!rule || !Array.isArray(rule.conditions)) return PLAN_INFO;
|
|
751
|
+
const met = new Set(options.conditionsMet ?? []);
|
|
752
|
+
if (!rule.conditions.every((condition) => met.has(condition))) return PLAN_INFO;
|
|
753
|
+
return { mode: "link_out", conditions: rule.conditions };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/realtime.ts
|
|
757
|
+
var MESSAGES_FALLBACK_POLL_MS = {
|
|
758
|
+
conversation: 1e4,
|
|
759
|
+
unreadCount: 6e4
|
|
760
|
+
};
|
|
761
|
+
var THREAD_CHANGED_EVENT = "thread_changed";
|
|
762
|
+
var MEMBER_TOPIC_PREFIX = "member:";
|
|
763
|
+
var TOKEN_RENEW_BEFORE_MS = 6e4;
|
|
764
|
+
var TOKEN_MIN_DELAY_MS = 5e3;
|
|
765
|
+
var RETRY_MS = 6e4;
|
|
766
|
+
var REJOIN_DELAYS_MS = [5e3, 15e3, 6e4];
|
|
767
|
+
var SIGNED_OUT_EVENTS = /* @__PURE__ */ new Set([
|
|
768
|
+
"signed_out",
|
|
769
|
+
"signed_out_due_to_token_reuse"
|
|
770
|
+
]);
|
|
771
|
+
var FINAL_ERROR_KINDS = /* @__PURE__ */ new Set([
|
|
772
|
+
"auth_required",
|
|
773
|
+
"forbidden",
|
|
774
|
+
"not_found",
|
|
775
|
+
"validation"
|
|
776
|
+
]);
|
|
777
|
+
function isFinalError(error) {
|
|
778
|
+
if (typeof error !== "object" || error === null || !("kind" in error)) return false;
|
|
779
|
+
const kind = error.kind;
|
|
780
|
+
return typeof kind === "string" && FINAL_ERROR_KINDS.has(kind);
|
|
781
|
+
}
|
|
782
|
+
function isMemberTopic(topic) {
|
|
783
|
+
return typeof topic === "string" && topic.startsWith(MEMBER_TOPIC_PREFIX) && topic.length > MEMBER_TOPIC_PREFIX.length;
|
|
784
|
+
}
|
|
785
|
+
function isConnection(value) {
|
|
786
|
+
if (typeof value !== "object" || value === null) return false;
|
|
787
|
+
if (!("url" in value) || !("anonKey" in value)) return false;
|
|
788
|
+
return typeof value.url === "string" && value.url.length > 0 && typeof value.anonKey === "string" && value.anonKey.length > 0;
|
|
789
|
+
}
|
|
790
|
+
function renewalDelay(expiresAt, now) {
|
|
791
|
+
const expiry = Date.parse(expiresAt);
|
|
792
|
+
if (Number.isNaN(expiry)) return RETRY_MS;
|
|
793
|
+
return Math.max(TOKEN_MIN_DELAY_MS, expiry - TOKEN_RENEW_BEFORE_MS - now);
|
|
794
|
+
}
|
|
795
|
+
function isUsable(expiresAt, now) {
|
|
796
|
+
return Date.parse(expiresAt) - now > TOKEN_MIN_DELAY_MS;
|
|
797
|
+
}
|
|
798
|
+
function rejoinDelay(closures) {
|
|
799
|
+
const last = REJOIN_DELAYS_MS.length - 1;
|
|
800
|
+
return REJOIN_DELAYS_MS[Math.min(Math.max(closures - 1, 0), last)];
|
|
801
|
+
}
|
|
802
|
+
function ignore() {
|
|
803
|
+
}
|
|
804
|
+
function authorise(client, token) {
|
|
805
|
+
return new Promise((resolve) => {
|
|
806
|
+
resolve(client.realtime.setAuth(token));
|
|
807
|
+
}).then(ignore, ignore);
|
|
808
|
+
}
|
|
809
|
+
function subscribeToMessages(client, options) {
|
|
810
|
+
let closed = false;
|
|
811
|
+
let lastStatus = null;
|
|
812
|
+
let timer = null;
|
|
813
|
+
let renewing = false;
|
|
814
|
+
let connection = null;
|
|
815
|
+
let socket = null;
|
|
816
|
+
let held = null;
|
|
817
|
+
let channel = null;
|
|
818
|
+
let rejoinTimer = null;
|
|
819
|
+
let closures = 0;
|
|
820
|
+
let stopAuthListener = null;
|
|
821
|
+
const report = (status) => {
|
|
822
|
+
if (closed || status === lastStatus) return;
|
|
823
|
+
lastStatus = status;
|
|
824
|
+
options.onStatus?.(status);
|
|
825
|
+
};
|
|
826
|
+
const clearTimer = () => {
|
|
827
|
+
if (timer === null) return;
|
|
828
|
+
clearTimeout(timer);
|
|
829
|
+
timer = null;
|
|
830
|
+
};
|
|
831
|
+
const after = (delayMs, run) => {
|
|
832
|
+
clearTimer();
|
|
833
|
+
if (closed) return;
|
|
834
|
+
timer = setTimeout(() => {
|
|
835
|
+
timer = null;
|
|
836
|
+
void run();
|
|
837
|
+
}, delayMs);
|
|
838
|
+
};
|
|
839
|
+
const cancelRejoin = () => {
|
|
840
|
+
if (rejoinTimer !== null) clearTimeout(rejoinTimer);
|
|
841
|
+
rejoinTimer = null;
|
|
842
|
+
};
|
|
843
|
+
const drop = () => {
|
|
844
|
+
const open = socket;
|
|
845
|
+
const joined = channel;
|
|
846
|
+
socket = null;
|
|
847
|
+
held = null;
|
|
848
|
+
channel = null;
|
|
849
|
+
cancelRejoin();
|
|
850
|
+
closures = 0;
|
|
851
|
+
if (open === null) return;
|
|
852
|
+
if (joined !== null) Promise.resolve(open.removeChannel(joined)).then(ignore, ignore);
|
|
853
|
+
open.realtime.disconnect();
|
|
854
|
+
};
|
|
855
|
+
const close = () => {
|
|
856
|
+
if (closed) return;
|
|
857
|
+
closed = true;
|
|
858
|
+
clearTimer();
|
|
859
|
+
drop();
|
|
860
|
+
const stop = stopAuthListener;
|
|
861
|
+
stopAuthListener = null;
|
|
862
|
+
stop?.();
|
|
863
|
+
};
|
|
864
|
+
const finish = (status) => {
|
|
865
|
+
if (closed) return;
|
|
866
|
+
const changed = status !== lastStatus;
|
|
867
|
+
lastStatus = status;
|
|
868
|
+
close();
|
|
869
|
+
if (changed) options.onStatus?.(status);
|
|
870
|
+
};
|
|
871
|
+
const join = (joinTopic, open) => {
|
|
872
|
+
if (closed || socket !== open || channel !== null) return;
|
|
873
|
+
const joined = open.channel(joinTopic, { config: { private: true } });
|
|
874
|
+
channel = joined;
|
|
875
|
+
const current = () => !closed && channel === joined;
|
|
876
|
+
joined.on("broadcast", { event: THREAD_CHANGED_EVENT }, () => {
|
|
877
|
+
if (current()) options.onChange();
|
|
878
|
+
}).subscribe((status) => {
|
|
879
|
+
if (!current()) return;
|
|
880
|
+
if (status === "SUBSCRIBED") {
|
|
881
|
+
closures = 0;
|
|
882
|
+
report("live");
|
|
883
|
+
options.onChange();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
report("fallback");
|
|
887
|
+
if (status === "CLOSED") {
|
|
888
|
+
channel = null;
|
|
889
|
+
scheduleRejoin();
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
const authoriseAndJoin = (grant, open) => {
|
|
894
|
+
void authorise(open, grant.token).then(() => join(grant.topic, open));
|
|
895
|
+
};
|
|
896
|
+
const rejoin = () => {
|
|
897
|
+
const open = socket;
|
|
898
|
+
const grant = held;
|
|
899
|
+
if (closed || open === null || grant === null || channel !== null) return;
|
|
900
|
+
if (!isUsable(grant.expiresAt, Date.now())) {
|
|
901
|
+
void renew();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
authoriseAndJoin(grant, open);
|
|
905
|
+
};
|
|
906
|
+
const scheduleRejoin = () => {
|
|
907
|
+
if (closed || rejoinTimer !== null) return;
|
|
908
|
+
closures += 1;
|
|
909
|
+
rejoinTimer = setTimeout(() => {
|
|
910
|
+
rejoinTimer = null;
|
|
911
|
+
rejoin();
|
|
912
|
+
}, rejoinDelay(closures));
|
|
913
|
+
};
|
|
914
|
+
const connect = (grant, settings) => {
|
|
915
|
+
let opened;
|
|
916
|
+
try {
|
|
917
|
+
opened = options.realtime(settings);
|
|
918
|
+
} catch {
|
|
919
|
+
finish("fallback");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
socket = opened;
|
|
923
|
+
held = grant;
|
|
924
|
+
after(renewalDelay(grant.expiresAt, Date.now()), renew);
|
|
925
|
+
authoriseAndJoin(grant, opened);
|
|
926
|
+
};
|
|
927
|
+
const openGrant = async (grant) => {
|
|
928
|
+
if (!isMemberTopic(grant.topic)) {
|
|
929
|
+
finish("fallback");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (connection === null) {
|
|
933
|
+
let realtime;
|
|
934
|
+
try {
|
|
935
|
+
const bootstrap = await client.app.bootstrap();
|
|
936
|
+
realtime = bootstrap.status === "ok" ? bootstrap.data.realtime : void 0;
|
|
937
|
+
} catch (error) {
|
|
938
|
+
if (closed) return;
|
|
939
|
+
if (isFinalError(error)) {
|
|
940
|
+
finish("fallback");
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
report("fallback");
|
|
944
|
+
after(RETRY_MS, start);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (closed) return;
|
|
948
|
+
if (!isConnection(realtime)) {
|
|
949
|
+
finish("fallback");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
connection = realtime;
|
|
953
|
+
}
|
|
954
|
+
connect(grant, connection);
|
|
955
|
+
};
|
|
956
|
+
const renew = async () => {
|
|
957
|
+
if (renewing) return;
|
|
958
|
+
renewing = true;
|
|
959
|
+
let grant;
|
|
960
|
+
try {
|
|
961
|
+
grant = (await client.messages.realtimeToken({})).data;
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (closed) return;
|
|
964
|
+
if (isFinalError(error)) finish("refused");
|
|
965
|
+
else after(RETRY_MS, renew);
|
|
966
|
+
return;
|
|
967
|
+
} finally {
|
|
968
|
+
renewing = false;
|
|
969
|
+
}
|
|
970
|
+
const current = socket;
|
|
971
|
+
if (closed || current === null || held === null) return;
|
|
972
|
+
if (grant.topic !== held.topic) {
|
|
973
|
+
drop();
|
|
974
|
+
report("connecting");
|
|
975
|
+
await openGrant(grant);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
held = grant;
|
|
979
|
+
after(renewalDelay(grant.expiresAt, Date.now()), renew);
|
|
980
|
+
if (channel !== null) {
|
|
981
|
+
void authorise(current, grant.token);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
cancelRejoin();
|
|
985
|
+
authoriseAndJoin(grant, current);
|
|
986
|
+
};
|
|
987
|
+
const start = async () => {
|
|
988
|
+
let grant;
|
|
989
|
+
try {
|
|
990
|
+
grant = (await client.messages.realtimeToken({})).data;
|
|
991
|
+
} catch (error) {
|
|
992
|
+
if (closed) return;
|
|
993
|
+
if (isFinalError(error)) {
|
|
994
|
+
finish("refused");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
report("fallback");
|
|
998
|
+
after(RETRY_MS, start);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (closed) return;
|
|
1002
|
+
await openGrant(grant);
|
|
1003
|
+
};
|
|
1004
|
+
report("connecting");
|
|
1005
|
+
stopAuthListener = client.auth?.onAuthStateChange((event) => {
|
|
1006
|
+
if (SIGNED_OUT_EVENTS.has(event.type)) finish("refused");
|
|
1007
|
+
}) ?? null;
|
|
1008
|
+
void start();
|
|
1009
|
+
return close;
|
|
1010
|
+
}
|
|
553
1011
|
|
|
554
1012
|
// src/index.ts
|
|
555
|
-
var DEFAULT_BASE_URL = "https://api.fytkit.com";
|
|
556
1013
|
function createFytkitClient(options) {
|
|
557
1014
|
if (!options || typeof options.clientId !== "string" || options.clientId.length === 0) {
|
|
558
1015
|
throw new Error("createFytkitClient: `clientId` is required");
|
|
@@ -571,6 +1028,7 @@ function createFytkitClient(options) {
|
|
|
571
1028
|
attachRefreshInterceptor({ transport, auth: authInstance });
|
|
572
1029
|
const data = createDataNamespaces(transport);
|
|
573
1030
|
return {
|
|
1031
|
+
clientId: options.clientId,
|
|
574
1032
|
auth: {
|
|
575
1033
|
signUp: authInstance.signUp,
|
|
576
1034
|
signIn: authInstance.signIn,
|
|
@@ -580,6 +1038,7 @@ function createFytkitClient(options) {
|
|
|
580
1038
|
requestPasswordReset: authInstance.requestPasswordReset,
|
|
581
1039
|
confirmPasswordReset: authInstance.confirmPasswordReset,
|
|
582
1040
|
confirmEmail: authInstance.confirmEmail,
|
|
1041
|
+
resendVerification: authInstance.resendVerification,
|
|
583
1042
|
verifyTotp: authInstance.verifyTotp,
|
|
584
1043
|
verifyBackupCode: authInstance.verifyBackupCode,
|
|
585
1044
|
requestEmailOtp: authInstance.requestEmailOtp,
|
|
@@ -591,15 +1050,17 @@ function createFytkitClient(options) {
|
|
|
591
1050
|
regenerateBackupCodes: authInstance.regenerateBackupCodes,
|
|
592
1051
|
changePassword: authInstance.changePassword,
|
|
593
1052
|
changeEmail: authInstance.changeEmail,
|
|
1053
|
+
confirmEmailChange: authInstance.confirmEmailChange,
|
|
594
1054
|
listSessions: authInstance.listSessions,
|
|
595
1055
|
revokeSession: authInstance.revokeSession,
|
|
596
1056
|
revokeOtherSessions: authInstance.revokeOtherSessions,
|
|
597
1057
|
closeAccount: authInstance.closeAccount
|
|
598
1058
|
},
|
|
599
|
-
...data
|
|
1059
|
+
...data,
|
|
1060
|
+
app: createAppNamespace(transport)
|
|
600
1061
|
};
|
|
601
1062
|
}
|
|
602
1063
|
|
|
603
|
-
export { createFytkitClient, createLocalStorageAdapter, createMemoryStorageAdapter };
|
|
1064
|
+
export { MESSAGES_FALLBACK_POLL_MS, createFytkitClient, createLocalStorageAdapter, createMemoryStorageAdapter, refusalData, resolveJoinCode, resolvePaymentMode, subscribeToMessages };
|
|
604
1065
|
//# sourceMappingURL=index.js.map
|
|
605
1066
|
//# sourceMappingURL=index.js.map
|