rastack 0.0.62 → 0.0.64
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 +14 -0
- package/admin/AdminApp.tsx +56 -3
- package/admin/index.tsx +8 -2
- package/admin/ui/icons.tsx +9 -0
- package/admin/ui/styles.ts +3 -0
- package/admin/useAdmin.ts +41 -4
- package/auth/provider.ts +71 -8
- package/dist/admin.js +17 -14
- package/dist/compile/analyze.js +11 -1
- package/dist/define/index.d.ts +8 -1
- package/dist/define/index.js +2 -2
- package/dist/define/manifest.js +7 -1
- package/dist/deploy/ci.d.ts +10 -3
- package/dist/deploy/ci.js +22 -11
- package/dist/deploy/config.d.ts +155 -5
- package/dist/deploy/config.js +216 -6
- package/dist/deploy/io.d.ts +57 -0
- package/dist/deploy/io.js +141 -0
- package/dist/deploy/workflows.d.ts +30 -0
- package/dist/deploy/workflows.js +117 -12
- package/dist/rastack-bootstrap.js +18 -47
- package/dist/rastack-ci.js +19 -1
- package/dist/rastack-dev.js +48 -2
- package/dist/rastack-init.d.ts +6 -3
- package/dist/rastack-init.js +231 -92
- package/dist/rastack.js +1 -0
- package/dist/templates/cloudformation/app.yaml +133 -2
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/provider/provider.tsx +23 -0
- package/provider/types.ts +9 -0
- package/src/compile/analyze.ts +11 -1
- package/src/define/index.ts +5 -1
- package/src/define/manifest.ts +7 -1
- package/src/deploy/ci.ts +50 -16
- package/src/deploy/config.ts +340 -7
- package/src/deploy/io.ts +179 -2
- package/src/deploy/workflows.ts +122 -12
- package/src/rastack-bootstrap.ts +24 -69
- package/src/rastack-ci.ts +20 -2
- package/src/rastack-dev.ts +61 -6
- package/src/rastack-init.ts +283 -115
- package/src/rastack.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.0.64](https://github.com/theserverkid/reactapistack/compare/v0.0.63...v0.0.64) (2026-07-20)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **define,provider:** nullable FK relations + bundled-warehouse seed ([276f8c6](https://github.com/theserverkid/reactapistack/commit/276f8c6d6555a6dbcb8965926009f0ddd076ad96))
|
|
11
|
+
|
|
12
|
+
### [0.0.63](https://github.com/theserverkid/reactapistack/compare/v0.0.62...v0.0.63) (2026-07-20)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **deploy:** dev + prod environments with per-env Cognito, proxy, and .env ([996e726](https://github.com/theserverkid/reactapistack/commit/996e726e7e0d55f01e3111579594c2b66e88a79c))
|
|
18
|
+
|
|
5
19
|
### [0.0.62](https://github.com/theserverkid/reactapistack/compare/v0.0.61...v0.0.62) (2026-07-19)
|
|
6
20
|
|
|
7
21
|
|
package/admin/AdminApp.tsx
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Layout: models on the left (`<Sidebar>`), the changelist table in the main
|
|
11
11
|
* area, a Linear-style filter bar on top (`<FilterBar>`), and add/change forms.
|
|
12
12
|
*/
|
|
13
|
-
import React, { useEffect, useMemo, useState } from "react";
|
|
13
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
14
14
|
import {
|
|
15
15
|
AutoForm,
|
|
16
16
|
Button,
|
|
@@ -28,9 +28,11 @@ import {
|
|
|
28
28
|
mergeDiscovered,
|
|
29
29
|
} from "./adapters";
|
|
30
30
|
import {
|
|
31
|
+
RefreshContext,
|
|
31
32
|
useDashboardStats,
|
|
32
33
|
useDeleteRecord,
|
|
33
34
|
useFilterModel,
|
|
35
|
+
useRefresh,
|
|
34
36
|
useResourceForm,
|
|
35
37
|
useResourceTable,
|
|
36
38
|
} from "./useAdmin";
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
IconLayers,
|
|
57
59
|
IconLink,
|
|
58
60
|
IconPlus,
|
|
61
|
+
IconRefresh,
|
|
59
62
|
IconTable,
|
|
60
63
|
IconTrash,
|
|
61
64
|
} from "./ui/icons";
|
|
@@ -100,7 +103,43 @@ interface Crumb {
|
|
|
100
103
|
hash?: string;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/** Compact relative age, e.g. "just now", "3m ago", "2h ago". */
|
|
107
|
+
function formatAgo(ts: number, now: number): string {
|
|
108
|
+
if (!ts) return "—";
|
|
109
|
+
const secs = Math.max(0, Math.round((now - ts) / 1000));
|
|
110
|
+
if (secs < 5) return "just now";
|
|
111
|
+
if (secs < 60) return `${secs}s ago`;
|
|
112
|
+
const mins = Math.round(secs / 60);
|
|
113
|
+
if (mins < 60) return `${mins}m ago`;
|
|
114
|
+
const hrs = Math.round(mins / 60);
|
|
115
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
116
|
+
return `${Math.round(hrs / 24)}d ago`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The "Updated …" label — ticks over time and snaps fresh when data lands. */
|
|
120
|
+
function LastUpdated(): React.JSX.Element {
|
|
121
|
+
const { lastUpdated } = useRefresh();
|
|
122
|
+
const [now, setNow] = useState(() => Date.now());
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const id = window.setInterval(() => setNow(Date.now()), 30_000);
|
|
125
|
+
return () => window.clearInterval(id);
|
|
126
|
+
}, []);
|
|
127
|
+
useEffect(() => setNow(Date.now()), [lastUpdated]);
|
|
128
|
+
return (
|
|
129
|
+
<span className="ras-updated" style={{ marginLeft: "auto" }}>
|
|
130
|
+
Updated {formatAgo(lastUpdated, now)}
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
103
135
|
function Topbar({ crumbs }: { crumbs: Crumb[] }): React.JSX.Element {
|
|
136
|
+
const { refresh } = useRefresh();
|
|
137
|
+
const [spinning, setSpinning] = useState(false);
|
|
138
|
+
const onReload = () => {
|
|
139
|
+
setSpinning(true);
|
|
140
|
+
refresh();
|
|
141
|
+
window.setTimeout(() => setSpinning(false), 500);
|
|
142
|
+
};
|
|
104
143
|
return (
|
|
105
144
|
<header className="ras-topbar">
|
|
106
145
|
<nav className="ras-crumbs">
|
|
@@ -115,6 +154,16 @@ function Topbar({ crumbs }: { crumbs: Crumb[] }): React.JSX.Element {
|
|
|
115
154
|
</React.Fragment>
|
|
116
155
|
))}
|
|
117
156
|
</nav>
|
|
157
|
+
<LastUpdated />
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
className="ras-icon-btn"
|
|
161
|
+
onClick={onReload}
|
|
162
|
+
title="Reload data"
|
|
163
|
+
aria-label="Reload data"
|
|
164
|
+
>
|
|
165
|
+
<IconRefresh size={16} className={spinning ? "ras-spin" : undefined} />
|
|
166
|
+
</button>
|
|
118
167
|
</header>
|
|
119
168
|
);
|
|
120
169
|
}
|
|
@@ -695,6 +744,10 @@ export function AdminApp(props: {
|
|
|
695
744
|
const { engine } = props;
|
|
696
745
|
const resources = useDiscoveredResources(engine, props.resources);
|
|
697
746
|
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
|
747
|
+
const [nonce, setNonce] = useState(0);
|
|
748
|
+
const [lastUpdated, setLastUpdated] = useState(0);
|
|
749
|
+
const refresh = useCallback(() => setNonce((n) => n + 1), []);
|
|
750
|
+
const markUpdated = useCallback(() => setLastUpdated(Date.now()), []);
|
|
698
751
|
|
|
699
752
|
const activeKey =
|
|
700
753
|
route.name === "index" ? undefined : `${route.app}.${route.model}`;
|
|
@@ -737,7 +790,7 @@ export function AdminApp(props: {
|
|
|
737
790
|
}
|
|
738
791
|
|
|
739
792
|
return (
|
|
740
|
-
|
|
793
|
+
<RefreshContext.Provider value={{ nonce, refresh, lastUpdated, markUpdated }}>
|
|
741
794
|
<AdminStyles />
|
|
742
795
|
<div className={"ras-root" + (collapsed ? " is-collapsed" : "")}>
|
|
743
796
|
<Sidebar
|
|
@@ -748,6 +801,6 @@ export function AdminApp(props: {
|
|
|
748
801
|
/>
|
|
749
802
|
<main className="ras-main">{screen}</main>
|
|
750
803
|
</div>
|
|
751
|
-
|
|
804
|
+
</RefreshContext.Provider>
|
|
752
805
|
);
|
|
753
806
|
}
|
package/admin/index.tsx
CHANGED
|
@@ -132,8 +132,14 @@ function Boot(): React.JSX.Element {
|
|
|
132
132
|
openapi={openapi}
|
|
133
133
|
loadEngine={loadEngine}
|
|
134
134
|
warehouseBaseUrl={cfg.warehouseBaseUrl ?? "/data/warehouse/"}
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// In local mode the WASM warehouse *is* the database — there is no server
|
|
136
|
+
// behind it. So edits (creates/updates/deletes) MUST persist to IndexedDB;
|
|
137
|
+
// otherwise every reload re-seeds from the committed `/data/warehouse/`
|
|
138
|
+
// and silently resurrects deleted rows. The Iceberg file cache (`cache`,
|
|
139
|
+
// on by default) still makes those reloads fast — it caches the immutable
|
|
140
|
+
// Parquet used for the *first* seed; the persisted warehouse blob, once it
|
|
141
|
+
// exists, wins over re-seeding and carries the user's edits forward.
|
|
142
|
+
persist={true}
|
|
137
143
|
>
|
|
138
144
|
<ThemeProvider tokens={theme}>
|
|
139
145
|
<LocalAdmin resources={resources} />
|
package/admin/ui/icons.tsx
CHANGED
|
@@ -152,6 +152,15 @@ export const IconArrowUpRight = (p: SvgProps) => (
|
|
|
152
152
|
</Svg>
|
|
153
153
|
);
|
|
154
154
|
|
|
155
|
+
export const IconRefresh = (p: SvgProps) => (
|
|
156
|
+
<Svg {...p}>
|
|
157
|
+
<path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
|
|
158
|
+
<path d="M21 3v5h-5" />
|
|
159
|
+
<path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
|
|
160
|
+
<path d="M3 21v-5h5" />
|
|
161
|
+
</Svg>
|
|
162
|
+
);
|
|
163
|
+
|
|
155
164
|
export const IconPanelLeft = (p: SvgProps) => (
|
|
156
165
|
<Svg {...p}>
|
|
157
166
|
<rect x="3.5" y="4.5" width="17" height="15" rx="2" />
|
package/admin/ui/styles.ts
CHANGED
|
@@ -192,6 +192,8 @@ a { color: inherit; text-decoration: none; }
|
|
|
192
192
|
transition: background 0.14s, color 0.14s, border-color 0.14s;
|
|
193
193
|
}
|
|
194
194
|
.ras-icon-btn:hover { color: var(--color-text); border-color: color-mix(in srgb, var(--color-text) 22%, transparent); }
|
|
195
|
+
@keyframes ras-spin { to { transform: rotate(360deg); } }
|
|
196
|
+
.ras-spin { animation: ras-spin 0.5s linear; }
|
|
195
197
|
|
|
196
198
|
/* ---- top bar ------------------------------------------------------------- */
|
|
197
199
|
.ras-topbar {
|
|
@@ -212,6 +214,7 @@ a { color: inherit; text-decoration: none; }
|
|
|
212
214
|
.ras-crumbs a:hover { color: var(--color-text); }
|
|
213
215
|
.ras-crumb-sep { color: color-mix(in srgb, var(--color-text-muted) 55%, transparent); }
|
|
214
216
|
.ras-crumb-current { color: var(--color-text); font-weight: 600; }
|
|
217
|
+
.ras-updated { font-size: 12.5px; color: var(--color-text-muted); white-space: nowrap; }
|
|
215
218
|
|
|
216
219
|
.ras-content { padding: 26px 28px 64px; max-width: 1240px; width: 100%; }
|
|
217
220
|
|
package/admin/useAdmin.ts
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
* one admin serves any app.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createContext,
|
|
12
|
+
useCallback,
|
|
13
|
+
useContext,
|
|
14
|
+
useEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useState,
|
|
17
|
+
} from "react";
|
|
11
18
|
import type { RastackEngine } from "../provider/types";
|
|
12
19
|
import type { AutoFormField, AutoFormLike, TableQuery } from "../components/types";
|
|
13
20
|
import type {
|
|
@@ -45,6 +52,30 @@ interface EngineResult {
|
|
|
45
52
|
}
|
|
46
53
|
const ok = (r: EngineResult) => r.status >= 200 && r.status < 300;
|
|
47
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A global "reload" signal for the admin. The engine holds the live data (the
|
|
57
|
+
* WASM warehouse under `rastack dev`, the real `rastack-server` under
|
|
58
|
+
* `rastack admin`), so a manual reload just re-runs the read effects against it.
|
|
59
|
+
* `nonce` bumps on every {@link RefreshApi.refresh}; the data hooks include it in
|
|
60
|
+
* their fetch deps so they refetch **without** resetting filters/paging. The
|
|
61
|
+
* default value keeps the hooks usable with no provider (tests, embedding).
|
|
62
|
+
*/
|
|
63
|
+
export interface RefreshApi {
|
|
64
|
+
nonce: number;
|
|
65
|
+
refresh: () => void;
|
|
66
|
+
/** Epoch ms of the most recent successful load (0 before the first). */
|
|
67
|
+
lastUpdated: number;
|
|
68
|
+
/** Called by the data hooks when a fetch resolves, to stamp {@link lastUpdated}. */
|
|
69
|
+
markUpdated: () => void;
|
|
70
|
+
}
|
|
71
|
+
export const RefreshContext = createContext<RefreshApi>({
|
|
72
|
+
nonce: 0,
|
|
73
|
+
refresh: () => {},
|
|
74
|
+
lastUpdated: 0,
|
|
75
|
+
markUpdated: () => {},
|
|
76
|
+
});
|
|
77
|
+
export const useRefresh = (): RefreshApi => useContext(RefreshContext);
|
|
78
|
+
|
|
48
79
|
/** What `useResourceTable` returns: a `TableQuery` plus changelist controls. */
|
|
49
80
|
export interface ResourceTable {
|
|
50
81
|
query: TableQuery;
|
|
@@ -75,6 +106,7 @@ export function useResourceTable(
|
|
|
75
106
|
const [isLoading, setLoading] = useState(true);
|
|
76
107
|
const [error, setError] = useState<unknown>(undefined);
|
|
77
108
|
const [tick, setTick] = useState(0);
|
|
109
|
+
const { nonce, markUpdated } = useRefresh();
|
|
78
110
|
|
|
79
111
|
// Reset paging/search/filters when the resource changes.
|
|
80
112
|
useEffect(() => {
|
|
@@ -111,6 +143,7 @@ export function useResourceTable(
|
|
|
111
143
|
setData([]);
|
|
112
144
|
}
|
|
113
145
|
setLoading(false);
|
|
146
|
+
markUpdated();
|
|
114
147
|
})
|
|
115
148
|
.catch((e: unknown) => {
|
|
116
149
|
if (cancelled) return;
|
|
@@ -121,7 +154,7 @@ export function useResourceTable(
|
|
|
121
154
|
return () => {
|
|
122
155
|
cancelled = true;
|
|
123
156
|
};
|
|
124
|
-
}, [engine, resource.key, resource.app, resource.model, page, search, filterKey, tick]);
|
|
157
|
+
}, [engine, resource.key, resource.app, resource.model, page, search, filterKey, tick, nonce]);
|
|
125
158
|
|
|
126
159
|
const setSearch = (v: string | undefined) => {
|
|
127
160
|
setSearchState(v && v.length ? v : undefined);
|
|
@@ -447,6 +480,7 @@ export function useDashboardStats(
|
|
|
447
480
|
): DashboardStats {
|
|
448
481
|
const models = useMemo(() => adminResources(resources), [resources]);
|
|
449
482
|
const [counts, setCounts] = useState<Record<string, number> | null>(null);
|
|
483
|
+
const { nonce, markUpdated } = useRefresh();
|
|
450
484
|
|
|
451
485
|
useEffect(() => {
|
|
452
486
|
let cancelled = false;
|
|
@@ -467,12 +501,15 @@ export function useDashboardStats(
|
|
|
467
501
|
}
|
|
468
502
|
}),
|
|
469
503
|
);
|
|
470
|
-
if (!cancelled)
|
|
504
|
+
if (!cancelled) {
|
|
505
|
+
setCounts(Object.fromEntries(entries));
|
|
506
|
+
markUpdated();
|
|
507
|
+
}
|
|
471
508
|
})();
|
|
472
509
|
return () => {
|
|
473
510
|
cancelled = true;
|
|
474
511
|
};
|
|
475
|
-
}, [engine, models]);
|
|
512
|
+
}, [engine, models, nonce]);
|
|
476
513
|
|
|
477
514
|
return useMemo((): DashboardStats => {
|
|
478
515
|
const loading = counts === null;
|
package/auth/provider.ts
CHANGED
|
@@ -74,10 +74,7 @@ export interface CognitoAuthProviderConfig extends Partial<CognitoAuthConfig> {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export type CognitoAuthStatus =
|
|
77
|
-
| "
|
|
78
|
-
| "authenticated"
|
|
79
|
-
| "unauthenticated"
|
|
80
|
-
| "error";
|
|
77
|
+
"loading" | "authenticated" | "unauthenticated" | "error";
|
|
81
78
|
|
|
82
79
|
/** What `useCognitoAuth()` returns. */
|
|
83
80
|
export interface CognitoAuthContextValue {
|
|
@@ -178,7 +175,8 @@ export function CognitoAuthProvider({
|
|
|
178
175
|
const keyBase = config.storageKey ?? "rastack.auth";
|
|
179
176
|
const sessionKey = `${keyBase}.session`;
|
|
180
177
|
const pkceKey = `${keyBase}.pkce`;
|
|
181
|
-
const doFetch =
|
|
178
|
+
const doFetch =
|
|
179
|
+
config.fetchImpl ?? (globalThis.fetch?.bind(globalThis) as typeof fetch);
|
|
182
180
|
|
|
183
181
|
const [session, setSession] = useState<CognitoSession | null>(null);
|
|
184
182
|
const [status, setStatus] = useState<CognitoAuthStatus>("loading");
|
|
@@ -242,7 +240,8 @@ export function CognitoAuthProvider({
|
|
|
242
240
|
const redirect = parseRedirect(dom.window.location.search);
|
|
243
241
|
if (redirect.error) {
|
|
244
242
|
clearUrl();
|
|
245
|
-
if (!cancelled)
|
|
243
|
+
if (!cancelled)
|
|
244
|
+
fail(new Error(redirect.errorDescription ?? redirect.error));
|
|
246
245
|
return;
|
|
247
246
|
}
|
|
248
247
|
if (redirect.code) {
|
|
@@ -288,14 +287,17 @@ export function CognitoAuthProvider({
|
|
|
288
287
|
): Promise<CognitoSession> {
|
|
289
288
|
const hosted = requireHostedConfig(config);
|
|
290
289
|
const pending = storage.getItem(pkceKey);
|
|
291
|
-
if (!pending)
|
|
290
|
+
if (!pending)
|
|
291
|
+
throw new Error("No pending PKCE verifier — restart sign-in.");
|
|
292
292
|
const { verifier, state: expectedState } = JSON.parse(pending) as {
|
|
293
293
|
verifier: string;
|
|
294
294
|
state: string;
|
|
295
295
|
};
|
|
296
296
|
storage.removeItem(pkceKey);
|
|
297
297
|
if (expectedState && state && expectedState !== state) {
|
|
298
|
-
throw new Error(
|
|
298
|
+
throw new Error(
|
|
299
|
+
"OAuth state mismatch — possible CSRF, sign-in rejected.",
|
|
300
|
+
);
|
|
299
301
|
}
|
|
300
302
|
const response = await doFetch(tokenEndpoint(hosted), {
|
|
301
303
|
method: "POST",
|
|
@@ -484,3 +486,64 @@ export function useRastackAuthFromCognito(): RastackAuth {
|
|
|
484
486
|
[auth.isAuthenticated, auth.claims],
|
|
485
487
|
);
|
|
486
488
|
}
|
|
489
|
+
|
|
490
|
+
// -- Dev-time Cognito runtime config (real dev pool, opt-in) ------------------
|
|
491
|
+
// `rastack dev` serves the dev environment's Cognito coordinates at
|
|
492
|
+
// `/__rastack/cognito.json` so a locally-running app can sign in against the
|
|
493
|
+
// *real* dev user pool (to obtain real tokens — e.g. for the deployed
|
|
494
|
+
// external-API proxy) instead of the unsigned `mode:"local"` shim. These
|
|
495
|
+
// helpers turn that JSON into a `<CognitoAuthProvider config>`; a developer opts
|
|
496
|
+
// in with `cognitoConfigFromGlobal() ?? { mode: "local" }`.
|
|
497
|
+
|
|
498
|
+
/** The shape `rastack dev` serves at `/__rastack/cognito.json`. */
|
|
499
|
+
export interface RastackCognitoRuntime {
|
|
500
|
+
userPoolId?: string;
|
|
501
|
+
clientId: string;
|
|
502
|
+
/** Hosted UI base URL (`https://…amazoncognito.com`). */
|
|
503
|
+
hostedUiUrl: string;
|
|
504
|
+
region: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Map a served runtime config into a `CognitoAuthProviderConfig` for the real
|
|
509
|
+
* Hosted UI flow. `redirectUri` defaults to the current page origin (where
|
|
510
|
+
* `rastack dev` serves the app), so local sign-in round-trips back correctly.
|
|
511
|
+
*/
|
|
512
|
+
export function cognitoRuntimeToConfig(
|
|
513
|
+
runtime: RastackCognitoRuntime,
|
|
514
|
+
opts: { redirectUri?: string } & Partial<CognitoAuthProviderConfig> = {},
|
|
515
|
+
): CognitoAuthProviderConfig {
|
|
516
|
+
const origin = dom.window?.location?.origin as string | undefined;
|
|
517
|
+
const { redirectUri, ...rest } = opts;
|
|
518
|
+
return {
|
|
519
|
+
mode: "hosted-ui",
|
|
520
|
+
domain: runtime.hostedUiUrl,
|
|
521
|
+
clientId: runtime.clientId,
|
|
522
|
+
region: runtime.region,
|
|
523
|
+
userPoolId: runtime.userPoolId,
|
|
524
|
+
redirectUri: redirectUri ?? origin ?? "",
|
|
525
|
+
...rest,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Read the runtime Cognito config `rastack dev` injects as
|
|
531
|
+
* `globalThis.__RASTACK_COGNITO__`, mapped to a provider config — or undefined
|
|
532
|
+
* when none is present (so the caller falls back to `{ mode: "local" }`). Set
|
|
533
|
+
* the global from the served JSON, e.g.:
|
|
534
|
+
*
|
|
535
|
+
* ```html
|
|
536
|
+
* <script>
|
|
537
|
+
* fetch("/__rastack/cognito.json").then(r => r.ok && r.json())
|
|
538
|
+
* .then(c => { if (c) window.__RASTACK_COGNITO__ = c; })
|
|
539
|
+
* </script>
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
export function cognitoConfigFromGlobal(
|
|
543
|
+
opts: { redirectUri?: string } & Partial<CognitoAuthProviderConfig> = {},
|
|
544
|
+
): CognitoAuthProviderConfig | undefined {
|
|
545
|
+
const runtime = (dom as { __RASTACK_COGNITO__?: RastackCognitoRuntime })
|
|
546
|
+
.__RASTACK_COGNITO__;
|
|
547
|
+
if (!runtime?.clientId || !runtime?.hostedUiUrl) return undefined;
|
|
548
|
+
return cognitoRuntimeToConfig(runtime, opts);
|
|
549
|
+
}
|