@orangecheck/auth-client 2.17.1 → 2.18.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.d.mts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +224 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +216 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -27,6 +27,91 @@ function buildAddAccountUrl(cfg, returnTo) {
|
|
|
27
27
|
u.searchParams.set("add", "1");
|
|
28
28
|
return u.toString();
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
// src/tab-session.ts
|
|
32
|
+
var TAB_SESSION_HEADER = "x-oc-tab-session";
|
|
33
|
+
var TAB_SESSION_STORAGE_KEY = "oc_tab_session";
|
|
34
|
+
var TAB_ADOPT_HASH = "#oc-adopt";
|
|
35
|
+
function readTabSession() {
|
|
36
|
+
if (typeof window === "undefined") return null;
|
|
37
|
+
try {
|
|
38
|
+
const raw = window.sessionStorage.getItem(TAB_SESSION_STORAGE_KEY);
|
|
39
|
+
if (!raw) return null;
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
if (typeof parsed.token !== "string" || parsed.token.length === 0) return null;
|
|
42
|
+
if (typeof parsed.didOc !== "string" || parsed.didOc.length === 0) return null;
|
|
43
|
+
return { token: parsed.token, didOc: parsed.didOc };
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function writeTabSession(session) {
|
|
49
|
+
if (typeof window === "undefined") return;
|
|
50
|
+
try {
|
|
51
|
+
window.sessionStorage.setItem(TAB_SESSION_STORAGE_KEY, JSON.stringify(session));
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function clearTabSession() {
|
|
56
|
+
if (typeof window === "undefined") return;
|
|
57
|
+
try {
|
|
58
|
+
window.sessionStorage.removeItem(TAB_SESSION_STORAGE_KEY);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function tabSessionHeader() {
|
|
63
|
+
const pin = readTabSession();
|
|
64
|
+
return pin ? { [TAB_SESSION_HEADER]: pin.token } : {};
|
|
65
|
+
}
|
|
66
|
+
function isPinnableUrl(url, authOrigin) {
|
|
67
|
+
if (typeof window === "undefined") return false;
|
|
68
|
+
try {
|
|
69
|
+
const u = new URL(url, window.location.href);
|
|
70
|
+
return u.origin === window.location.origin || u.origin === authOrigin;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function installTabFetchInterceptor(authOrigin) {
|
|
76
|
+
if (typeof window === "undefined") return () => {
|
|
77
|
+
};
|
|
78
|
+
const original = window.fetch;
|
|
79
|
+
const wrapped = (input, init) => {
|
|
80
|
+
try {
|
|
81
|
+
const pin = readTabSession();
|
|
82
|
+
if (pin) {
|
|
83
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
84
|
+
if (isPinnableUrl(url, authOrigin)) {
|
|
85
|
+
const headers = new Headers(
|
|
86
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
87
|
+
);
|
|
88
|
+
if (!headers.has("authorization") && !headers.has(TAB_SESSION_HEADER)) {
|
|
89
|
+
headers.set(TAB_SESSION_HEADER, pin.token);
|
|
90
|
+
init = { ...init, headers };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
return original.call(window, input, init);
|
|
97
|
+
};
|
|
98
|
+
window.fetch = wrapped;
|
|
99
|
+
return () => {
|
|
100
|
+
if (window.fetch === wrapped) window.fetch = original;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function consumeTabAdoptMarker() {
|
|
104
|
+
if (typeof window === "undefined") return false;
|
|
105
|
+
if (!window.location.hash.includes(TAB_ADOPT_HASH.slice(1))) return false;
|
|
106
|
+
clearTabSession();
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(window.location.href);
|
|
109
|
+
url.hash = "";
|
|
110
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
30
115
|
var SessionContext = React3.createContext(null);
|
|
31
116
|
function normalizeDisplayIdentity(raw, didOc) {
|
|
32
117
|
const di = raw.display_identity ?? raw.displayIdentity;
|
|
@@ -48,6 +133,20 @@ function normalizeRosterEntry(raw) {
|
|
|
48
133
|
lastSeenAt: raw.last_seen_at ?? raw.lastSeenAt ?? null
|
|
49
134
|
};
|
|
50
135
|
}
|
|
136
|
+
async function fetchHostRoster(cfg) {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${cfg.authOrigin}${cfg.mePath}`, {
|
|
139
|
+
method: "GET",
|
|
140
|
+
credentials: "include",
|
|
141
|
+
headers: { Accept: "application/json", ...tabSessionHeader() }
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) return [];
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
return Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
|
|
146
|
+
} catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
51
150
|
function normalizeAccount(raw) {
|
|
52
151
|
if (!raw) return null;
|
|
53
152
|
const didOc = raw.did_oc ?? raw.didOc;
|
|
@@ -76,14 +175,50 @@ function OcSessionProvider({
|
|
|
76
175
|
const [roster, setRoster] = React3.useState([]);
|
|
77
176
|
const [status, setStatus] = React3.useState("loading");
|
|
78
177
|
const [error, setError] = React3.useState(null);
|
|
178
|
+
const [tabPinned, setTabPinned] = React3.useState(false);
|
|
179
|
+
const pinInFlightRef = React3.useRef(false);
|
|
180
|
+
const pinThisTab = React3.useCallback(
|
|
181
|
+
async (didOc) => {
|
|
182
|
+
if (pinInFlightRef.current || readTabSession()) return;
|
|
183
|
+
pinInFlightRef.current = true;
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetch(`${cfg.authOrigin}/api/auth/tab`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
credentials: "include",
|
|
188
|
+
headers: { Accept: "application/json" }
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) return;
|
|
191
|
+
const body = await res.json();
|
|
192
|
+
if (body.ok && typeof body.token === "string" && body.account?.did_oc === didOc) {
|
|
193
|
+
writeTabSession({ token: body.token, didOc });
|
|
194
|
+
setTabPinned(true);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
} finally {
|
|
198
|
+
pinInFlightRef.current = false;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
[cfg.authOrigin]
|
|
202
|
+
);
|
|
79
203
|
const refresh = React3.useCallback(async () => {
|
|
80
204
|
if (typeof window === "undefined") return;
|
|
81
205
|
try {
|
|
82
|
-
|
|
206
|
+
let pin = readTabSession();
|
|
207
|
+
let res = await fetch(cfg.mePath, {
|
|
83
208
|
method: "GET",
|
|
84
209
|
credentials: "include",
|
|
85
|
-
headers: { Accept: "application/json" }
|
|
210
|
+
headers: { Accept: "application/json", ...tabSessionHeader() }
|
|
86
211
|
});
|
|
212
|
+
if (res.status === 401 && pin) {
|
|
213
|
+
clearTabSession();
|
|
214
|
+
pin = null;
|
|
215
|
+
setTabPinned(false);
|
|
216
|
+
res = await fetch(cfg.mePath, {
|
|
217
|
+
method: "GET",
|
|
218
|
+
credentials: "include",
|
|
219
|
+
headers: { Accept: "application/json" }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
87
222
|
if (res.status === 401) {
|
|
88
223
|
setAccount(null);
|
|
89
224
|
setRoster([]);
|
|
@@ -98,19 +233,32 @@ function OcSessionProvider({
|
|
|
98
233
|
}
|
|
99
234
|
const body = await res.json();
|
|
100
235
|
const acct = normalizeAccount(body.account);
|
|
236
|
+
if (acct && pin && acct.didOc !== pin.didOc) {
|
|
237
|
+
clearTabSession();
|
|
238
|
+
pin = null;
|
|
239
|
+
}
|
|
240
|
+
setTabPinned(pin !== null);
|
|
101
241
|
const rosterEntries = Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
|
|
102
242
|
setAccount(acct);
|
|
103
243
|
setRoster(rosterEntries);
|
|
104
244
|
setStatus(acct ? "authenticated" : "anonymous");
|
|
105
245
|
setError(null);
|
|
246
|
+
if (acct && !pin) void pinThisTab(acct.didOc);
|
|
247
|
+
const currentOrigin = typeof window !== "undefined" ? window.location.origin : null;
|
|
248
|
+
if (acct && rosterEntries.length === 0 && currentOrigin !== null && !cfg.authOrigin.startsWith(currentOrigin)) {
|
|
249
|
+
const peers = await fetchHostRoster(cfg);
|
|
250
|
+
if (peers.length > 0) setRoster(peers);
|
|
251
|
+
}
|
|
106
252
|
} catch (err) {
|
|
107
253
|
setStatus("error");
|
|
108
254
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
109
255
|
}
|
|
110
|
-
}, [cfg
|
|
256
|
+
}, [cfg, pinThisTab]);
|
|
111
257
|
React3.useEffect(() => {
|
|
258
|
+
consumeTabAdoptMarker();
|
|
112
259
|
void refresh();
|
|
113
260
|
}, [refresh]);
|
|
261
|
+
React3.useEffect(() => installTabFetchInterceptor(cfg.authOrigin), [cfg.authOrigin]);
|
|
114
262
|
const signOut = React3.useCallback(
|
|
115
263
|
async (opts) => {
|
|
116
264
|
const scope = opts?.scope ?? "all";
|
|
@@ -120,6 +268,10 @@ function OcSessionProvider({
|
|
|
120
268
|
const res = await fetch(url.toString(), {
|
|
121
269
|
method: "POST",
|
|
122
270
|
credentials: "include",
|
|
271
|
+
// Per-tab · carry the pin so scope='current' signs out
|
|
272
|
+
// the account THIS tab is operating as, not whichever
|
|
273
|
+
// account the shared cookie happens to point at.
|
|
274
|
+
headers: { ...tabSessionHeader() },
|
|
123
275
|
// `keepalive` lets the logout round-trip complete even if
|
|
124
276
|
// the caller hard-navigates away in the same tick (e.g.
|
|
125
277
|
// `<OcAccountMenu>` redirects home immediately on sign-out).
|
|
@@ -127,11 +279,15 @@ function OcSessionProvider({
|
|
|
127
279
|
// and the `.ochk.io` cookie may never get cleared.
|
|
128
280
|
keepalive: true
|
|
129
281
|
});
|
|
282
|
+
clearTabSession();
|
|
283
|
+
setTabPinned(false);
|
|
130
284
|
if (scope === "current" && res.ok) {
|
|
131
285
|
await refresh();
|
|
132
286
|
return;
|
|
133
287
|
}
|
|
134
288
|
} catch {
|
|
289
|
+
clearTabSession();
|
|
290
|
+
setTabPinned(false);
|
|
135
291
|
}
|
|
136
292
|
setAccount(null);
|
|
137
293
|
setRoster([]);
|
|
@@ -145,7 +301,7 @@ function OcSessionProvider({
|
|
|
145
301
|
const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
|
|
146
302
|
method: "POST",
|
|
147
303
|
credentials: "include",
|
|
148
|
-
headers: { "Content-Type": "application/json" },
|
|
304
|
+
headers: { "Content-Type": "application/json", ...tabSessionHeader() },
|
|
149
305
|
body: JSON.stringify({ did_oc: didOc })
|
|
150
306
|
});
|
|
151
307
|
if (!res.ok) {
|
|
@@ -157,6 +313,14 @@ function OcSessionProvider({
|
|
|
157
313
|
}
|
|
158
314
|
throw new Error(`[@orangecheck/auth-client] switchAccount failed: ${reason}`);
|
|
159
315
|
}
|
|
316
|
+
try {
|
|
317
|
+
const body = await res.json();
|
|
318
|
+
if (typeof body.token === "string" && body.account?.did_oc) {
|
|
319
|
+
writeTabSession({ token: body.token, didOc: body.account.did_oc });
|
|
320
|
+
setTabPinned(true);
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
160
324
|
await refresh();
|
|
161
325
|
},
|
|
162
326
|
[cfg.authOrigin, refresh]
|
|
@@ -174,7 +338,7 @@ function OcSessionProvider({
|
|
|
174
338
|
const res = await fetch(`${cfg.authOrigin}/api/auth/account`, {
|
|
175
339
|
method: "PATCH",
|
|
176
340
|
credentials: "include",
|
|
177
|
-
headers: { "Content-Type": "application/json" },
|
|
341
|
+
headers: { "Content-Type": "application/json", ...tabSessionHeader() },
|
|
178
342
|
body: JSON.stringify({ display_identity: kind })
|
|
179
343
|
});
|
|
180
344
|
if (!res.ok) {
|
|
@@ -186,6 +350,14 @@ function OcSessionProvider({
|
|
|
186
350
|
}
|
|
187
351
|
throw new Error(`[@orangecheck/auth-client] setDisplayIdentity failed: ${reason}`);
|
|
188
352
|
}
|
|
353
|
+
try {
|
|
354
|
+
const body = await res.json();
|
|
355
|
+
const pin = readTabSession();
|
|
356
|
+
if (pin && typeof body.token === "string" && body.account?.did_oc === pin.didOc) {
|
|
357
|
+
writeTabSession({ token: body.token, didOc: pin.didOc });
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
189
361
|
await refresh();
|
|
190
362
|
},
|
|
191
363
|
[cfg.authOrigin, refresh]
|
|
@@ -196,6 +368,7 @@ function OcSessionProvider({
|
|
|
196
368
|
status,
|
|
197
369
|
account,
|
|
198
370
|
roster,
|
|
371
|
+
tabPinned,
|
|
199
372
|
error,
|
|
200
373
|
refresh,
|
|
201
374
|
signOut,
|
|
@@ -208,6 +381,7 @@ function OcSessionProvider({
|
|
|
208
381
|
status,
|
|
209
382
|
account,
|
|
210
383
|
roster,
|
|
384
|
+
tabPinned,
|
|
211
385
|
error,
|
|
212
386
|
refresh,
|
|
213
387
|
signOut,
|
|
@@ -1733,6 +1907,18 @@ function safeReturnTo(input) {
|
|
|
1733
1907
|
if (!candidate.startsWith("/") || candidate.startsWith("//")) return "/";
|
|
1734
1908
|
return candidate;
|
|
1735
1909
|
}
|
|
1910
|
+
function familyReturnTarget(input) {
|
|
1911
|
+
if (typeof input !== "string" || input.length === 0) return void 0;
|
|
1912
|
+
if (input.startsWith("/") && !input.startsWith("//")) return input;
|
|
1913
|
+
try {
|
|
1914
|
+
const u = new URL(input);
|
|
1915
|
+
if (u.protocol !== "https:") return void 0;
|
|
1916
|
+
const host = u.hostname.toLowerCase();
|
|
1917
|
+
if (host === "ochk.io" || host.endsWith(".ochk.io")) return u.toString();
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
return void 0;
|
|
1921
|
+
}
|
|
1736
1922
|
function hardNavigate(target) {
|
|
1737
1923
|
if (typeof window === "undefined") return;
|
|
1738
1924
|
window.location.assign(target);
|
|
@@ -1751,7 +1937,20 @@ function OcSignIn({
|
|
|
1751
1937
|
}) {
|
|
1752
1938
|
const walletEnabled = paths?.wallet ?? true;
|
|
1753
1939
|
const emailEnabled = paths?.email ?? true;
|
|
1754
|
-
const
|
|
1940
|
+
const [resolvedReturn, setResolvedReturn] = React3.useState(
|
|
1941
|
+
() => familyReturnTarget(returnTo) ?? safeReturnTo(returnTo)
|
|
1942
|
+
);
|
|
1943
|
+
React3.useEffect(() => {
|
|
1944
|
+
const fromProp = familyReturnTarget(returnTo);
|
|
1945
|
+
if (fromProp) {
|
|
1946
|
+
setResolvedReturn(fromProp);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (typeof window === "undefined") return;
|
|
1950
|
+
const q = new URLSearchParams(window.location.search);
|
|
1951
|
+
const fromQuery = familyReturnTarget(q.get("return_to")) ?? familyReturnTarget(q.get("next"));
|
|
1952
|
+
setResolvedReturn(fromQuery ?? "/");
|
|
1953
|
+
}, [returnTo]);
|
|
1755
1954
|
const [addMode, setAddMode] = React3.useState(Boolean(addProp));
|
|
1756
1955
|
React3.useEffect(() => {
|
|
1757
1956
|
if (typeof window === "undefined") return;
|
|
@@ -1775,18 +1974,20 @@ function OcSignIn({
|
|
|
1775
1974
|
async (account) => {
|
|
1776
1975
|
if (resolveReturnTo) {
|
|
1777
1976
|
try {
|
|
1778
|
-
|
|
1977
|
+
const resolved = familyReturnTarget(await resolveReturnTo(account));
|
|
1978
|
+
hardNavigate(resolved ?? resolvedReturn);
|
|
1779
1979
|
return;
|
|
1780
1980
|
} catch {
|
|
1781
1981
|
}
|
|
1782
1982
|
}
|
|
1783
|
-
hardNavigate(
|
|
1983
|
+
hardNavigate(resolvedReturn);
|
|
1784
1984
|
},
|
|
1785
|
-
[resolveReturnTo,
|
|
1985
|
+
[resolveReturnTo, resolvedReturn]
|
|
1786
1986
|
);
|
|
1787
1987
|
const handleSuccess = React3.useCallback(
|
|
1788
1988
|
async (account, token, via) => {
|
|
1789
1989
|
const proceed = () => {
|
|
1990
|
+
clearTabSession();
|
|
1790
1991
|
if (onSuccess) onSuccess(account, token);
|
|
1791
1992
|
else void navigate(account);
|
|
1792
1993
|
};
|
|
@@ -1951,7 +2152,7 @@ function OcSignIn({
|
|
|
1951
2152
|
}
|
|
1952
2153
|
)
|
|
1953
2154
|
] }),
|
|
1954
|
-
/* @__PURE__ */ jsx(ProviderSignIn, { authOrigin, returnTo:
|
|
2155
|
+
/* @__PURE__ */ jsx(ProviderSignIn, { authOrigin, returnTo: resolvedReturn, add: addMode }),
|
|
1955
2156
|
linkPrompt && /* @__PURE__ */ jsxs("label", { "data-oc-signin-linkalso": "", style: linkAlsoStyle, children: [
|
|
1956
2157
|
/* @__PURE__ */ jsx(
|
|
1957
2158
|
"input",
|
|
@@ -2021,7 +2222,8 @@ function ProviderIcon({ id }) {
|
|
|
2021
2222
|
}
|
|
2022
2223
|
function ProviderSignIn({
|
|
2023
2224
|
authOrigin,
|
|
2024
|
-
returnTo
|
|
2225
|
+
returnTo,
|
|
2226
|
+
add
|
|
2025
2227
|
}) {
|
|
2026
2228
|
const [providers, setProviders] = React3.useState([]);
|
|
2027
2229
|
const [origin, setOrigin] = React3.useState("");
|
|
@@ -2037,7 +2239,7 @@ function ProviderSignIn({
|
|
|
2037
2239
|
};
|
|
2038
2240
|
}, [authOrigin]);
|
|
2039
2241
|
if (providers.length === 0) return null;
|
|
2040
|
-
const providerReturnTo = origin ? `${origin}${returnTo}` : returnTo;
|
|
2242
|
+
const providerReturnTo = returnTo.startsWith("/") ? origin ? `${origin}${returnTo}` : returnTo : returnTo;
|
|
2041
2243
|
const line = { flex: 1, height: 1, background: "var(--border, #27272a)" };
|
|
2042
2244
|
return /* @__PURE__ */ jsxs("div", { "data-oc-signin-providers": "", style: { marginTop: 20 }, children: [
|
|
2043
2245
|
/* @__PURE__ */ jsxs(
|
|
@@ -2066,7 +2268,7 @@ function ProviderSignIn({
|
|
|
2066
2268
|
{
|
|
2067
2269
|
href: `${authOrigin}/api/auth/${p.id}/start?return_to=${encodeURIComponent(
|
|
2068
2270
|
providerReturnTo
|
|
2069
|
-
)}`,
|
|
2271
|
+
)}${add ? "&add=1" : ""}`,
|
|
2070
2272
|
"data-oc-signin-provider": p.id,
|
|
2071
2273
|
style: {
|
|
2072
2274
|
display: "flex",
|
|
@@ -2747,6 +2949,6 @@ function handleSudoRequired(body, args = {}) {
|
|
|
2747
2949
|
return false;
|
|
2748
2950
|
}
|
|
2749
2951
|
|
|
2750
|
-
export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, OcAccountChip, OcAccountPill, OcAddressInput, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, buildAddAccountUrl, buildSignInUrl, fetchOcLinkedIdentities, handleSudoRequired, redirectToSudo, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister };
|
|
2952
|
+
export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, OcAccountChip, OcAccountPill, OcAddressInput, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, TAB_ADOPT_HASH, TAB_SESSION_HEADER, TAB_SESSION_STORAGE_KEY, buildAddAccountUrl, buildSignInUrl, clearTabSession, consumeTabAdoptMarker, fetchOcLinkedIdentities, handleSudoRequired, installTabFetchInterceptor, readTabSession, redirectToSudo, tabSessionHeader, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister, writeTabSession };
|
|
2751
2953
|
//# sourceMappingURL=index.mjs.map
|
|
2752
2954
|
//# sourceMappingURL=index.mjs.map
|