@zweer/mage-space-client 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +391 -114
- package/dist/index.d.cts +143 -16
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +143 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +387 -115
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -17,12 +17,38 @@ var AuthError = class extends MageSpaceError {
|
|
|
17
17
|
};
|
|
18
18
|
/** Action-hash discovery failures (page/chunk fetch, empty registry). */
|
|
19
19
|
var DiscoveryError = class extends MageSpaceError {
|
|
20
|
+
/**
|
|
21
|
+
* `true` when the action name was absent even after a forced re-discovery
|
|
22
|
+
* (a fallback trigger); `false` for scrape failures (no chunks/no hashes).
|
|
23
|
+
*/
|
|
24
|
+
notFound;
|
|
20
25
|
constructor(message, options) {
|
|
21
26
|
super(message, options);
|
|
22
27
|
this.name = "DiscoveryError";
|
|
28
|
+
this.notFound = options?.notFound ?? false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* A required action is unavailable on the active host (a persistent 404 / HTML
|
|
33
|
+
* fallback that survived one in-place re-discovery). A fallback trigger.
|
|
34
|
+
*/
|
|
35
|
+
var ActionUnavailableError = class extends MageSpaceError {
|
|
36
|
+
action;
|
|
37
|
+
targetKey;
|
|
38
|
+
constructor(action, targetKey, options) {
|
|
39
|
+
super(`Action "${action}" is unavailable on target "${targetKey}"`, options);
|
|
40
|
+
this.action = action;
|
|
41
|
+
this.targetKey = targetKey;
|
|
42
|
+
this.name = "ActionUnavailableError";
|
|
23
43
|
}
|
|
24
44
|
};
|
|
25
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* A Server Action returned 404, indicating a stale (post-deploy) hash.
|
|
47
|
+
*
|
|
48
|
+
* @deprecated Legacy export kept for backward compatibility; no longer thrown.
|
|
49
|
+
* The current signal for a definitively unavailable action is
|
|
50
|
+
* {@link ActionUnavailableError}.
|
|
51
|
+
*/
|
|
26
52
|
var StaleActionError = class extends MageSpaceError {
|
|
27
53
|
action;
|
|
28
54
|
constructor(action, options) {
|
|
@@ -214,7 +240,7 @@ var ActionRegistry = class {
|
|
|
214
240
|
snapshot = await this.refresh();
|
|
215
241
|
hash = snapshot.hashes[action];
|
|
216
242
|
}
|
|
217
|
-
if (hash === void 0) throw new DiscoveryError(`Action "${action}" not found after discovery
|
|
243
|
+
if (hash === void 0) throw new DiscoveryError(`Action "${action}" not found after discovery`, { notFound: true });
|
|
218
244
|
return {
|
|
219
245
|
hash,
|
|
220
246
|
deploymentId: snapshot.deploymentId
|
|
@@ -326,9 +352,11 @@ function resolveFlightValue(value, rows, seen = /* @__PURE__ */ new Set()) {
|
|
|
326
352
|
if (ref) {
|
|
327
353
|
const refId = ref[1];
|
|
328
354
|
if (refId === void 0 || seen.has(refId)) return;
|
|
329
|
-
seen.add(refId);
|
|
330
355
|
const target = rows.get(refId);
|
|
331
|
-
|
|
356
|
+
if (target === void 0) return;
|
|
357
|
+
const nextSeen = new Set(seen);
|
|
358
|
+
nextSeen.add(refId);
|
|
359
|
+
return resolveFlightValue(target, rows, nextSeen);
|
|
332
360
|
}
|
|
333
361
|
if (value.startsWith("$$")) return value.slice(1);
|
|
334
362
|
if (value.startsWith("$D")) return new Date(value.slice(2));
|
|
@@ -614,7 +642,7 @@ const SESSION_DEFAULT_TTL_MS = 864e5;
|
|
|
614
642
|
const ID_TOKEN_DEFAULT_TTL_S = 3600;
|
|
615
643
|
var AuthManager = class {
|
|
616
644
|
refreshToken;
|
|
617
|
-
|
|
645
|
+
getHttp;
|
|
618
646
|
fetchImpl;
|
|
619
647
|
apiKey;
|
|
620
648
|
sessionPath;
|
|
@@ -622,11 +650,19 @@ var AuthManager = class {
|
|
|
622
650
|
session = null;
|
|
623
651
|
constructor(opts) {
|
|
624
652
|
this.refreshToken = opts.refreshToken;
|
|
625
|
-
this.
|
|
653
|
+
this.getHttp = opts.getHttp;
|
|
626
654
|
this.fetchImpl = opts.fetch;
|
|
627
655
|
this.apiKey = opts.firebaseApiKey ?? DEFAULT_FIREBASE_API_KEY;
|
|
628
656
|
this.sessionPath = opts.sessionPath ?? "/creations";
|
|
629
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Drop the cached `__session` cookie so the next {@link getSession} mints a
|
|
660
|
+
* fresh one against the (newly) active host. The Firebase token chain is kept
|
|
661
|
+
* because it is host-independent.
|
|
662
|
+
*/
|
|
663
|
+
resetSession() {
|
|
664
|
+
this.session = null;
|
|
665
|
+
}
|
|
630
666
|
/** Return a valid Firebase ID token, refreshing via the refresh token if needed. */
|
|
631
667
|
async getIdToken() {
|
|
632
668
|
if (this.tokens !== null && Date.now() < this.tokens.expiresAt - EXPIRY_BUFFER_MS) return this.tokens.idToken;
|
|
@@ -641,7 +677,7 @@ var AuthManager = class {
|
|
|
641
677
|
async getSession() {
|
|
642
678
|
if (this.session !== null && Date.now() < this.session.expiresAt - EXPIRY_BUFFER_MS) return this.session.cookie;
|
|
643
679
|
const idToken = await this.getIdToken();
|
|
644
|
-
const cookie = extractSessionCookie((await this.
|
|
680
|
+
const cookie = extractSessionCookie((await this.getHttp().callAction({
|
|
645
681
|
action: "createUserSession",
|
|
646
682
|
path: this.sessionPath,
|
|
647
683
|
args: [idToken]
|
|
@@ -699,9 +735,58 @@ function extractSessionCookie(headers) {
|
|
|
699
735
|
return null;
|
|
700
736
|
}
|
|
701
737
|
//#endregion
|
|
738
|
+
//#region lib/targets.ts
|
|
739
|
+
/** Resolve a logical action to its host-specific wire name (identity if unlisted). */
|
|
740
|
+
function resolveActionName(target, logical) {
|
|
741
|
+
return target.actionNames[logical] ?? logical;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Production target.
|
|
745
|
+
*
|
|
746
|
+
* @remarks
|
|
747
|
+
* Maps each logical action to the wire name the current production deploy
|
|
748
|
+
* actually serves (all VERIFIED against `lib/seed.ts` / the existing MSW tests):
|
|
749
|
+
* `submitGeneration → runArchitecture`, `cancelJob → cancelArchitectureJob`,
|
|
750
|
+
* `listHistory → getHistoryPaginated`, `listCreations → getCreationsPaginatedParallel`,
|
|
751
|
+
* and `getHistoryById → getHistoryById` (identity). The table is explicit rather
|
|
752
|
+
* than empty so production behaviour is reproduced exactly regardless of the
|
|
753
|
+
* logical-name vocabulary.
|
|
754
|
+
*/
|
|
755
|
+
const PRODUCTION_TARGET = {
|
|
756
|
+
key: "production",
|
|
757
|
+
baseUrl: "https://www.mage.space",
|
|
758
|
+
actionNames: {
|
|
759
|
+
submitGeneration: "runArchitecture",
|
|
760
|
+
getHistoryById: "getHistoryById",
|
|
761
|
+
cancelJob: "cancelArchitectureJob",
|
|
762
|
+
listHistory: "getHistoryPaginated",
|
|
763
|
+
listCreations: "getCreationsPaginatedParallel"
|
|
764
|
+
},
|
|
765
|
+
pollStrategy: "byId"
|
|
766
|
+
};
|
|
767
|
+
/**
|
|
768
|
+
* Beta target. Beta facts are assumptions pending a reverse-engineering capture
|
|
769
|
+
* under `docs/api/beta` — treat them as unverified until that doc lands.
|
|
770
|
+
*/
|
|
771
|
+
const BETA_TARGET = {
|
|
772
|
+
key: "beta",
|
|
773
|
+
baseUrl: "https://beta.mage.space",
|
|
774
|
+
actionNames: {
|
|
775
|
+
submitGeneration: "runArchitecture",
|
|
776
|
+
cancelJob: "cancelArchitectureJob",
|
|
777
|
+
listHistory: "getHistoryPaginated",
|
|
778
|
+
listCreations: "getCreationsPaginated"
|
|
779
|
+
},
|
|
780
|
+
pollStrategy: "paginated"
|
|
781
|
+
};
|
|
782
|
+
/** Default ordered fallback chain: production first, then beta. */
|
|
783
|
+
const DEFAULT_TARGET_CHAIN = [PRODUCTION_TARGET, BETA_TARGET];
|
|
784
|
+
//#endregion
|
|
702
785
|
//#region lib/generation.ts
|
|
703
786
|
/** Generation actions are served from the `/explore` page. */
|
|
704
787
|
const GENERATION_PATH = "/explore";
|
|
788
|
+
/** Mirrors HistoryService.HISTORY_PATH ('/creations'); kept local to avoid coupling the two services. */
|
|
789
|
+
const HISTORY_PATH$1 = "/creations";
|
|
705
790
|
/** RSC sentinel for an absent value. */
|
|
706
791
|
const UNDEFINED$1 = "$undefined";
|
|
707
792
|
/** Default concept id for mango-v3-pro (from docs — verify per model via rev-eng). */
|
|
@@ -713,9 +798,14 @@ const DEFAULT_TIMEOUT_MS = 3e5;
|
|
|
713
798
|
var GenerationService = class {
|
|
714
799
|
http;
|
|
715
800
|
auth;
|
|
716
|
-
|
|
801
|
+
target;
|
|
802
|
+
/** Single-status-read primitive; overridable so the client can make it fallback-aware. */
|
|
803
|
+
pollOnce;
|
|
804
|
+
constructor(http, auth, target, pollOnce) {
|
|
717
805
|
this.http = http;
|
|
718
806
|
this.auth = auth;
|
|
807
|
+
this.target = target;
|
|
808
|
+
this.pollOnce = pollOnce ?? ((id) => this.getHistory(id));
|
|
719
809
|
}
|
|
720
810
|
/** Submit an image generation job and return its `historyId`. */
|
|
721
811
|
async submit(opts) {
|
|
@@ -767,7 +857,7 @@ var GenerationService = class {
|
|
|
767
857
|
generationMode
|
|
768
858
|
}];
|
|
769
859
|
const { data } = await this.http.callAction({
|
|
770
|
-
action: "
|
|
860
|
+
action: resolveActionName(this.target, "submitGeneration"),
|
|
771
861
|
path: GENERATION_PATH,
|
|
772
862
|
args,
|
|
773
863
|
session
|
|
@@ -780,22 +870,68 @@ var GenerationService = class {
|
|
|
780
870
|
if (typeof historyId !== "string") throw new GenerationError("runArchitecture did not return a history_id");
|
|
781
871
|
return { historyId };
|
|
782
872
|
}
|
|
783
|
-
/** Fetch a single history record by id. */
|
|
873
|
+
/** Fetch a single history record by id, dispatching on the target's poll strategy. */
|
|
784
874
|
async getHistory(historyId) {
|
|
875
|
+
if (this.target.pollStrategy === "paginated") return this.getHistoryPaginated(historyId);
|
|
785
876
|
const session = await this.auth.getSession();
|
|
786
877
|
const { data } = await this.http.callAction({
|
|
787
|
-
action: "getHistoryById",
|
|
878
|
+
action: resolveActionName(this.target, "getHistoryById"),
|
|
788
879
|
path: GENERATION_PATH,
|
|
789
880
|
args: [historyId],
|
|
790
881
|
session
|
|
791
882
|
});
|
|
792
883
|
return data;
|
|
793
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Beta status read: `getHistoryById` is absent, so page `getHistoryPaginated`
|
|
887
|
+
* and find the item whose `id === historyId`.
|
|
888
|
+
*
|
|
889
|
+
* @remarks
|
|
890
|
+
* ASSUMED (pending docs/api/beta): each page window is swept in two phases —
|
|
891
|
+
* `running` first, then the raw `$undefined` sentinel (terminal items) — since
|
|
892
|
+
* which filter surfaces a running job on beta is not yet documented. Bounded by
|
|
893
|
+
* {@link PAGINATED_POLL_MAX_PAGES} pages of {@link PAGINATED_POLL_PAGE_SIZE}.
|
|
894
|
+
*/
|
|
895
|
+
async getHistoryPaginated(historyId) {
|
|
896
|
+
const uid = await this.auth.getUserId();
|
|
897
|
+
const limit = 20;
|
|
898
|
+
let offset = 0;
|
|
899
|
+
for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
|
|
900
|
+
for (const status of ["running", UNDEFINED$1]) {
|
|
901
|
+
const page = await this.fetchHistoryPage(uid, status, limit, offset);
|
|
902
|
+
const found = page.histories.find((h) => h.id === historyId);
|
|
903
|
+
if (found !== void 0) return found;
|
|
904
|
+
if (status === UNDEFINED$1 && !page.hasMore) throw new GenerationError(`history ${historyId} not found on beta`);
|
|
905
|
+
}
|
|
906
|
+
offset += limit;
|
|
907
|
+
}
|
|
908
|
+
throw new GenerationError(`history ${historyId} not found on beta`);
|
|
909
|
+
}
|
|
910
|
+
/** Fetch one page of history via `listHistory` (resolved per target) on `/creations`. */
|
|
911
|
+
async fetchHistoryPage(uid, status, limit, offset) {
|
|
912
|
+
const session = await this.auth.getSession();
|
|
913
|
+
const filters = {
|
|
914
|
+
status,
|
|
915
|
+
type: UNDEFINED$1
|
|
916
|
+
};
|
|
917
|
+
const { data } = await this.http.callAction({
|
|
918
|
+
action: resolveActionName(this.target, "listHistory"),
|
|
919
|
+
path: HISTORY_PATH$1,
|
|
920
|
+
args: [
|
|
921
|
+
uid,
|
|
922
|
+
limit,
|
|
923
|
+
offset,
|
|
924
|
+
filters
|
|
925
|
+
],
|
|
926
|
+
session
|
|
927
|
+
});
|
|
928
|
+
return unwrapResult(data);
|
|
929
|
+
}
|
|
794
930
|
/** Cancel a running generation job. */
|
|
795
931
|
async cancelJob(historyId) {
|
|
796
932
|
const session = await this.auth.getSession();
|
|
797
933
|
await this.http.callAction({
|
|
798
|
-
action: "
|
|
934
|
+
action: resolveActionName(this.target, "cancelJob"),
|
|
799
935
|
path: GENERATION_PATH,
|
|
800
936
|
args: [historyId],
|
|
801
937
|
session
|
|
@@ -808,7 +944,7 @@ var GenerationService = class {
|
|
|
808
944
|
const deadline = Date.now() + timeoutMs;
|
|
809
945
|
for (;;) {
|
|
810
946
|
if (opts.signal?.aborted === true) throw new TimeoutError("waitForResult was aborted");
|
|
811
|
-
const item = await this.
|
|
947
|
+
const item = await this.pollOnce(historyId);
|
|
812
948
|
if (item.status === "success") return item;
|
|
813
949
|
if (item.status === "error") throw new GenerationError(`Generation failed: ${JSON.stringify(item.error ?? null)}`);
|
|
814
950
|
if (Date.now() > deadline) throw new TimeoutError(`Generation timed out after ${timeoutMs}ms (last status: ${item.status})`);
|
|
@@ -854,9 +990,11 @@ const UNDEFINED = "$undefined";
|
|
|
854
990
|
var HistoryService = class {
|
|
855
991
|
http;
|
|
856
992
|
auth;
|
|
857
|
-
|
|
993
|
+
target;
|
|
994
|
+
constructor(http, auth, target) {
|
|
858
995
|
this.http = http;
|
|
859
996
|
this.auth = auth;
|
|
997
|
+
this.target = target;
|
|
860
998
|
}
|
|
861
999
|
/** List generation history for the authenticated user. */
|
|
862
1000
|
async list(opts = {}) {
|
|
@@ -868,7 +1006,7 @@ var HistoryService = class {
|
|
|
868
1006
|
type: UNDEFINED
|
|
869
1007
|
};
|
|
870
1008
|
const { data } = await this.http.callAction({
|
|
871
|
-
action: "
|
|
1009
|
+
action: resolveActionName(this.target, "listHistory"),
|
|
872
1010
|
path: HISTORY_PATH,
|
|
873
1011
|
args: [
|
|
874
1012
|
uid,
|
|
@@ -886,7 +1024,7 @@ var HistoryService = class {
|
|
|
886
1024
|
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
887
1025
|
const offset = opts.offset ?? 0;
|
|
888
1026
|
const { data } = await this.http.callAction({
|
|
889
|
-
action: "
|
|
1027
|
+
action: resolveActionName(this.target, "listCreations"),
|
|
890
1028
|
path: HISTORY_PATH,
|
|
891
1029
|
args: [
|
|
892
1030
|
uid,
|
|
@@ -971,74 +1109,6 @@ var HistoryService = class {
|
|
|
971
1109
|
}
|
|
972
1110
|
};
|
|
973
1111
|
//#endregion
|
|
974
|
-
//#region lib/http.ts
|
|
975
|
-
const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
|
|
976
|
-
/**
|
|
977
|
-
* Known RSC error digests mapped to human-readable messages.
|
|
978
|
-
* Discovered via reverse engineering; the server does not expose error details.
|
|
979
|
-
*/
|
|
980
|
-
const KNOWN_DIGESTS = {
|
|
981
|
-
"709776019": "Username already taken (duplicate character/reference username)",
|
|
982
|
-
"1853426377": "Invalid username (must be 1–15 lowercase chars, start with a letter, only a-z 0-9 _ -)"
|
|
983
|
-
};
|
|
984
|
-
var HttpClient = class {
|
|
985
|
-
registry;
|
|
986
|
-
fetchImpl;
|
|
987
|
-
baseUrl;
|
|
988
|
-
userAgent;
|
|
989
|
-
constructor(registry, fetchImpl, baseUrl, userAgent = DEFAULT_USER_AGENT) {
|
|
990
|
-
this.registry = registry;
|
|
991
|
-
this.fetchImpl = fetchImpl;
|
|
992
|
-
this.baseUrl = baseUrl;
|
|
993
|
-
this.userAgent = userAgent;
|
|
994
|
-
}
|
|
995
|
-
async callAction(params, retried = false) {
|
|
996
|
-
const { hash, deploymentId } = await this.registry.getHash(params.action);
|
|
997
|
-
const headers = {
|
|
998
|
-
accept: "text/x-component",
|
|
999
|
-
"content-type": "text/plain;charset=UTF-8",
|
|
1000
|
-
"next-action": hash,
|
|
1001
|
-
"x-deployment-id": deploymentId,
|
|
1002
|
-
origin: this.baseUrl,
|
|
1003
|
-
referer: `${this.baseUrl}${params.path}`,
|
|
1004
|
-
"user-agent": this.userAgent
|
|
1005
|
-
};
|
|
1006
|
-
if (params.session !== void 0) headers.cookie = `__session=${params.session}`;
|
|
1007
|
-
const res = await this.fetchImpl(`${this.baseUrl}${params.path}`, {
|
|
1008
|
-
method: "POST",
|
|
1009
|
-
headers,
|
|
1010
|
-
body: JSON.stringify(params.args)
|
|
1011
|
-
});
|
|
1012
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
1013
|
-
const isHtmlFallback = res.ok && contentType.includes("text/html");
|
|
1014
|
-
if ((res.status === 404 || isHtmlFallback) && !retried) {
|
|
1015
|
-
await safeText(res);
|
|
1016
|
-
await this.registry.invalidate();
|
|
1017
|
-
return this.callAction(params, true);
|
|
1018
|
-
}
|
|
1019
|
-
if (!res.ok) {
|
|
1020
|
-
const body = await safeText(res);
|
|
1021
|
-
const digest = body.match(/E\{"digest":"([^"]+)"\}/)?.[1];
|
|
1022
|
-
const knownDigest = digest !== void 0 ? KNOWN_DIGESTS[digest] : void 0;
|
|
1023
|
-
const hint = knownDigest ? ` — ${knownDigest}` : digest ? ` (server digest: ${digest})` : "";
|
|
1024
|
-
const raw = body.length > 0 ? ` — ${body.slice(0, 200)}` : "";
|
|
1025
|
-
throw new MageSpaceError(`Action "${params.action}" failed: HTTP ${res.status}${hint}${raw}`);
|
|
1026
|
-
}
|
|
1027
|
-
return {
|
|
1028
|
-
data: parseServerActionResponse(await res.text()),
|
|
1029
|
-
headers: res.headers,
|
|
1030
|
-
deploymentId: res.headers.get("x-deployment-id") ?? deploymentId
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
async function safeText(res) {
|
|
1035
|
-
try {
|
|
1036
|
-
return await res.text();
|
|
1037
|
-
} catch {
|
|
1038
|
-
return "";
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
//#endregion
|
|
1042
1112
|
//#region lib/references.ts
|
|
1043
1113
|
/** Server-enforced username constraints (shared with characters, module 64428). */
|
|
1044
1114
|
const USERNAME_MAX = 15;
|
|
@@ -1279,105 +1349,307 @@ const SEED_SNAPSHOT = {
|
|
|
1279
1349
|
}
|
|
1280
1350
|
};
|
|
1281
1351
|
//#endregion
|
|
1352
|
+
//#region lib/http.ts
|
|
1353
|
+
const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
|
|
1354
|
+
/**
|
|
1355
|
+
* HTTP statuses that mean "this action is unavailable on this host". A stale
|
|
1356
|
+
* (post-deploy) action hash surfaces as a 404 on the production host and as a
|
|
1357
|
+
* 409 ("Server Action unavailable") on the beta host — both mean the same
|
|
1358
|
+
* thing: invalidate the registry, re-discover, and (if still unavailable) fall
|
|
1359
|
+
* back to the next host.
|
|
1360
|
+
*/
|
|
1361
|
+
const UNAVAILABLE_STATUSES = /* @__PURE__ */ new Set([404, 409]);
|
|
1362
|
+
/**
|
|
1363
|
+
* Known RSC error digests mapped to human-readable messages.
|
|
1364
|
+
* Discovered via reverse engineering; the server does not expose error details.
|
|
1365
|
+
*/
|
|
1366
|
+
const KNOWN_DIGESTS = {
|
|
1367
|
+
"709776019": "Username already taken (duplicate character/reference username)",
|
|
1368
|
+
"1853426377": "Invalid username (must be 1–15 lowercase chars, start with a letter, only a-z 0-9 _ -)"
|
|
1369
|
+
};
|
|
1370
|
+
var HttpClient = class {
|
|
1371
|
+
registry;
|
|
1372
|
+
fetchImpl;
|
|
1373
|
+
baseUrl;
|
|
1374
|
+
targetKey;
|
|
1375
|
+
userAgent;
|
|
1376
|
+
constructor(registry, fetchImpl, baseUrl, targetKey, userAgent = DEFAULT_USER_AGENT) {
|
|
1377
|
+
this.registry = registry;
|
|
1378
|
+
this.fetchImpl = fetchImpl;
|
|
1379
|
+
this.baseUrl = baseUrl;
|
|
1380
|
+
this.targetKey = targetKey;
|
|
1381
|
+
this.userAgent = userAgent;
|
|
1382
|
+
}
|
|
1383
|
+
/** A response that means the action is unavailable (stale hash or gone). */
|
|
1384
|
+
isUnavailable(res) {
|
|
1385
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
1386
|
+
const isHtmlFallback = res.ok && contentType.includes("text/html");
|
|
1387
|
+
return UNAVAILABLE_STATUSES.has(res.status) || isHtmlFallback;
|
|
1388
|
+
}
|
|
1389
|
+
async callAction(params, retried = false) {
|
|
1390
|
+
const { hash, deploymentId } = await this.registry.getHash(params.action);
|
|
1391
|
+
const headers = {
|
|
1392
|
+
accept: "text/x-component",
|
|
1393
|
+
"content-type": "text/plain;charset=UTF-8",
|
|
1394
|
+
"next-action": hash,
|
|
1395
|
+
"x-deployment-id": deploymentId,
|
|
1396
|
+
origin: this.baseUrl,
|
|
1397
|
+
referer: `${this.baseUrl}${params.path}`,
|
|
1398
|
+
"user-agent": this.userAgent
|
|
1399
|
+
};
|
|
1400
|
+
if (params.session !== void 0) headers.cookie = `__session=${params.session}`;
|
|
1401
|
+
const res = await this.fetchImpl(`${this.baseUrl}${params.path}`, {
|
|
1402
|
+
method: "POST",
|
|
1403
|
+
headers,
|
|
1404
|
+
body: JSON.stringify(params.args)
|
|
1405
|
+
});
|
|
1406
|
+
if (this.isUnavailable(res) && !retried) {
|
|
1407
|
+
await safeText(res);
|
|
1408
|
+
await this.registry.invalidate();
|
|
1409
|
+
return this.callAction(params, true);
|
|
1410
|
+
}
|
|
1411
|
+
if (this.isUnavailable(res)) {
|
|
1412
|
+
await safeText(res);
|
|
1413
|
+
throw new ActionUnavailableError(params.action, this.targetKey);
|
|
1414
|
+
}
|
|
1415
|
+
if (!res.ok) {
|
|
1416
|
+
const body = await safeText(res);
|
|
1417
|
+
const digest = body.match(/E\{"digest":"([^"]+)"\}/)?.[1];
|
|
1418
|
+
const knownDigest = digest !== void 0 ? KNOWN_DIGESTS[digest] : void 0;
|
|
1419
|
+
const hint = knownDigest ? ` — ${knownDigest}` : digest ? ` (server digest: ${digest})` : "";
|
|
1420
|
+
const raw = body.length > 0 ? ` — ${body.slice(0, 200)}` : "";
|
|
1421
|
+
throw new MageSpaceError(`Action "${params.action}" failed: HTTP ${res.status}${hint}${raw}`);
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
data: parseServerActionResponse(await res.text()),
|
|
1425
|
+
headers: res.headers,
|
|
1426
|
+
deploymentId: res.headers.get("x-deployment-id") ?? deploymentId
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
async function safeText(res) {
|
|
1431
|
+
try {
|
|
1432
|
+
return await res.text();
|
|
1433
|
+
} catch {
|
|
1434
|
+
return "";
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
//#endregion
|
|
1438
|
+
//#region lib/target-context.ts
|
|
1439
|
+
/**
|
|
1440
|
+
* A {@link TargetContext} bundles one host's {@link ActionRegistry} and
|
|
1441
|
+
* {@link HttpClient} so the client can hold one pair per target and switch by
|
|
1442
|
+
* selecting the other pair (rather than mutating a shared `baseUrl`).
|
|
1443
|
+
*/
|
|
1444
|
+
/**
|
|
1445
|
+
* Build the registry + HTTP client for a single target.
|
|
1446
|
+
*
|
|
1447
|
+
* @remarks
|
|
1448
|
+
* The caller's `cache` and the production `seed` are production-only concerns:
|
|
1449
|
+
* the bundled seed holds production hashes and the single-slot cache cannot be
|
|
1450
|
+
* namespaced per host. Non-production targets always discover fresh with their
|
|
1451
|
+
* own {@link InMemoryActionCache} and no seed.
|
|
1452
|
+
*/
|
|
1453
|
+
function createTargetContext(target, deps) {
|
|
1454
|
+
const isProduction = target.key === "production";
|
|
1455
|
+
const registry = new ActionRegistry({
|
|
1456
|
+
fetch: deps.fetch,
|
|
1457
|
+
baseUrl: target.baseUrl,
|
|
1458
|
+
cache: isProduction ? deps.cache : new InMemoryActionCache(),
|
|
1459
|
+
seed: isProduction ? deps.seed : void 0
|
|
1460
|
+
});
|
|
1461
|
+
return {
|
|
1462
|
+
target,
|
|
1463
|
+
registry,
|
|
1464
|
+
http: new HttpClient(registry, deps.fetch, target.baseUrl, target.key, deps.userAgent)
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
//#endregion
|
|
1282
1468
|
//#region lib/client.ts
|
|
1283
1469
|
/**
|
|
1284
1470
|
* {@link MageSpaceClient} — the public facade that wires together action
|
|
1285
1471
|
* discovery, authentication, generation, and history.
|
|
1472
|
+
*
|
|
1473
|
+
* The client models each mage.space host as a {@link Target} and holds an
|
|
1474
|
+
* ordered chain (production first, then beta). When the active host can no
|
|
1475
|
+
* longer serve a required action, the client advances to the next target and
|
|
1476
|
+
* retries once. The switch is sticky for the lifetime of the instance.
|
|
1286
1477
|
*/
|
|
1287
|
-
const DEFAULT_BASE_URL = "https://www.mage.space";
|
|
1288
1478
|
var MageSpaceClient = class {
|
|
1289
|
-
registry;
|
|
1290
1479
|
auth;
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1480
|
+
chain;
|
|
1481
|
+
auto;
|
|
1482
|
+
contextDeps;
|
|
1483
|
+
contexts = /* @__PURE__ */ new Map();
|
|
1484
|
+
activeIndex;
|
|
1485
|
+
activeContext;
|
|
1486
|
+
activeBundle;
|
|
1487
|
+
/**
|
|
1488
|
+
* Character management (search, CRUD, pagination, uploads).
|
|
1489
|
+
*
|
|
1490
|
+
* @remarks
|
|
1491
|
+
* Bound to the initial host (production by default) and does not follow an
|
|
1492
|
+
* automatic beta fallback. Construct a client with `fallback: { start: 'beta' }`
|
|
1493
|
+
* to use these against beta.
|
|
1494
|
+
*/
|
|
1294
1495
|
characters;
|
|
1295
|
-
/**
|
|
1496
|
+
/**
|
|
1497
|
+
* Reference asset management (upload, create, update, list, feed).
|
|
1498
|
+
*
|
|
1499
|
+
* @remarks
|
|
1500
|
+
* Bound to the initial host (production by default) and does not follow an
|
|
1501
|
+
* automatic beta fallback. Construct a client with `fallback: { start: 'beta' }`
|
|
1502
|
+
* to use these against beta.
|
|
1503
|
+
*/
|
|
1296
1504
|
references;
|
|
1297
1505
|
constructor(options) {
|
|
1298
1506
|
if (!options.refreshToken) throw new Error("MageSpaceClient requires a refreshToken");
|
|
1299
|
-
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
1300
1507
|
const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1301
|
-
|
|
1508
|
+
const fallback = options.fallback;
|
|
1509
|
+
const baseUrlPinned = typeof options.baseUrl === "string" && options.baseUrl.length > 0;
|
|
1510
|
+
if (baseUrlPinned && fallback !== void 0 && (fallback.chain !== void 0 || fallback.start !== void 0 || fallback.auto !== void 0)) throw new Error("baseUrl pins a single host and cannot be combined with fallback options");
|
|
1511
|
+
if (baseUrlPinned) {
|
|
1512
|
+
this.chain = [{
|
|
1513
|
+
key: "production",
|
|
1514
|
+
baseUrl: options.baseUrl,
|
|
1515
|
+
actionNames: {},
|
|
1516
|
+
pollStrategy: "byId"
|
|
1517
|
+
}];
|
|
1518
|
+
this.auto = false;
|
|
1519
|
+
} else {
|
|
1520
|
+
const chain = fallback?.chain ?? DEFAULT_TARGET_CHAIN;
|
|
1521
|
+
if (chain.length === 0) throw new Error("fallback.chain must be a non-empty array");
|
|
1522
|
+
this.chain = chain;
|
|
1523
|
+
this.auto = fallback?.auto ?? true;
|
|
1524
|
+
}
|
|
1525
|
+
const startKey = baseUrlPinned ? "production" : fallback?.start ?? "production";
|
|
1526
|
+
const startIndex = this.chain.findIndex((t) => t.key === startKey);
|
|
1527
|
+
if (startIndex === -1) throw new Error(`fallback.start '${startKey}' is not in the target chain`);
|
|
1528
|
+
this.contextDeps = {
|
|
1302
1529
|
fetch: fetchImpl,
|
|
1303
|
-
|
|
1530
|
+
userAgent: options.userAgent,
|
|
1304
1531
|
cache: options.cache,
|
|
1305
1532
|
seed: options.seed ?? SEED_SNAPSHOT
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1533
|
+
};
|
|
1534
|
+
this.activeIndex = startIndex;
|
|
1535
|
+
this.activeContext = this.getContext(startIndex);
|
|
1308
1536
|
this.auth = new AuthManager({
|
|
1309
1537
|
refreshToken: options.refreshToken,
|
|
1310
|
-
http,
|
|
1538
|
+
getHttp: () => this.activeContext.http,
|
|
1311
1539
|
fetch: fetchImpl,
|
|
1312
1540
|
firebaseApiKey: options.firebaseApiKey
|
|
1313
1541
|
});
|
|
1314
|
-
this.
|
|
1315
|
-
this.
|
|
1316
|
-
this.
|
|
1317
|
-
|
|
1542
|
+
this.activeBundle = this.buildBundle(this.activeContext);
|
|
1543
|
+
this.characters = new CharactersService(this.activeContext.http, this.auth);
|
|
1544
|
+
this.references = new ReferencesService(this.activeContext.http, this.auth);
|
|
1545
|
+
}
|
|
1546
|
+
/** Lazily build (and memoise) the {@link TargetContext} for a chain index. */
|
|
1547
|
+
getContext(index) {
|
|
1548
|
+
const target = this.chain[index];
|
|
1549
|
+
const existing = this.contexts.get(target.key);
|
|
1550
|
+
if (existing !== void 0) return existing;
|
|
1551
|
+
const context = createTargetContext(target, this.contextDeps);
|
|
1552
|
+
this.contexts.set(target.key, context);
|
|
1553
|
+
return context;
|
|
1554
|
+
}
|
|
1555
|
+
/** Build the generation/history service bundle for a context. */
|
|
1556
|
+
buildBundle(context) {
|
|
1557
|
+
const pollOnce = (id) => this.withFallback((bundle) => bundle.generationService.getHistory(id));
|
|
1558
|
+
return {
|
|
1559
|
+
target: context.target,
|
|
1560
|
+
generationService: new GenerationService(context.http, this.auth, context.target, pollOnce),
|
|
1561
|
+
historyService: new HistoryService(context.http, this.auth, context.target)
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
/** Advance to the next target (sticky). Returns `null` if none or `auto` is off. */
|
|
1565
|
+
advanceTarget() {
|
|
1566
|
+
if (this.auto === false || this.activeIndex >= this.chain.length - 1) return null;
|
|
1567
|
+
this.activeIndex += 1;
|
|
1568
|
+
this.activeContext = this.getContext(this.activeIndex);
|
|
1569
|
+
this.activeBundle = this.buildBundle(this.activeContext);
|
|
1570
|
+
return this.activeBundle;
|
|
1571
|
+
}
|
|
1572
|
+
/** Whether an error is a definitive action-unavailable signal (a fallback trigger). */
|
|
1573
|
+
shouldFallback(err) {
|
|
1574
|
+
return err instanceof ActionUnavailableError || err instanceof DiscoveryError && err.notFound === true;
|
|
1575
|
+
}
|
|
1576
|
+
/** Run `op` against the active bundle, switching to the next target once on a trigger. */
|
|
1577
|
+
async withFallback(op) {
|
|
1578
|
+
try {
|
|
1579
|
+
return await op(this.activeBundle);
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
if (!this.shouldFallback(err)) throw err;
|
|
1582
|
+
if (this.advanceTarget() === null) throw err;
|
|
1583
|
+
this.auth.resetSession();
|
|
1584
|
+
return op(this.activeBundle);
|
|
1585
|
+
}
|
|
1318
1586
|
}
|
|
1319
1587
|
/** Submit a generation job; returns its `historyId`. */
|
|
1320
1588
|
generate(options) {
|
|
1321
|
-
return this.generationService.submit(options);
|
|
1589
|
+
return this.withFallback((b) => b.generationService.submit(options));
|
|
1322
1590
|
}
|
|
1323
1591
|
/** Submit a video generation job (Berry-2 family); returns its `historyId`. */
|
|
1324
1592
|
generateVideo(options) {
|
|
1325
|
-
return this.generationService.submitVideo(options);
|
|
1593
|
+
return this.withFallback((b) => b.generationService.submitVideo(options));
|
|
1326
1594
|
}
|
|
1327
1595
|
/** Fetch the current state of a generation by id. */
|
|
1328
1596
|
getResult(historyId) {
|
|
1329
|
-
return this.generationService.getHistory(historyId);
|
|
1597
|
+
return this.withFallback((b) => b.generationService.getHistory(historyId));
|
|
1330
1598
|
}
|
|
1331
1599
|
/** Poll until a generation finishes (or fails / times out). */
|
|
1332
1600
|
waitForResult(historyId, options) {
|
|
1333
|
-
return this.generationService.waitForResult(historyId, options);
|
|
1601
|
+
return this.withFallback((b) => b.generationService.waitForResult(historyId, options));
|
|
1334
1602
|
}
|
|
1335
1603
|
/** Cancel a running generation job. */
|
|
1336
1604
|
cancelJob(historyId) {
|
|
1337
|
-
return this.generationService.cancelJob(historyId);
|
|
1605
|
+
return this.withFallback((b) => b.generationService.cancelJob(historyId));
|
|
1338
1606
|
}
|
|
1339
1607
|
/** List past generations for the authenticated user. */
|
|
1340
1608
|
listHistory(options) {
|
|
1341
|
-
return this.historyService.list(options);
|
|
1609
|
+
return this.withFallback((b) => b.historyService.list(options));
|
|
1342
1610
|
}
|
|
1343
1611
|
/** List the user's permanently saved creations. */
|
|
1344
1612
|
listCreations(options) {
|
|
1345
|
-
return this.historyService.listCreations(options);
|
|
1613
|
+
return this.withFallback((b) => b.historyService.listCreations(options));
|
|
1346
1614
|
}
|
|
1347
1615
|
/** Save a completed generation result as a permanent creation. */
|
|
1348
1616
|
saveCreation(result) {
|
|
1349
|
-
return this.historyService.saveCreation(result);
|
|
1617
|
+
return this.withFallback((b) => b.historyService.saveCreation(result));
|
|
1350
1618
|
}
|
|
1351
1619
|
/** Permanently delete a history item. Irreversible — requires `{ confirm: true }`. */
|
|
1352
1620
|
deleteHistory(historyId, options) {
|
|
1353
|
-
return this.historyService.deleteHistory(historyId, options);
|
|
1621
|
+
return this.withFallback((b) => b.historyService.deleteHistory(historyId, options));
|
|
1354
1622
|
}
|
|
1355
1623
|
/** Permanently delete many history items. Irreversible — requires `{ confirm: true }`. */
|
|
1356
1624
|
deleteManyHistories(historyIds, options) {
|
|
1357
|
-
return this.historyService.deleteManyHistories(historyIds, options);
|
|
1625
|
+
return this.withFallback((b) => b.historyService.deleteManyHistories(historyIds, options));
|
|
1358
1626
|
}
|
|
1359
1627
|
/** Permanently delete a saved creation. Irreversible — requires `{ confirm: true }`. */
|
|
1360
1628
|
deleteCreation(creationId, options) {
|
|
1361
|
-
return this.historyService.deleteCreation(creationId, options);
|
|
1629
|
+
return this.withFallback((b) => b.historyService.deleteCreation(creationId, options));
|
|
1362
1630
|
}
|
|
1363
1631
|
/** Permanently delete many creations. Irreversible — requires `{ confirm: true }`. */
|
|
1364
1632
|
deleteManyCreations(creationIds, options) {
|
|
1365
|
-
return this.historyService.deleteManyCreations(creationIds, options);
|
|
1633
|
+
return this.withFallback((b) => b.historyService.deleteManyCreations(creationIds, options));
|
|
1366
1634
|
}
|
|
1367
|
-
/** Force re-discovery of the Server Action hashes. */
|
|
1635
|
+
/** Force re-discovery of the Server Action hashes on the active host. */
|
|
1368
1636
|
refreshActions() {
|
|
1369
|
-
return this.registry.refresh();
|
|
1637
|
+
return this.activeContext.registry.refresh();
|
|
1370
1638
|
}
|
|
1371
1639
|
/** Return the authenticated user's Firebase uid. */
|
|
1372
1640
|
getUid() {
|
|
1373
1641
|
return this.auth.getUserId();
|
|
1374
1642
|
}
|
|
1643
|
+
/** Return the key of the currently active target (`'production'` or `'beta'`). */
|
|
1644
|
+
getActiveTarget() {
|
|
1645
|
+
return this.chain[this.activeIndex].key;
|
|
1646
|
+
}
|
|
1375
1647
|
};
|
|
1376
1648
|
//#endregion
|
|
1377
1649
|
//#region lib/index.ts
|
|
1378
1650
|
/** Public entry point for `@zweer/mage-space-client`. */
|
|
1379
1651
|
const VERSION = "0.0.0";
|
|
1380
1652
|
//#endregion
|
|
1381
|
-
export { ActionRegistry, AuthError, CharactersService, DiscoveryError, GenerationError, InMemoryActionCache, MageSpaceClient, MageSpaceError, ReferencesService, RscParseError, SEED_SNAPSHOT, StaleActionError, TimeoutError, VERSION, discoverActions, parseFlightRows, parseServerActionResponse, resolveFlightValue, unwrapResult };
|
|
1653
|
+
export { ActionRegistry, ActionUnavailableError, AuthError, BETA_TARGET, CharactersService, DEFAULT_TARGET_CHAIN, DiscoveryError, GenerationError, InMemoryActionCache, MageSpaceClient, MageSpaceError, PRODUCTION_TARGET, ReferencesService, RscParseError, SEED_SNAPSHOT, StaleActionError, TimeoutError, VERSION, discoverActions, parseFlightRows, parseServerActionResponse, resolveActionName, resolveFlightValue, unwrapResult };
|
|
1382
1654
|
|
|
1383
1655
|
//# sourceMappingURL=index.mjs.map
|