@zitadel/components 0.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/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/atoms/index.d.mts +2 -0
- package/dist/atoms/index.d.ts +9 -0
- package/dist/atoms/index.d.ts.map +1 -0
- package/dist/atoms/index.mjs +2 -0
- package/dist/atoms/zl-alert.d.ts +56 -0
- package/dist/atoms/zl-alert.d.ts.map +1 -0
- package/dist/atoms/zl-button.d.ts +83 -0
- package/dist/atoms/zl-button.d.ts.map +1 -0
- package/dist/atoms/zl-card.d.ts +46 -0
- package/dist/atoms/zl-card.d.ts.map +1 -0
- package/dist/atoms/zl-field.d.ts +73 -0
- package/dist/atoms/zl-field.d.ts.map +1 -0
- package/dist/atoms/zl-icon.d.ts +48 -0
- package/dist/atoms/zl-icon.d.ts.map +1 -0
- package/dist/atoms/zl-page-shell.d.ts +42 -0
- package/dist/atoms/zl-page-shell.d.ts.map +1 -0
- package/dist/atoms/zl-passkey.d.ts +118 -0
- package/dist/atoms/zl-passkey.d.ts.map +1 -0
- package/dist/atoms/zl-pill.d.ts +55 -0
- package/dist/atoms/zl-pill.d.ts.map +1 -0
- package/dist/atoms-CmOTltTH.mjs +1311 -0
- package/dist/atoms-CmOTltTH.mjs.map +1 -0
- package/dist/index-7FDHEpv5.d.mts +996 -0
- package/dist/index-7FDHEpv5.d.mts.map +1 -0
- package/dist/index-DIUFZjXw.d.mts +6696 -0
- package/dist/index-DIUFZjXw.d.mts.map +1 -0
- package/dist/index-DaOSCm0T.d.mts +249 -0
- package/dist/index-DaOSCm0T.d.mts.map +1 -0
- package/dist/index-bZEvwHtq.d.mts +996 -0
- package/dist/index-bZEvwHtq.d.mts.map +1 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +6 -0
- package/dist/internal/base64url.d.ts +18 -0
- package/dist/internal/base64url.d.ts.map +1 -0
- package/dist/internal/unique-id.d.ts +2 -0
- package/dist/internal/unique-id.d.ts.map +1 -0
- package/dist/manifest-DCC2FxCE.d.mts +39 -0
- package/dist/manifest-DCC2FxCE.d.mts.map +1 -0
- package/dist/manifest.d.ts +36 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifests.d.mts +9 -0
- package/dist/manifests.d.mts.map +1 -0
- package/dist/manifests.d.ts +6 -0
- package/dist/manifests.d.ts.map +1 -0
- package/dist/manifests.mjs +30 -0
- package/dist/manifests.mjs.map +1 -0
- package/dist/orchestrator/api-client.d.ts +33 -0
- package/dist/orchestrator/api-client.d.ts.map +1 -0
- package/dist/orchestrator/branding-to-tokens.d.ts +28 -0
- package/dist/orchestrator/branding-to-tokens.d.ts.map +1 -0
- package/dist/orchestrator/branding-validator.d.ts +7 -0
- package/dist/orchestrator/branding-validator.d.ts.map +1 -0
- package/dist/orchestrator/branding.d.ts +76 -0
- package/dist/orchestrator/branding.d.ts.map +1 -0
- package/dist/orchestrator/font-loader.d.ts +2 -0
- package/dist/orchestrator/font-loader.d.ts.map +1 -0
- package/dist/orchestrator/index.d.mts +2 -0
- package/dist/orchestrator/index.d.ts +25 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/index.mjs +2 -0
- package/dist/orchestrator/liquid.d.ts +28 -0
- package/dist/orchestrator/liquid.d.ts.map +1 -0
- package/dist/orchestrator/locales/de.d.ts +9 -0
- package/dist/orchestrator/locales/de.d.ts.map +1 -0
- package/dist/orchestrator/locales/en.d.ts +13 -0
- package/dist/orchestrator/locales/en.d.ts.map +1 -0
- package/dist/orchestrator/locales/index.d.ts +12 -0
- package/dist/orchestrator/locales/index.d.ts.map +1 -0
- package/dist/orchestrator/mandatory-gates.d.ts +26 -0
- package/dist/orchestrator/mandatory-gates.d.ts.map +1 -0
- package/dist/orchestrator/sanitiser.d.ts +6 -0
- package/dist/orchestrator/sanitiser.d.ts.map +1 -0
- package/dist/orchestrator/template-context.d.ts +55 -0
- package/dist/orchestrator/template-context.d.ts.map +1 -0
- package/dist/orchestrator/template-names.d.ts +16 -0
- package/dist/orchestrator/template-names.d.ts.map +1 -0
- package/dist/orchestrator/templates/auth-form.liquid.d.ts +14 -0
- package/dist/orchestrator/templates/auth-form.liquid.d.ts.map +1 -0
- package/dist/orchestrator/templates/default.liquid.d.ts +23 -0
- package/dist/orchestrator/templates/default.liquid.d.ts.map +1 -0
- package/dist/orchestrator/templates/passkey-upsell.liquid.d.ts +15 -0
- package/dist/orchestrator/templates/passkey-upsell.liquid.d.ts.map +1 -0
- package/dist/orchestrator/templates/signed-in.liquid.d.ts +20 -0
- package/dist/orchestrator/templates/signed-in.liquid.d.ts.map +1 -0
- package/dist/orchestrator/theme-controller.d.ts +30 -0
- package/dist/orchestrator/theme-controller.d.ts.map +1 -0
- package/dist/orchestrator/zitadel-login.d.ts +182 -0
- package/dist/orchestrator/zitadel-login.d.ts.map +1 -0
- package/dist/orchestrator/zitadel-logout.d.ts +86 -0
- package/dist/orchestrator/zitadel-logout.d.ts.map +1 -0
- package/dist/orchestrator-CNKo2WQG.mjs +3336 -0
- package/dist/orchestrator-CNKo2WQG.mjs.map +1 -0
- package/dist/styles/base.d.ts +7 -0
- package/dist/styles/base.d.ts.map +1 -0
- package/dist/styles/focus-ring.d.ts +7 -0
- package/dist/styles/focus-ring.d.ts.map +1 -0
- package/dist/styles/index.d.ts +5 -0
- package/dist/styles/index.d.ts.map +1 -0
- package/dist/styles/surface.d.ts +8 -0
- package/dist/styles/surface.d.ts.map +1 -0
- package/dist/styles/tokens.d.ts +11 -0
- package/dist/styles/tokens.d.ts.map +1 -0
- package/dist/tokens/index.d.mts +2 -0
- package/dist/tokens/index.d.ts +9 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.mjs +2 -0
- package/dist/tokens-TXmurodq.mjs +244 -0
- package/dist/tokens-TXmurodq.mjs.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,3336 @@
|
|
|
1
|
+
import { _ as __decorate, b as t, v as focusVisibleStyles, y as baseHostStyles } from "./atoms-CmOTltTH.mjs";
|
|
2
|
+
import { r as tokensCss } from "./tokens-TXmurodq.mjs";
|
|
3
|
+
import { manifestRegistry } from "./manifests.mjs";
|
|
4
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
5
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
6
|
+
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
7
|
+
import { Liquid } from "liquidjs";
|
|
8
|
+
import DOMPurify from "dompurify";
|
|
9
|
+
//#region ../api/dist/runtime/base-url.mjs
|
|
10
|
+
let proxyPath = "";
|
|
11
|
+
function getProxyPath() {
|
|
12
|
+
return proxyPath;
|
|
13
|
+
}
|
|
14
|
+
function setProxyPath(path) {
|
|
15
|
+
proxyPath = path;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region ../api/dist/runtime/auth.mjs
|
|
19
|
+
/**
|
|
20
|
+
* Module-global bearer token used by the orval-generated client's
|
|
21
|
+
* custom fetch. Set once at command boot (the same lifecycle pattern
|
|
22
|
+
* as `base-url.ts`); every generated request reads from here.
|
|
23
|
+
*
|
|
24
|
+
* Keeping the token here means the CLI doesn't have to thread an
|
|
25
|
+
* `Authorization` header through every generated call site; orval's
|
|
26
|
+
* `mutator` wires `runtime/fetch.ts` in, and that file pulls the token
|
|
27
|
+
* from this module.
|
|
28
|
+
*/
|
|
29
|
+
let apiAuthToken;
|
|
30
|
+
function getApiAuthToken() {
|
|
31
|
+
return apiAuthToken;
|
|
32
|
+
}
|
|
33
|
+
function setApiAuthToken(token) {
|
|
34
|
+
apiAuthToken = token;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region ../api/dist/chunk-CfYAbeIz.mjs
|
|
38
|
+
var __defProp = Object.defineProperty;
|
|
39
|
+
var __exportAll = (all, no_symbols) => {
|
|
40
|
+
let target = {};
|
|
41
|
+
for (var name in all) __defProp(target, name, {
|
|
42
|
+
get: all[name],
|
|
43
|
+
enumerable: true
|
|
44
|
+
});
|
|
45
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
46
|
+
return target;
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region ../api/dist/runtime/fetch.mjs
|
|
50
|
+
/**
|
|
51
|
+
* Framework-neutral failure type the orval-generated client throws on
|
|
52
|
+
* any non-2xx response. Carries the HTTP status, the parsed error body
|
|
53
|
+
* (the spec's `{code, message, details?}` envelope when the server
|
|
54
|
+
* returned one), and the request URL — enough for callers to map to
|
|
55
|
+
* their own error taxonomy without re-implementing the fetch layer.
|
|
56
|
+
*
|
|
57
|
+
* Callers that don't care about the distinction can catch `ApiError`
|
|
58
|
+
* generically; callers that do (the CLI's `toZitadelError`) read
|
|
59
|
+
* `status` and decide.
|
|
60
|
+
*/
|
|
61
|
+
var ApiError = class extends Error {
|
|
62
|
+
status;
|
|
63
|
+
url;
|
|
64
|
+
body;
|
|
65
|
+
constructor(status, url, body, message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "ApiError";
|
|
68
|
+
this.status = status;
|
|
69
|
+
this.url = url;
|
|
70
|
+
this.body = body;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The orval `mutator` for the fetch client. Every generated operation
|
|
75
|
+
* routes its request through this function instead of the global
|
|
76
|
+
* `fetch`. We pin three concerns here so generated call sites stay
|
|
77
|
+
* focused on the shape of one HTTP call:
|
|
78
|
+
*
|
|
79
|
+
* - bearer auth — read from `runtime/auth.ts` and attached automatically;
|
|
80
|
+
* - non-2xx → throw — orval's stock client parses the body regardless
|
|
81
|
+
* of status, so callers would have to inspect every response. Throw
|
|
82
|
+
* `ApiError` on `!res.ok` so failures interrupt control flow;
|
|
83
|
+
* - body parsing — return the parsed JSON typed as `T` (the operation's
|
|
84
|
+
* return type, threaded through by orval), or `undefined` for the
|
|
85
|
+
* spec's `204`/`205`/`304` no-body responses.
|
|
86
|
+
*/
|
|
87
|
+
async function customFetch(url, options) {
|
|
88
|
+
const token = getApiAuthToken();
|
|
89
|
+
const headers = new Headers(options.headers);
|
|
90
|
+
if (token && !headers.has("authorization")) headers.set("authorization", `Bearer ${token}`);
|
|
91
|
+
const res = await fetch(url, {
|
|
92
|
+
...options,
|
|
93
|
+
headers
|
|
94
|
+
});
|
|
95
|
+
const rawBody = [
|
|
96
|
+
204,
|
|
97
|
+
205,
|
|
98
|
+
304
|
|
99
|
+
].includes(res.status) ? "" : await res.text();
|
|
100
|
+
const parsed = rawBody ? safeJsonParse(rawBody) : void 0;
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const message = `${options.method ?? "GET"} ${url} returned ${res.status}`;
|
|
103
|
+
throw new ApiError(res.status, url, parsed, message);
|
|
104
|
+
}
|
|
105
|
+
return parsed;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `JSON.parse` wrapped so a non-JSON body (e.g. an HTML 502 from a
|
|
109
|
+
* proxy) becomes `{ raw: "<text>" }` instead of throwing inside
|
|
110
|
+
* `customFetch`. The thrown `ApiError` then carries something useful
|
|
111
|
+
* for the user to see.
|
|
112
|
+
*/
|
|
113
|
+
function safeJsonParse(text) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(text);
|
|
116
|
+
} catch {
|
|
117
|
+
return { raw: text };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region ../api/dist/generated/endpoints/zitadelNextGen.mjs
|
|
122
|
+
/**
|
|
123
|
+
* Generated by orval v8.10.0 🍺
|
|
124
|
+
* Do not edit manually.
|
|
125
|
+
* Zitadel NextGen
|
|
126
|
+
* This is the next generation of the Zitadel identity platform.
|
|
127
|
+
* OpenAPI spec version: 0.0.1
|
|
128
|
+
*/
|
|
129
|
+
var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
|
|
130
|
+
authorizeDevice: () => authorizeDevice,
|
|
131
|
+
authorizeGet: () => authorizeGet,
|
|
132
|
+
createAuthAttempt: () => createAuthAttempt,
|
|
133
|
+
createFlow: () => createFlow,
|
|
134
|
+
createFlowDefinition: () => createFlowDefinition,
|
|
135
|
+
createHandoff: () => createHandoff,
|
|
136
|
+
createProject: () => createProject,
|
|
137
|
+
createSchema: () => createSchema,
|
|
138
|
+
createSession: () => createSession,
|
|
139
|
+
createTeam: () => createTeam,
|
|
140
|
+
createUser: () => createUser,
|
|
141
|
+
deleteFlowDefinition: () => deleteFlowDefinition,
|
|
142
|
+
endSession: () => endSession,
|
|
143
|
+
exchangeHandoff: () => exchangeHandoff,
|
|
144
|
+
getAuthAttempt: () => getAuthAttempt,
|
|
145
|
+
getAuthorizeDeviceUrl: () => getAuthorizeDeviceUrl,
|
|
146
|
+
getAuthorizeGetUrl: () => getAuthorizeGetUrl,
|
|
147
|
+
getCreateAuthAttemptUrl: () => getCreateAuthAttemptUrl,
|
|
148
|
+
getCreateFlowDefinitionUrl: () => getCreateFlowDefinitionUrl,
|
|
149
|
+
getCreateFlowUrl: () => getCreateFlowUrl,
|
|
150
|
+
getCreateHandoffUrl: () => getCreateHandoffUrl,
|
|
151
|
+
getCreateProjectUrl: () => getCreateProjectUrl,
|
|
152
|
+
getCreateSchemaUrl: () => getCreateSchemaUrl,
|
|
153
|
+
getCreateSessionUrl: () => getCreateSessionUrl,
|
|
154
|
+
getCreateTeamUrl: () => getCreateTeamUrl,
|
|
155
|
+
getCreateUserUrl: () => getCreateUserUrl,
|
|
156
|
+
getDeleteFlowDefinitionUrl: () => getDeleteFlowDefinitionUrl,
|
|
157
|
+
getEndSessionUrl: () => getEndSessionUrl,
|
|
158
|
+
getExchangeHandoffUrl: () => getExchangeHandoffUrl,
|
|
159
|
+
getFlowDefinition: () => getFlowDefinition,
|
|
160
|
+
getFlowStep: () => getFlowStep,
|
|
161
|
+
getGetAuthAttemptUrl: () => getGetAuthAttemptUrl,
|
|
162
|
+
getGetFlowDefinitionUrl: () => getGetFlowDefinitionUrl,
|
|
163
|
+
getGetFlowStepUrl: () => getGetFlowStepUrl,
|
|
164
|
+
getGetHealthUrl: () => getGetHealthUrl,
|
|
165
|
+
getGetKeysUrl: () => getGetKeysUrl,
|
|
166
|
+
getGetLiveUrl: () => getGetLiveUrl,
|
|
167
|
+
getGetMySessionUrl: () => getGetMySessionUrl,
|
|
168
|
+
getGetOpenIDConfigurationUrl: () => getGetOpenIDConfigurationUrl,
|
|
169
|
+
getGetProjectUrl: () => getGetProjectUrl,
|
|
170
|
+
getGetReadyUrl: () => getGetReadyUrl,
|
|
171
|
+
getGetSchemaByIdUrl: () => getGetSchemaByIdUrl,
|
|
172
|
+
getGetSessionUrl: () => getGetSessionUrl,
|
|
173
|
+
getGetTeamUrl: () => getGetTeamUrl,
|
|
174
|
+
getGetTokenUrl: () => getGetTokenUrl,
|
|
175
|
+
getGetUserByIDUrl: () => getGetUserByIDUrl,
|
|
176
|
+
getGetUserInfoUrl: () => getGetUserInfoUrl,
|
|
177
|
+
getHealth: () => getHealth,
|
|
178
|
+
getIntrospectUrl: () => getIntrospectUrl,
|
|
179
|
+
getIssueChallengeUrl: () => getIssueChallengeUrl,
|
|
180
|
+
getKeys: () => getKeys,
|
|
181
|
+
getListFlowDefinitionsUrl: () => getListFlowDefinitionsUrl,
|
|
182
|
+
getListSessionsUrl: () => getListSessionsUrl,
|
|
183
|
+
getListUsersUrl: () => getListUsersUrl,
|
|
184
|
+
getLive: () => getLive,
|
|
185
|
+
getMySession: () => getMySession,
|
|
186
|
+
getOpenIDConfiguration: () => getOpenIDConfiguration,
|
|
187
|
+
getProject: () => getProject,
|
|
188
|
+
getReady: () => getReady,
|
|
189
|
+
getRevokeMySessionUrl: () => getRevokeMySessionUrl,
|
|
190
|
+
getRevokeSessionUrl: () => getRevokeSessionUrl,
|
|
191
|
+
getRevokeTokenUrl: () => getRevokeTokenUrl,
|
|
192
|
+
getSchemaById: () => getSchemaById,
|
|
193
|
+
getSession: () => getSession,
|
|
194
|
+
getSubmitFlowEventUrl: () => getSubmitFlowEventUrl,
|
|
195
|
+
getSubmitFlowStepUrl: () => getSubmitFlowStepUrl,
|
|
196
|
+
getTeam: () => getTeam,
|
|
197
|
+
getToken: () => getToken,
|
|
198
|
+
getUpdateFlowDefinitionUrl: () => getUpdateFlowDefinitionUrl,
|
|
199
|
+
getUserByID: () => getUserByID,
|
|
200
|
+
getUserInfo: () => getUserInfo,
|
|
201
|
+
getVerifyChallengeProofUrl: () => getVerifyChallengeProofUrl,
|
|
202
|
+
introspect: () => introspect,
|
|
203
|
+
issueChallenge: () => issueChallenge,
|
|
204
|
+
listFlowDefinitions: () => listFlowDefinitions,
|
|
205
|
+
listSessions: () => listSessions,
|
|
206
|
+
listUsers: () => listUsers,
|
|
207
|
+
revokeMySession: () => revokeMySession,
|
|
208
|
+
revokeSession: () => revokeSession,
|
|
209
|
+
revokeToken: () => revokeToken,
|
|
210
|
+
submitFlowEvent: () => submitFlowEvent,
|
|
211
|
+
submitFlowStep: () => submitFlowStep,
|
|
212
|
+
updateFlowDefinition: () => updateFlowDefinition,
|
|
213
|
+
verifyChallengeProof: () => verifyChallengeProof
|
|
214
|
+
});
|
|
215
|
+
const getGetOpenIDConfigurationUrl = () => {
|
|
216
|
+
return `${getProxyPath()}/.well-known/openid-configuration`;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Retrieve the OpenID Connect configuration
|
|
220
|
+
* @summary Get OpenID Connect configuration
|
|
221
|
+
*/
|
|
222
|
+
const getOpenIDConfiguration = async (options) => {
|
|
223
|
+
return customFetch(getGetOpenIDConfigurationUrl(), {
|
|
224
|
+
...options,
|
|
225
|
+
method: "GET"
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
const getAuthorizeGetUrl = (params) => {
|
|
229
|
+
const normalizedParams = new URLSearchParams();
|
|
230
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
231
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
232
|
+
});
|
|
233
|
+
const stringifiedParams = normalizedParams.toString();
|
|
234
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/auth/authorize?${stringifiedParams}` : `${getProxyPath()}/auth/authorize`;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* @summary Authorize a user
|
|
238
|
+
*/
|
|
239
|
+
const authorizeGet = async (params, options) => {
|
|
240
|
+
return customFetch(getAuthorizeGetUrl(params), {
|
|
241
|
+
...options,
|
|
242
|
+
method: "GET"
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
const getAuthorizeDeviceUrl = (params) => {
|
|
246
|
+
const normalizedParams = new URLSearchParams();
|
|
247
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
248
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
249
|
+
});
|
|
250
|
+
const stringifiedParams = normalizedParams.toString();
|
|
251
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/auth/device-authorization?${stringifiedParams}` : `${getProxyPath()}/auth/device-authorization`;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* @summary Authorize a device
|
|
255
|
+
*/
|
|
256
|
+
const authorizeDevice = async (params, options) => {
|
|
257
|
+
return customFetch(getAuthorizeDeviceUrl(params), {
|
|
258
|
+
...options,
|
|
259
|
+
method: "GET"
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
const getEndSessionUrl = (params) => {
|
|
263
|
+
const normalizedParams = new URLSearchParams();
|
|
264
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
265
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
266
|
+
});
|
|
267
|
+
const stringifiedParams = normalizedParams.toString();
|
|
268
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/auth/end-session?${stringifiedParams}` : `${getProxyPath()}/auth/end-session`;
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* @summary End a session
|
|
272
|
+
*/
|
|
273
|
+
const endSession = async (params, options) => {
|
|
274
|
+
return customFetch(getEndSessionUrl(params), {
|
|
275
|
+
...options,
|
|
276
|
+
method: "GET"
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
const getIntrospectUrl = () => {
|
|
280
|
+
return `${getProxyPath()}/auth/introspect`;
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* @summary Introspect a token
|
|
284
|
+
*/
|
|
285
|
+
const introspect = async (introspectBody, options) => {
|
|
286
|
+
const formUrlEncoded = new URLSearchParams();
|
|
287
|
+
formUrlEncoded.append(`token`, introspectBody.token);
|
|
288
|
+
if (introspectBody.token_type_hint !== void 0) formUrlEncoded.append(`token_type_hint`, introspectBody.token_type_hint);
|
|
289
|
+
return customFetch(getIntrospectUrl(), {
|
|
290
|
+
...options,
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
294
|
+
...options?.headers
|
|
295
|
+
},
|
|
296
|
+
body: formUrlEncoded
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
const getGetKeysUrl = () => {
|
|
300
|
+
return `${getProxyPath()}/auth/keys`;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* @summary Get public keys
|
|
304
|
+
*/
|
|
305
|
+
const getKeys = async (options) => {
|
|
306
|
+
return customFetch(getGetKeysUrl(), {
|
|
307
|
+
...options,
|
|
308
|
+
method: "GET"
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
const getRevokeTokenUrl = () => {
|
|
312
|
+
return `${getProxyPath()}/auth/revoke`;
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* @summary Revoke an access token or refresh token
|
|
316
|
+
*/
|
|
317
|
+
const revokeToken = async (revokeTokenBody, options) => {
|
|
318
|
+
const formUrlEncoded = new URLSearchParams();
|
|
319
|
+
if (revokeTokenBody.token !== void 0) formUrlEncoded.append(`token`, revokeTokenBody.token);
|
|
320
|
+
if (revokeTokenBody.token_type_hint !== void 0) formUrlEncoded.append(`token_type_hint`, revokeTokenBody.token_type_hint);
|
|
321
|
+
return customFetch(getRevokeTokenUrl(), {
|
|
322
|
+
...options,
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: {
|
|
325
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
326
|
+
...options?.headers
|
|
327
|
+
},
|
|
328
|
+
body: formUrlEncoded
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
const getGetTokenUrl = () => {
|
|
332
|
+
return `${getProxyPath()}/auth/token`;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* @summary Get accesstoken
|
|
336
|
+
*/
|
|
337
|
+
const getToken = async (getTokenBody, options) => {
|
|
338
|
+
const formUrlEncoded = new URLSearchParams();
|
|
339
|
+
if (getTokenBody.code !== void 0) formUrlEncoded.append(`code`, getTokenBody.code);
|
|
340
|
+
if (getTokenBody.client_assertion !== void 0) formUrlEncoded.append(`client_assertion`, getTokenBody.client_assertion);
|
|
341
|
+
if (getTokenBody.client_assertion_type !== void 0) formUrlEncoded.append(`client_assertion_type`, getTokenBody.client_assertion_type);
|
|
342
|
+
if (getTokenBody.client_id !== void 0) formUrlEncoded.append(`client_id`, getTokenBody.client_id);
|
|
343
|
+
if (getTokenBody.client_secret !== void 0) formUrlEncoded.append(`client_secret`, getTokenBody.client_secret);
|
|
344
|
+
if (getTokenBody.code_verifier !== void 0) formUrlEncoded.append(`code_verifier`, getTokenBody.code_verifier);
|
|
345
|
+
if (getTokenBody.grant_type !== void 0) formUrlEncoded.append(`grant_type`, getTokenBody.grant_type);
|
|
346
|
+
if (getTokenBody.redirect_uri !== void 0) formUrlEncoded.append(`redirect_uri`, getTokenBody.redirect_uri);
|
|
347
|
+
return customFetch(getGetTokenUrl(), {
|
|
348
|
+
...options,
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: {
|
|
351
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
352
|
+
...options?.headers
|
|
353
|
+
},
|
|
354
|
+
body: formUrlEncoded
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
const getGetUserInfoUrl = () => {
|
|
358
|
+
return `${getProxyPath()}/auth/userinfo`;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* @summary Get user info
|
|
362
|
+
*/
|
|
363
|
+
const getUserInfo = async (options) => {
|
|
364
|
+
return customFetch(getGetUserInfoUrl(), {
|
|
365
|
+
...options,
|
|
366
|
+
method: "GET"
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
const getGetHealthUrl = () => {
|
|
370
|
+
return `${getProxyPath()}/healthz`;
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Check whether the server is healthy
|
|
374
|
+
* @summary Check server health
|
|
375
|
+
*/
|
|
376
|
+
const getHealth = async (options) => {
|
|
377
|
+
return customFetch(getGetHealthUrl(), {
|
|
378
|
+
...options,
|
|
379
|
+
method: "GET"
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
const getGetLiveUrl = () => {
|
|
383
|
+
return `${getProxyPath()}/livez`;
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Check whether the server is started
|
|
387
|
+
* @summary Check server liveness
|
|
388
|
+
*/
|
|
389
|
+
const getLive = async (options) => {
|
|
390
|
+
return customFetch(getGetLiveUrl(), {
|
|
391
|
+
...options,
|
|
392
|
+
method: "GET"
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
const getGetReadyUrl = () => {
|
|
396
|
+
return `${getProxyPath()}/readyz`;
|
|
397
|
+
};
|
|
398
|
+
/**
|
|
399
|
+
* Check whether the server is ready to accept requests
|
|
400
|
+
* @summary Check server readiness
|
|
401
|
+
*/
|
|
402
|
+
const getReady = async (options) => {
|
|
403
|
+
return customFetch(getGetReadyUrl(), {
|
|
404
|
+
...options,
|
|
405
|
+
method: "GET"
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
const getCreateUserUrl = (params) => {
|
|
409
|
+
const normalizedParams = new URLSearchParams();
|
|
410
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
411
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
412
|
+
});
|
|
413
|
+
const stringifiedParams = normalizedParams.toString();
|
|
414
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/user?${stringifiedParams}` : `${getProxyPath()}/user`;
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* @summary Create user
|
|
418
|
+
*/
|
|
419
|
+
const createUser = async (createUserBody, params, options) => {
|
|
420
|
+
return customFetch(getCreateUserUrl(params), {
|
|
421
|
+
...options,
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: {
|
|
424
|
+
"Content-Type": "application/json",
|
|
425
|
+
...options?.headers
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify(createUserBody)
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
const getListUsersUrl = (params) => {
|
|
431
|
+
const normalizedParams = new URLSearchParams();
|
|
432
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
433
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
434
|
+
});
|
|
435
|
+
const stringifiedParams = normalizedParams.toString();
|
|
436
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/user?${stringifiedParams}` : `${getProxyPath()}/user`;
|
|
437
|
+
};
|
|
438
|
+
/**
|
|
439
|
+
* @summary List users
|
|
440
|
+
*/
|
|
441
|
+
const listUsers = async (params, options) => {
|
|
442
|
+
return customFetch(getListUsersUrl(params), {
|
|
443
|
+
...options,
|
|
444
|
+
method: "GET"
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
const getGetUserByIDUrl = (userId, params) => {
|
|
448
|
+
const normalizedParams = new URLSearchParams();
|
|
449
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
450
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
451
|
+
});
|
|
452
|
+
const stringifiedParams = normalizedParams.toString();
|
|
453
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/user/${userId}?${stringifiedParams}` : `${getProxyPath()}/user/${userId}`;
|
|
454
|
+
};
|
|
455
|
+
/**
|
|
456
|
+
* @summary Get user by ID
|
|
457
|
+
*/
|
|
458
|
+
const getUserByID = async (userId, params, options) => {
|
|
459
|
+
return customFetch(getGetUserByIDUrl(userId, params), {
|
|
460
|
+
...options,
|
|
461
|
+
method: "GET"
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
const getCreateFlowUrl = () => {
|
|
465
|
+
return `${getProxyPath()}/flow`;
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* Resolves a flow definition based on purpose + audience context and returns
|
|
469
|
+
the first capability step. Creates a new session implicitly unless
|
|
470
|
+
`session_id` is provided (for step-up / reauth on an existing session).
|
|
471
|
+
|
|
472
|
+
The response contains an `id` field — the flow handle. Use it as the path
|
|
473
|
+
parameter for all subsequent `/flow/{id}/submit` and `/flow/{id}/event` calls.
|
|
474
|
+
|
|
475
|
+
The response also sets an encrypted `HttpOnly` cookie (`_zflow`) containing
|
|
476
|
+
the flow's orchestration state (current step, collected data, history).
|
|
477
|
+
The server is stateless between requests — all flow state lives in this
|
|
478
|
+
cookie. The browser sends it automatically on subsequent requests.
|
|
479
|
+
|
|
480
|
+
* @summary Start a new flow
|
|
481
|
+
*/
|
|
482
|
+
const createFlow = async (createFlowBody, options) => {
|
|
483
|
+
return customFetch(getCreateFlowUrl(), {
|
|
484
|
+
...options,
|
|
485
|
+
method: "POST",
|
|
486
|
+
headers: {
|
|
487
|
+
"Content-Type": "application/json",
|
|
488
|
+
...options?.headers
|
|
489
|
+
},
|
|
490
|
+
body: JSON.stringify(createFlowBody)
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
const getGetFlowStepUrl = (id) => {
|
|
494
|
+
return `${getProxyPath()}/flow/${id}`;
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* Returns the current capability step without advancing the state machine.
|
|
498
|
+
Useful for page reloads or re-rendering after a network error.
|
|
499
|
+
|
|
500
|
+
* @summary Get current step (re-render)
|
|
501
|
+
*/
|
|
502
|
+
const getFlowStep = async (id, options) => {
|
|
503
|
+
return customFetch(getGetFlowStepUrl(id), {
|
|
504
|
+
...options,
|
|
505
|
+
method: "GET"
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
const getSubmitFlowStepUrl = (id) => {
|
|
509
|
+
return `${getProxyPath()}/flow/${id}/submit`;
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* Submits user input for the current step. The server validates,
|
|
513
|
+
processes (e.g., verifies a credential), advances the state machine
|
|
514
|
+
through any invisible steps, and returns the next visible step.
|
|
515
|
+
|
|
516
|
+
The response sets an updated encrypted `HttpOnly` cookie (`_zflow`)
|
|
517
|
+
with the new flow state. The server is stateless — all orchestration
|
|
518
|
+
state is carried in this cookie between requests.
|
|
519
|
+
|
|
520
|
+
**Important:** The `id` in the response may differ from the `id` used in
|
|
521
|
+
the request. This happens when a flow pivots (pushes a new flow onto the
|
|
522
|
+
stack) or when a stacked flow completes (auto-pops to the parent flow).
|
|
523
|
+
Always use the `id` from the latest response for the next request.
|
|
524
|
+
|
|
525
|
+
## Flow completion
|
|
526
|
+
|
|
527
|
+
When `step.type` is `complete`, the flow is terminal. The `step.behavior`
|
|
528
|
+
field tells the frontend what to do:
|
|
529
|
+
|
|
530
|
+
| `behavior` | Action |
|
|
531
|
+
|------------- |------------------------------------------------------------|
|
|
532
|
+
| `redirect` | Navigate to `redirect_uri` (OIDC/SAML auth request done). |
|
|
533
|
+
| `show` | Render the step as a success screen (e.g., registration). |
|
|
534
|
+
|
|
535
|
+
A `complete` step is only returned when the **entire flow stack** is done.
|
|
536
|
+
If a stacked flow (e.g., recovery pivoted from login) finishes, the server
|
|
537
|
+
auto-pops to the parent flow and returns the parent's next step — the
|
|
538
|
+
frontend never sees a `complete` for intermediate flows.
|
|
539
|
+
|
|
540
|
+
* @summary Submit step data and advance
|
|
541
|
+
*/
|
|
542
|
+
const submitFlowStep = async (id, submitFlowStepBody, options) => {
|
|
543
|
+
return customFetch(getSubmitFlowStepUrl(id), {
|
|
544
|
+
...options,
|
|
545
|
+
method: "POST",
|
|
546
|
+
headers: {
|
|
547
|
+
"Content-Type": "application/json",
|
|
548
|
+
...options?.headers
|
|
549
|
+
},
|
|
550
|
+
body: JSON.stringify(submitFlowStepBody)
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
const getSubmitFlowEventUrl = (id) => {
|
|
554
|
+
return `${getProxyPath()}/flow/${id}/event`;
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Submits telemetry or fingerprint data from the frontend.
|
|
558
|
+
Does not advance the state machine. Used for risk evaluation.
|
|
559
|
+
|
|
560
|
+
* @summary Submit client-side event
|
|
561
|
+
*/
|
|
562
|
+
const submitFlowEvent = async (id, submitFlowEventBody, options) => {
|
|
563
|
+
return customFetch(getSubmitFlowEventUrl(id), {
|
|
564
|
+
...options,
|
|
565
|
+
method: "POST",
|
|
566
|
+
headers: {
|
|
567
|
+
"Content-Type": "application/json",
|
|
568
|
+
...options?.headers
|
|
569
|
+
},
|
|
570
|
+
body: JSON.stringify(submitFlowEventBody)
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
const getCreateAuthAttemptUrl = () => {
|
|
574
|
+
return `${getProxyPath()}/auth_attempts`;
|
|
575
|
+
};
|
|
576
|
+
/**
|
|
577
|
+
* Starts a new authentication attempt. This is the entry point for the auth_attempts state machine.
|
|
578
|
+
|
|
579
|
+
An attempt is an ephemeral (15-minute TTL) state machine that drives a single authentication round.
|
|
580
|
+
It accepts factor challenges, verifies proofs, and completes into a session or handoff token.
|
|
581
|
+
|
|
582
|
+
Accepts a project_id and challenge_nonce (from POST /bootstrap/challenge). For step-up re-auth,
|
|
583
|
+
also include session_id to add factors to an existing session.
|
|
584
|
+
|
|
585
|
+
* @summary Create a new authentication attempt
|
|
586
|
+
*/
|
|
587
|
+
const createAuthAttempt = async (createAuthAttemptBody, options) => {
|
|
588
|
+
return customFetch(getCreateAuthAttemptUrl(), {
|
|
589
|
+
...options,
|
|
590
|
+
method: "POST",
|
|
591
|
+
headers: {
|
|
592
|
+
"Content-Type": "application/json",
|
|
593
|
+
...options?.headers
|
|
594
|
+
},
|
|
595
|
+
body: JSON.stringify(createAuthAttemptBody)
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
const getGetAuthAttemptUrl = (attemptId) => {
|
|
599
|
+
return `${getProxyPath()}/auth_attempts/${attemptId}`;
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* Polls the current state of an authentication attempt.
|
|
603
|
+
|
|
604
|
+
Returns the attempt's state, available factors for the next challenge,
|
|
605
|
+
challenges issued so far, and any errors preventing progress.
|
|
606
|
+
|
|
607
|
+
Use this for polling during long-running factor verifications (e.g., waiting for
|
|
608
|
+
a federated IdP callback or a device flow).
|
|
609
|
+
|
|
610
|
+
* @summary Get authentication attempt state
|
|
611
|
+
*/
|
|
612
|
+
const getAuthAttempt = async (attemptId, options) => {
|
|
613
|
+
return customFetch(getGetAuthAttemptUrl(attemptId), {
|
|
614
|
+
...options,
|
|
615
|
+
method: "GET"
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
const getIssueChallengeUrl = (attemptId) => {
|
|
619
|
+
return `${getProxyPath()}/auth_attempts/${attemptId}/challenges`;
|
|
620
|
+
};
|
|
621
|
+
/**
|
|
622
|
+
* Issues a single-factor verification challenge within an auth attempt.
|
|
623
|
+
|
|
624
|
+
This advances the authentication state machine by requesting a specific factor method
|
|
625
|
+
(password, passkey, TOTP, OTP via SMS, etc.). The server responds with challenge details
|
|
626
|
+
including method, metadata, and any UI hints. The client then verifies the proof
|
|
627
|
+
by calling POST /auth_attempts/{attempt_id}/challenges/{challenge_id}/verify.
|
|
628
|
+
|
|
629
|
+
* @summary Issue a factor challenge
|
|
630
|
+
*/
|
|
631
|
+
const issueChallenge = async (attemptId, issueChallengeBody, options) => {
|
|
632
|
+
return customFetch(getIssueChallengeUrl(attemptId), {
|
|
633
|
+
...options,
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: {
|
|
636
|
+
"Content-Type": "application/json",
|
|
637
|
+
...options?.headers
|
|
638
|
+
},
|
|
639
|
+
body: JSON.stringify(issueChallengeBody)
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
const getVerifyChallengeProofUrl = (attemptId, challengeId) => {
|
|
643
|
+
return `${getProxyPath()}/auth_attempts/${attemptId}/challenges/${challengeId}/verify`;
|
|
644
|
+
};
|
|
645
|
+
/**
|
|
646
|
+
* Submits a proof (credential, code, assertion) to verify a factor challenge.
|
|
647
|
+
|
|
648
|
+
The proof format depends on the challenge method. For example:
|
|
649
|
+
- `password` method: { password: "…" }
|
|
650
|
+
- `totp` method: { totp: { code: "123456" } }
|
|
651
|
+
- `passkey` method: { passkey: { assertion: "…" } }
|
|
652
|
+
- `recovery_code` method: { recovery_code: "…" }
|
|
653
|
+
|
|
654
|
+
On successful verification, the factor is written to the auth attempt.
|
|
655
|
+
The attempt moves to the next pending challenge or completes if all required factors are verified.
|
|
656
|
+
|
|
657
|
+
* @summary Verify a factor proof
|
|
658
|
+
*/
|
|
659
|
+
const verifyChallengeProof = async (attemptId, challengeId, verifyChallengeProofBody, options) => {
|
|
660
|
+
return customFetch(getVerifyChallengeProofUrl(attemptId, challengeId), {
|
|
661
|
+
...options,
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: {
|
|
664
|
+
"Content-Type": "application/json",
|
|
665
|
+
...options?.headers
|
|
666
|
+
},
|
|
667
|
+
body: JSON.stringify(verifyChallengeProofBody)
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
const getCreateHandoffUrl = (attemptId) => {
|
|
671
|
+
return `${getProxyPath()}/auth_attempts/${attemptId}/handoff`;
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Completes the authentication attempt and mints a `handoff_token`.
|
|
675
|
+
|
|
676
|
+
Call this after all required factors have been verified and the attempt is in `completed` state.
|
|
677
|
+
The handoff token is short-lived (≤60 seconds) and must be exchanged at
|
|
678
|
+
POST /sessions/exchange to receive the final session and session_token.
|
|
679
|
+
|
|
680
|
+
The handoff token is:
|
|
681
|
+
- Single-use (atomic exchange, no retry)
|
|
682
|
+
- Audience-bound (requires matching project key for exchange)
|
|
683
|
+
- Idempotency-safe within a 5-minute window (see conventions)
|
|
684
|
+
|
|
685
|
+
* @summary Complete authentication and create handoff token
|
|
686
|
+
*/
|
|
687
|
+
const createHandoff = async (attemptId, options) => {
|
|
688
|
+
return customFetch(getCreateHandoffUrl(attemptId), {
|
|
689
|
+
...options,
|
|
690
|
+
method: "POST"
|
|
691
|
+
});
|
|
692
|
+
};
|
|
693
|
+
const getCreateProjectUrl = () => {
|
|
694
|
+
return `${getProxyPath()}/projects`;
|
|
695
|
+
};
|
|
696
|
+
/**
|
|
697
|
+
* @summary Create project
|
|
698
|
+
*/
|
|
699
|
+
const createProject = async (createProjectBody, options) => {
|
|
700
|
+
return customFetch(getCreateProjectUrl(), {
|
|
701
|
+
...options,
|
|
702
|
+
method: "POST",
|
|
703
|
+
headers: {
|
|
704
|
+
"Content-Type": "application/json",
|
|
705
|
+
...options?.headers
|
|
706
|
+
},
|
|
707
|
+
body: JSON.stringify(createProjectBody)
|
|
708
|
+
});
|
|
709
|
+
};
|
|
710
|
+
const getGetProjectUrl = (projectId) => {
|
|
711
|
+
return `${getProxyPath()}/projects/${projectId}`;
|
|
712
|
+
};
|
|
713
|
+
/**
|
|
714
|
+
* Returns the current state of a project.
|
|
715
|
+
|
|
716
|
+
* @summary Get project
|
|
717
|
+
*/
|
|
718
|
+
const getProject = async (projectId, options) => {
|
|
719
|
+
return customFetch(getGetProjectUrl(projectId), {
|
|
720
|
+
...options,
|
|
721
|
+
method: "GET"
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
const getCreateSessionUrl = () => {
|
|
725
|
+
return `${getProxyPath()}/sessions`;
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* Creates an anonymous session shell with no user and no factors (`state: building`).
|
|
729
|
+
|
|
730
|
+
This is optional — an `auth_attempt` will create a session implicitly if none is provided.
|
|
731
|
+
Use this explicitly when you want to:
|
|
732
|
+
- Pre-allocate a `session_id` before the user is known, so device/telemetry signals
|
|
733
|
+
can be correlated with the eventual authenticated session from the start.
|
|
734
|
+
- Track anonymous state (bot detection, device fingerprint) that survives until authentication.
|
|
735
|
+
|
|
736
|
+
The returned `session_token` authorises GET and DELETE on this session.
|
|
737
|
+
It is superseded when a handoff exchange completes — clients must replace it at that point.
|
|
738
|
+
|
|
739
|
+
Anonymous sessions expire aggressively (10-minute TTL). The TTL resets to the configured
|
|
740
|
+
full session TTL when the first authentication factor is written via a completing `auth_attempt`.
|
|
741
|
+
|
|
742
|
+
* @summary Create an anonymous session shell
|
|
743
|
+
*/
|
|
744
|
+
const createSession = async (createSessionBody, options) => {
|
|
745
|
+
return customFetch(getCreateSessionUrl(), {
|
|
746
|
+
...options,
|
|
747
|
+
method: "POST",
|
|
748
|
+
headers: {
|
|
749
|
+
"Content-Type": "application/json",
|
|
750
|
+
...options?.headers
|
|
751
|
+
},
|
|
752
|
+
body: JSON.stringify(createSessionBody)
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
const getListSessionsUrl = (params) => {
|
|
756
|
+
const normalizedParams = new URLSearchParams();
|
|
757
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
758
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
759
|
+
});
|
|
760
|
+
const stringifiedParams = normalizedParams.toString();
|
|
761
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/sessions?${stringifiedParams}` : `${getProxyPath()}/sessions`;
|
|
762
|
+
};
|
|
763
|
+
/**
|
|
764
|
+
* Returns a paginated list of sessions for a project.
|
|
765
|
+
Requires a project service key (OAuth2 client credentials).
|
|
766
|
+
|
|
767
|
+
* @summary List sessions
|
|
768
|
+
*/
|
|
769
|
+
const listSessions = async (params, options) => {
|
|
770
|
+
return customFetch(getListSessionsUrl(params), {
|
|
771
|
+
...options,
|
|
772
|
+
method: "GET"
|
|
773
|
+
});
|
|
774
|
+
};
|
|
775
|
+
const getExchangeHandoffUrl = (params) => {
|
|
776
|
+
const normalizedParams = new URLSearchParams();
|
|
777
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
778
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
779
|
+
});
|
|
780
|
+
const stringifiedParams = normalizedParams.toString();
|
|
781
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/sessions/exchange?${stringifiedParams}` : `${getProxyPath()}/sessions/exchange`;
|
|
782
|
+
};
|
|
783
|
+
/**
|
|
784
|
+
* Consumes a one-time `handoff_token` minted by `POST /auth_attempts/{id}/handoff`
|
|
785
|
+
and returns the resulting session and a `session_token`.
|
|
786
|
+
|
|
787
|
+
The server resolves the originating `auth_attempt` from the token and then:
|
|
788
|
+
|
|
789
|
+
| Originating auth_attempt | Outcome |
|
|
790
|
+
|---|---|
|
|
791
|
+
| No `session_id` | A new authenticated session is **created**. |
|
|
792
|
+
| `session_id` points to an anonymous shell | Existing session is **upgraded** — user and factors written in, TTL reset to full session TTL. |
|
|
793
|
+
| `session_id` points to an active session (step-up) | Existing session is **upgraded** — new factors merged, `assurance_levels[]` expanded. |
|
|
794
|
+
|
|
795
|
+
The response shape is identical in all three cases.
|
|
796
|
+
|
|
797
|
+
The `session_token` supersedes any previously issued `session_token` for the same session.
|
|
798
|
+
Clients must replace their stored token at this point.
|
|
799
|
+
|
|
800
|
+
Requires a project service key (OAuth2 client credentials).
|
|
801
|
+
|
|
802
|
+
* @summary Exchange handoff token for a session
|
|
803
|
+
*/
|
|
804
|
+
const exchangeHandoff = async (exchangeHandoffBody, params, options) => {
|
|
805
|
+
return customFetch(getExchangeHandoffUrl(params), {
|
|
806
|
+
...options,
|
|
807
|
+
method: "POST",
|
|
808
|
+
headers: {
|
|
809
|
+
"Content-Type": "application/json",
|
|
810
|
+
...options?.headers
|
|
811
|
+
},
|
|
812
|
+
body: JSON.stringify(exchangeHandoffBody)
|
|
813
|
+
});
|
|
814
|
+
};
|
|
815
|
+
const getGetSessionUrl = (sessionId, params) => {
|
|
816
|
+
const normalizedParams = new URLSearchParams();
|
|
817
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
818
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
819
|
+
});
|
|
820
|
+
const stringifiedParams = normalizedParams.toString();
|
|
821
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/sessions/${sessionId}?${stringifiedParams}` : `${getProxyPath()}/sessions/${sessionId}`;
|
|
822
|
+
};
|
|
823
|
+
/**
|
|
824
|
+
* Returns the current state of a session including its factors and all currently
|
|
825
|
+
satisfied assurance levels.
|
|
826
|
+
|
|
827
|
+
`assurance_levels[]` may shrink over time as factor freshness windows expire,
|
|
828
|
+
without the session itself expiring. Use step-up authentication (a new `auth_attempt`
|
|
829
|
+
against the same `session_id`) to restore a dropped assurance level.
|
|
830
|
+
|
|
831
|
+
* @summary Get session state
|
|
832
|
+
*/
|
|
833
|
+
const getSession = async (sessionId, params, options) => {
|
|
834
|
+
return customFetch(getGetSessionUrl(sessionId, params), {
|
|
835
|
+
...options,
|
|
836
|
+
method: "GET"
|
|
837
|
+
});
|
|
838
|
+
};
|
|
839
|
+
const getRevokeSessionUrl = (sessionId, params) => {
|
|
840
|
+
const normalizedParams = new URLSearchParams();
|
|
841
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
842
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
843
|
+
});
|
|
844
|
+
const stringifiedParams = normalizedParams.toString();
|
|
845
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/sessions/${sessionId}?${stringifiedParams}` : `${getProxyPath()}/sessions/${sessionId}`;
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* Revokes the session immediately (`state: revoked`). This is the logout operation.
|
|
849
|
+
|
|
850
|
+
The session_token issued at creation (or superseded by a handoff exchange) is required.
|
|
851
|
+
After revocation, any tokens derived from this session are invalidated.
|
|
852
|
+
|
|
853
|
+
* @summary Revoke session
|
|
854
|
+
*/
|
|
855
|
+
const revokeSession = async (sessionId, params, options) => {
|
|
856
|
+
return customFetch(getRevokeSessionUrl(sessionId, params), {
|
|
857
|
+
...options,
|
|
858
|
+
method: "DELETE"
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
const getGetMySessionUrl = () => {
|
|
862
|
+
return `${getProxyPath()}/sessions/me`;
|
|
863
|
+
};
|
|
864
|
+
/**
|
|
865
|
+
* Returns the current state of the current session including its factors and all currently
|
|
866
|
+
satisfied assurance levels.
|
|
867
|
+
|
|
868
|
+
`assurance_levels[]` may shrink over time as factor freshness windows expire,
|
|
869
|
+
without the session itself expiring. Use step-up authentication (a new `auth_attempt`
|
|
870
|
+
against the same `session_id`) to restore a dropped assurance level.
|
|
871
|
+
|
|
872
|
+
* @summary Get my session state
|
|
873
|
+
*/
|
|
874
|
+
const getMySession = async (options) => {
|
|
875
|
+
return customFetch(getGetMySessionUrl(), {
|
|
876
|
+
...options,
|
|
877
|
+
method: "GET"
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
const getRevokeMySessionUrl = () => {
|
|
881
|
+
return `${getProxyPath()}/sessions/me`;
|
|
882
|
+
};
|
|
883
|
+
/**
|
|
884
|
+
* Revokes the session immediately (`state: revoked`). This is the logout operation.
|
|
885
|
+
|
|
886
|
+
The __nextgen_session cookie issued at creation (or superseded by a handoff exchange) is required.
|
|
887
|
+
After revocation, any tokens derived from this session are invalidated including the cookie itself, which is cleared in the response.
|
|
888
|
+
|
|
889
|
+
* @summary Revoke my session
|
|
890
|
+
*/
|
|
891
|
+
const revokeMySession = async (options) => {
|
|
892
|
+
return customFetch(getRevokeMySessionUrl(), {
|
|
893
|
+
...options,
|
|
894
|
+
method: "DELETE"
|
|
895
|
+
});
|
|
896
|
+
};
|
|
897
|
+
const getCreateSchemaUrl = (params) => {
|
|
898
|
+
const normalizedParams = new URLSearchParams();
|
|
899
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
900
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
901
|
+
});
|
|
902
|
+
const stringifiedParams = normalizedParams.toString();
|
|
903
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/schemas?${stringifiedParams}` : `${getProxyPath()}/schemas`;
|
|
904
|
+
};
|
|
905
|
+
/**
|
|
906
|
+
* Create a new schema. The schema definition must include a unique $id field,
|
|
907
|
+
which will be used to identify the schema in future requests. The $id must
|
|
908
|
+
be a valid URI and should ideally point to the location where the schema
|
|
909
|
+
can be accessed.
|
|
910
|
+
|
|
911
|
+
The schema can either be a concrete schema, e.g. a user schema, or a
|
|
912
|
+
schema-url which will be resolved by the server.
|
|
913
|
+
|
|
914
|
+
* @summary Create new schema
|
|
915
|
+
*/
|
|
916
|
+
const createSchema = async (createSchemaBody, params, options) => {
|
|
917
|
+
return customFetch(getCreateSchemaUrl(params), {
|
|
918
|
+
...options,
|
|
919
|
+
method: "POST",
|
|
920
|
+
headers: {
|
|
921
|
+
"Content-Type": "application/json",
|
|
922
|
+
...options?.headers
|
|
923
|
+
},
|
|
924
|
+
body: JSON.stringify(createSchemaBody)
|
|
925
|
+
});
|
|
926
|
+
};
|
|
927
|
+
const getGetSchemaByIdUrl = (id, params) => {
|
|
928
|
+
const normalizedParams = new URLSearchParams();
|
|
929
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
930
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
931
|
+
});
|
|
932
|
+
const stringifiedParams = normalizedParams.toString();
|
|
933
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/schemas/${id}?${stringifiedParams}` : `${getProxyPath()}/schemas/${id}`;
|
|
934
|
+
};
|
|
935
|
+
/**
|
|
936
|
+
* Get a schema by its ID. This will return the default revision of the schema.
|
|
937
|
+
* @summary Get schema by ID
|
|
938
|
+
*/
|
|
939
|
+
const getSchemaById = async (id, params, options) => {
|
|
940
|
+
return customFetch(getGetSchemaByIdUrl(id, params), {
|
|
941
|
+
...options,
|
|
942
|
+
method: "GET"
|
|
943
|
+
});
|
|
944
|
+
};
|
|
945
|
+
const getCreateFlowDefinitionUrl = () => {
|
|
946
|
+
return `${getProxyPath()}/flow_definitions`;
|
|
947
|
+
};
|
|
948
|
+
/**
|
|
949
|
+
* Creates a new flow definition.
|
|
950
|
+
Flow definitions are templates that define the sequence of steps (capabilities)
|
|
951
|
+
for a particular user journey (e.g., registration, login, password reset).
|
|
952
|
+
|
|
953
|
+
Flow definitions are created based on the flow meta schema, which includes the flow's purpose, audience, and the steps involved.
|
|
954
|
+
|
|
955
|
+
* @summary Create a new flow definition
|
|
956
|
+
*/
|
|
957
|
+
const createFlowDefinition = async (createFlowDefinitionBody, options) => {
|
|
958
|
+
return customFetch(getCreateFlowDefinitionUrl(), {
|
|
959
|
+
...options,
|
|
960
|
+
method: "POST",
|
|
961
|
+
headers: {
|
|
962
|
+
"Content-Type": "application/json",
|
|
963
|
+
...options?.headers
|
|
964
|
+
},
|
|
965
|
+
body: JSON.stringify(createFlowDefinitionBody)
|
|
966
|
+
});
|
|
967
|
+
};
|
|
968
|
+
const getListFlowDefinitionsUrl = (params) => {
|
|
969
|
+
const normalizedParams = new URLSearchParams();
|
|
970
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
971
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
972
|
+
});
|
|
973
|
+
const stringifiedParams = normalizedParams.toString();
|
|
974
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/flow_definitions?${stringifiedParams}` : `${getProxyPath()}/flow_definitions`;
|
|
975
|
+
};
|
|
976
|
+
/**
|
|
977
|
+
* Retrieves a list of all flow definitions.
|
|
978
|
+
This endpoint can be used to view existing flow definitions and their configurations.
|
|
979
|
+
|
|
980
|
+
* @summary List flow definitions
|
|
981
|
+
*/
|
|
982
|
+
const listFlowDefinitions = async (params, options) => {
|
|
983
|
+
return customFetch(getListFlowDefinitionsUrl(params), {
|
|
984
|
+
...options,
|
|
985
|
+
method: "GET"
|
|
986
|
+
});
|
|
987
|
+
};
|
|
988
|
+
const getGetFlowDefinitionUrl = (id) => {
|
|
989
|
+
return `${getProxyPath()}/flow_definitions/${id}`;
|
|
990
|
+
};
|
|
991
|
+
/**
|
|
992
|
+
* Get a flow definition by id
|
|
993
|
+
* @summary Get a flow definition by id
|
|
994
|
+
*/
|
|
995
|
+
const getFlowDefinition = async (id, options) => {
|
|
996
|
+
return customFetch(getGetFlowDefinitionUrl(id), {
|
|
997
|
+
...options,
|
|
998
|
+
method: "GET"
|
|
999
|
+
});
|
|
1000
|
+
};
|
|
1001
|
+
const getUpdateFlowDefinitionUrl = (id) => {
|
|
1002
|
+
return `${getProxyPath()}/flow_definitions/${id}`;
|
|
1003
|
+
};
|
|
1004
|
+
/**
|
|
1005
|
+
* Update a flow definition by id
|
|
1006
|
+
* @summary Update a flow definition by id
|
|
1007
|
+
*/
|
|
1008
|
+
const updateFlowDefinition = async (id, updateFlowDefinitionBody, options) => {
|
|
1009
|
+
return customFetch(getUpdateFlowDefinitionUrl(id), {
|
|
1010
|
+
...options,
|
|
1011
|
+
method: "PATCH",
|
|
1012
|
+
headers: {
|
|
1013
|
+
"Content-Type": "application/json",
|
|
1014
|
+
...options?.headers
|
|
1015
|
+
},
|
|
1016
|
+
body: JSON.stringify(updateFlowDefinitionBody)
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
1019
|
+
const getDeleteFlowDefinitionUrl = (id) => {
|
|
1020
|
+
return `${getProxyPath()}/flow_definitions/${id}`;
|
|
1021
|
+
};
|
|
1022
|
+
/**
|
|
1023
|
+
* Delete a flow definition by id
|
|
1024
|
+
* @summary Delete a flow definition by id
|
|
1025
|
+
*/
|
|
1026
|
+
const deleteFlowDefinition = async (id, options) => {
|
|
1027
|
+
return customFetch(getDeleteFlowDefinitionUrl(id), {
|
|
1028
|
+
...options,
|
|
1029
|
+
method: "DELETE"
|
|
1030
|
+
});
|
|
1031
|
+
};
|
|
1032
|
+
const getCreateTeamUrl = (params) => {
|
|
1033
|
+
const normalizedParams = new URLSearchParams();
|
|
1034
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
1035
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
1036
|
+
});
|
|
1037
|
+
const stringifiedParams = normalizedParams.toString();
|
|
1038
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/teams?${stringifiedParams}` : `${getProxyPath()}/teams`;
|
|
1039
|
+
};
|
|
1040
|
+
/**
|
|
1041
|
+
* @summary Create team
|
|
1042
|
+
*/
|
|
1043
|
+
const createTeam = async (params, createTeamBody, options) => {
|
|
1044
|
+
return customFetch(getCreateTeamUrl(params), {
|
|
1045
|
+
...options,
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
headers: {
|
|
1048
|
+
"Content-Type": "application/json",
|
|
1049
|
+
...options?.headers
|
|
1050
|
+
},
|
|
1051
|
+
body: JSON.stringify(createTeamBody)
|
|
1052
|
+
});
|
|
1053
|
+
};
|
|
1054
|
+
const getGetTeamUrl = (teamId, params) => {
|
|
1055
|
+
const normalizedParams = new URLSearchParams();
|
|
1056
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
1057
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
1058
|
+
});
|
|
1059
|
+
const stringifiedParams = normalizedParams.toString();
|
|
1060
|
+
return stringifiedParams.length > 0 ? `${getProxyPath()}/teams/${teamId}?${stringifiedParams}` : `${getProxyPath()}/teams/${teamId}`;
|
|
1061
|
+
};
|
|
1062
|
+
/**
|
|
1063
|
+
* Returns the current state of a team.
|
|
1064
|
+
|
|
1065
|
+
* @summary Get team
|
|
1066
|
+
*/
|
|
1067
|
+
const getTeam = async (teamId, params, options) => {
|
|
1068
|
+
return customFetch(getGetTeamUrl(teamId, params), {
|
|
1069
|
+
...options,
|
|
1070
|
+
method: "GET"
|
|
1071
|
+
});
|
|
1072
|
+
};
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region ../api/dist/runtime/api-factory.mjs
|
|
1075
|
+
/**
|
|
1076
|
+
* Factory for the typed Zitadel client every consumer reaches for.
|
|
1077
|
+
*
|
|
1078
|
+
* Wraps the orval-generated endpoints in a {@link Proxy} so each
|
|
1079
|
+
* generated function sees the right base URL and (optionally) the
|
|
1080
|
+
* right bearer token at call time — without the caller having to
|
|
1081
|
+
* thread either through every operation, and without exposing the
|
|
1082
|
+
* module-globals (`setProxyPath` / `setApiAuthToken`) as part of the
|
|
1083
|
+
* public API.
|
|
1084
|
+
*
|
|
1085
|
+
* Per-instance isolation: multiple clients can coexist in one process
|
|
1086
|
+
* (different servers, different tokens). Their Proxy `get` traps set
|
|
1087
|
+
* the globals synchronously before invoking the underlying generated
|
|
1088
|
+
* function, which reads them at the top of `customFetch` before any
|
|
1089
|
+
* `await`. Two clients running interleaved within a single JS turn
|
|
1090
|
+
* are safe; two clients running in parallel across awaits could
|
|
1091
|
+
* clobber each other — not a pattern any current consumer uses.
|
|
1092
|
+
*/
|
|
1093
|
+
/**
|
|
1094
|
+
* Build a typed Zitadel client pre-bound to a base URL and (optionally)
|
|
1095
|
+
* a bearer token. Every method on the returned object mirrors a
|
|
1096
|
+
* generated orval function:
|
|
1097
|
+
*
|
|
1098
|
+
* const client = createZitadelClient({ baseUrl, token });
|
|
1099
|
+
* await client.createProject({ previewOrigins: [] });
|
|
1100
|
+
* await client.createSchema(body, { project_id });
|
|
1101
|
+
*/
|
|
1102
|
+
function createZitadelClient(opts) {
|
|
1103
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
1104
|
+
return new Proxy(zitadelNextGen_exports, { get(target, prop, receiver) {
|
|
1105
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1106
|
+
if (typeof value !== "function") return value;
|
|
1107
|
+
return (...args) => {
|
|
1108
|
+
setProxyPath(baseUrl);
|
|
1109
|
+
setApiAuthToken(opts.token);
|
|
1110
|
+
return value(...args);
|
|
1111
|
+
};
|
|
1112
|
+
} });
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Legacy entry point: builds a client with only the base URL set, no
|
|
1116
|
+
* token. Components and SDKs that don't carry an auth token (browser
|
|
1117
|
+
* flows authenticated by platform cookies) still consume this name.
|
|
1118
|
+
* Equivalent to `createZitadelClient({ baseUrl: apiBase })`.
|
|
1119
|
+
*/
|
|
1120
|
+
function createApi(apiBase) {
|
|
1121
|
+
return createZitadelClient({ baseUrl: apiBase });
|
|
1122
|
+
}
|
|
1123
|
+
//#endregion
|
|
1124
|
+
//#region ../api/dist/runtime/config.mjs
|
|
1125
|
+
let currentProject = null;
|
|
1126
|
+
/**
|
|
1127
|
+
* Per-project API client cache. Ensures `getApi(project)` returns the
|
|
1128
|
+
* same instance for the same project handle — no re-wrapping on every call.
|
|
1129
|
+
*/
|
|
1130
|
+
const apiCache = /* @__PURE__ */ new WeakMap();
|
|
1131
|
+
/**
|
|
1132
|
+
* Returns a typed API client for the given project, with the base URL
|
|
1133
|
+
* pre-bound. Cached per project handle — safe to call multiple times.
|
|
1134
|
+
*
|
|
1135
|
+
* ```ts
|
|
1136
|
+
* const project = configureZitadel({ ... });
|
|
1137
|
+
* export const api = getApi(project);
|
|
1138
|
+
* ```
|
|
1139
|
+
*/
|
|
1140
|
+
function getApi(project) {
|
|
1141
|
+
let api = apiCache.get(project);
|
|
1142
|
+
if (!api) {
|
|
1143
|
+
api = createApi(project.proxyPath);
|
|
1144
|
+
apiCache.set(project, api);
|
|
1145
|
+
}
|
|
1146
|
+
return api;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Returns the current project handle, or `null` if {@link configureZitadel}
|
|
1150
|
+
* has not been called yet.
|
|
1151
|
+
*/
|
|
1152
|
+
function getZitadelConfig() {
|
|
1153
|
+
return currentProject;
|
|
1154
|
+
}
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/orchestrator/api-client.ts
|
|
1157
|
+
const apiRequestInit = { credentials: "include" };
|
|
1158
|
+
async function startFlow(api, input) {
|
|
1159
|
+
return api.createFlow(input, apiRequestInit);
|
|
1160
|
+
}
|
|
1161
|
+
async function submitStep(api, id, body) {
|
|
1162
|
+
try {
|
|
1163
|
+
return await api.submitFlowStep(id, body, apiRequestInit);
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
if (error instanceof ApiError && error.status === 400 && isFlowResponse(error.body)) return error.body;
|
|
1166
|
+
throw error;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function isFlowResponse(body) {
|
|
1170
|
+
return typeof body === "object" && body !== null && "step" in body;
|
|
1171
|
+
}
|
|
1172
|
+
async function getCurrentStep(api, id) {
|
|
1173
|
+
return api.getFlowStep(id, apiRequestInit);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Exchange a terminal-flow `handoff_token` for an authenticated session.
|
|
1177
|
+
* The server sets the `__nextgen_session` HttpOnly cookie on success.
|
|
1178
|
+
*
|
|
1179
|
+
* Uses the generated `exchangeHandoff` client which handles the
|
|
1180
|
+
* `project_id` query parameter via {@link ExchangeHandoffParams}.
|
|
1181
|
+
*/
|
|
1182
|
+
async function exchangeSession(api, body, params) {
|
|
1183
|
+
return api.exchangeHandoff(body, params, apiRequestInit);
|
|
1184
|
+
}
|
|
1185
|
+
//#endregion
|
|
1186
|
+
//#region src/orchestrator/branding-to-tokens.ts
|
|
1187
|
+
/**
|
|
1188
|
+
* Branding -> CSS token bridge.
|
|
1189
|
+
*
|
|
1190
|
+
* Per `docs/design/branding/README.md` the orchestrator owns theming. Templates
|
|
1191
|
+
* never emit `<style>` blocks; they only structure HTML. This module translates
|
|
1192
|
+
* the `Branding` JSON into a CSSStyleSheet of `:host { --zl-* }` declarations
|
|
1193
|
+
* and applies it via `shadowRoot.adoptedStyleSheets`.
|
|
1194
|
+
*
|
|
1195
|
+
* Dark-mode overrides are emitted as `:host([data-theme="dark"]) { ... }`
|
|
1196
|
+
* (matching `docs/design/branding/tokens.md`). Resolution between
|
|
1197
|
+
* `light | dark | auto` happens in `<zitadel-login>`.
|
|
1198
|
+
*
|
|
1199
|
+
* Base layer: the design-tokens package ships the full `--zl-*` set as both a
|
|
1200
|
+
* `.css` file (for host pages) and a `tokensCss` string (for shadow roots and
|
|
1201
|
+
* SSR). We adopt the string into every orchestrator shadow root so atoms
|
|
1202
|
+
* paint correctly even when the host page didn't `@import` tokens.css — the
|
|
1203
|
+
* orchestrator is meant to drop into any page.
|
|
1204
|
+
*/
|
|
1205
|
+
const RADIUS_MAP = {
|
|
1206
|
+
none: "0",
|
|
1207
|
+
sm: "0.25rem",
|
|
1208
|
+
md: "0.5rem",
|
|
1209
|
+
lg: "0.75rem",
|
|
1210
|
+
full: "9999px"
|
|
1211
|
+
};
|
|
1212
|
+
const DENSITY_MAP = {
|
|
1213
|
+
compact: {
|
|
1214
|
+
"--zl-spacing-03": "0.75rem",
|
|
1215
|
+
"--zl-spacing-05": "1.5rem"
|
|
1216
|
+
},
|
|
1217
|
+
regular: {},
|
|
1218
|
+
comfortable: {
|
|
1219
|
+
"--zl-spacing-03": "1.25rem",
|
|
1220
|
+
"--zl-spacing-05": "2.25rem"
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const PALETTE_MAP = {
|
|
1224
|
+
primary: ["--zl-color-surface-default-white"],
|
|
1225
|
+
on_primary: ["--zl-color-text-button-default"],
|
|
1226
|
+
background: ["--zl-color-surface-default-black"],
|
|
1227
|
+
surface: ["--zl-color-surface-default-primary-gray"],
|
|
1228
|
+
muted: ["--zl-color-surface-default-secondary-gray"],
|
|
1229
|
+
border: ["--zl-color-border-default-gray-200", "--zl-color-border-default-gray-100"],
|
|
1230
|
+
text: ["--zl-color-text-primary-white"],
|
|
1231
|
+
text_muted: ["--zl-color-text-secondary-gray"],
|
|
1232
|
+
link: ["--zl-color-text-subtitle-pink"],
|
|
1233
|
+
success: [
|
|
1234
|
+
"--zl-color-text-success",
|
|
1235
|
+
"--zl-color-border-success",
|
|
1236
|
+
"--zl-color-icon-success"
|
|
1237
|
+
],
|
|
1238
|
+
warning: ["--zl-color-text-subtitle-orange"],
|
|
1239
|
+
error: [
|
|
1240
|
+
"--zl-color-text-error",
|
|
1241
|
+
"--zl-color-border-error",
|
|
1242
|
+
"--zl-color-icon-error"
|
|
1243
|
+
]
|
|
1244
|
+
};
|
|
1245
|
+
/**
|
|
1246
|
+
* Build a CSS string of `:host { --zl-* }` declarations from a Branding
|
|
1247
|
+
* payload. Light values land on `:host`; dark overrides land on
|
|
1248
|
+
* `:host([data-theme="dark"])`.
|
|
1249
|
+
*/
|
|
1250
|
+
function buildBrandingStylesheet(branding, options = {}) {
|
|
1251
|
+
const lightDecls = collectDeclarations(branding);
|
|
1252
|
+
const darkPalette = branding?.theme?.dark?.palette;
|
|
1253
|
+
const darkDecls = darkPalette ? mapPalette(darkPalette) : {};
|
|
1254
|
+
const blocks = [];
|
|
1255
|
+
if (Object.keys(lightDecls).length > 0) blocks.push(formatBlock(":host", lightDecls));
|
|
1256
|
+
if (Object.keys(darkDecls).length > 0) blocks.push(formatBlock(":host([data-theme=\"dark\"])", darkDecls));
|
|
1257
|
+
if (options.resolvedTheme === "dark" && darkPalette) {
|
|
1258
|
+
const merged = {
|
|
1259
|
+
...lightDecls,
|
|
1260
|
+
...darkDecls
|
|
1261
|
+
};
|
|
1262
|
+
blocks.length = 0;
|
|
1263
|
+
blocks.push(formatBlock(":host", merged));
|
|
1264
|
+
}
|
|
1265
|
+
return blocks.join("\n");
|
|
1266
|
+
}
|
|
1267
|
+
function collectDeclarations(branding) {
|
|
1268
|
+
if (!branding) return {};
|
|
1269
|
+
const decls = {};
|
|
1270
|
+
Object.assign(decls, mapPalette(branding.palette));
|
|
1271
|
+
Object.assign(decls, mapTypography(branding.typography));
|
|
1272
|
+
Object.assign(decls, mapShape(branding.shape));
|
|
1273
|
+
return decls;
|
|
1274
|
+
}
|
|
1275
|
+
function mapPalette(palette) {
|
|
1276
|
+
if (!palette) return {};
|
|
1277
|
+
const out = {};
|
|
1278
|
+
for (const [key, varNames] of Object.entries(PALETTE_MAP)) {
|
|
1279
|
+
const value = palette[key];
|
|
1280
|
+
if (typeof value === "string" && value.length > 0) for (const varName of varNames) out[varName] = value;
|
|
1281
|
+
}
|
|
1282
|
+
return out;
|
|
1283
|
+
}
|
|
1284
|
+
function mapTypography(typography) {
|
|
1285
|
+
if (!typography) return {};
|
|
1286
|
+
const out = {};
|
|
1287
|
+
if (typography.font_family) {
|
|
1288
|
+
out["--zl-font-family-sans"] = typography.font_family;
|
|
1289
|
+
out["--zl-font-family-heading"] = typography.font_family;
|
|
1290
|
+
}
|
|
1291
|
+
if (typography.font_family_mono) out["--zl-font-family-mono"] = typography.font_family_mono;
|
|
1292
|
+
const scale = clamp(typography.scale ?? 1, .75, 1.25);
|
|
1293
|
+
if (scale !== 1) out["--zl-font-scale"] = `${scale}`;
|
|
1294
|
+
return out;
|
|
1295
|
+
}
|
|
1296
|
+
function mapShape(shape) {
|
|
1297
|
+
if (!shape) return {};
|
|
1298
|
+
const out = {};
|
|
1299
|
+
if (shape.radius && RADIUS_MAP[shape.radius]) {
|
|
1300
|
+
const radius = RADIUS_MAP[shape.radius];
|
|
1301
|
+
out["--zl-radius-s"] = radius;
|
|
1302
|
+
out["--zl-radius-m"] = radius === "0" ? "0" : `calc(${radius} * 1.5)`;
|
|
1303
|
+
out["--zl-radius-l"] = radius === "0" ? "0" : `calc(${radius} * 2)`;
|
|
1304
|
+
}
|
|
1305
|
+
if (shape.density) Object.assign(out, DENSITY_MAP[shape.density]);
|
|
1306
|
+
return out;
|
|
1307
|
+
}
|
|
1308
|
+
function formatBlock(selector, decls) {
|
|
1309
|
+
return `${selector} {\n${Object.entries(decls).map(([prop, value]) => ` ${prop}: ${value};`).join("\n")}\n}`;
|
|
1310
|
+
}
|
|
1311
|
+
function clamp(value, min, max) {
|
|
1312
|
+
if (Number.isNaN(value)) return min;
|
|
1313
|
+
return Math.min(max, Math.max(min, value));
|
|
1314
|
+
}
|
|
1315
|
+
let baseTokenSheet;
|
|
1316
|
+
function getBaseTokenSheet() {
|
|
1317
|
+
if (typeof CSSStyleSheet === "undefined") return void 0;
|
|
1318
|
+
if (!baseTokenSheet) {
|
|
1319
|
+
baseTokenSheet = new CSSStyleSheet();
|
|
1320
|
+
baseTokenSheet.replaceSync(tokensCss.replaceAll(":root,\n[data-theme=\"dark\"]", ":host,\n:host([data-theme=\"dark\"])").replaceAll("[data-theme=\"light\"]", ":host([data-theme=\"light\"])"));
|
|
1321
|
+
}
|
|
1322
|
+
return baseTokenSheet;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Adopt the design-system base token layer onto a `ShadowRoot`. Safe to call
|
|
1326
|
+
* many times — the underlying constructable sheet is shared across every
|
|
1327
|
+
* orchestrator instance and de-duplicated in the adopted list.
|
|
1328
|
+
*/
|
|
1329
|
+
function applyBaseTokens(shadowRoot) {
|
|
1330
|
+
const sheet = getBaseTokenSheet();
|
|
1331
|
+
if (!sheet) return;
|
|
1332
|
+
const existing = Array.isArray(shadowRoot.adoptedStyleSheets) ? shadowRoot.adoptedStyleSheets : [];
|
|
1333
|
+
if (existing.includes(sheet)) return;
|
|
1334
|
+
try {
|
|
1335
|
+
shadowRoot.adoptedStyleSheets = [sheet, ...existing];
|
|
1336
|
+
} catch {}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Apply a branding payload as a `--zl-*` overrides layer on top of the base
|
|
1340
|
+
* token sheet. Subsequent calls replace the previous override sheet so a new
|
|
1341
|
+
* branding payload paints cleanly without leaking older declarations.
|
|
1342
|
+
*
|
|
1343
|
+
* Callers should run `applyBaseTokens(shadowRoot)` first (once per shadow
|
|
1344
|
+
* root) so the base values exist before branding patches them.
|
|
1345
|
+
*/
|
|
1346
|
+
function applyBrandingTokens(shadowRoot, branding, resolvedTheme) {
|
|
1347
|
+
const css = buildBrandingStylesheet(branding, { resolvedTheme });
|
|
1348
|
+
if (typeof CSSStyleSheet === "undefined") return;
|
|
1349
|
+
const sheet = new CSSStyleSheet();
|
|
1350
|
+
sheet.replaceSync(css);
|
|
1351
|
+
const previous = shadowRoot.__zlTokenSheet;
|
|
1352
|
+
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets.filter((s) => s !== previous), sheet];
|
|
1353
|
+
shadowRoot.__zlTokenSheet = sheet;
|
|
1354
|
+
}
|
|
1355
|
+
function resolveTheme(branding) {
|
|
1356
|
+
const mode = branding?.theme?.mode ?? "dark";
|
|
1357
|
+
if (mode === "light") return "light";
|
|
1358
|
+
if (mode === "dark") return "dark";
|
|
1359
|
+
if (typeof matchMedia === "function") return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
1360
|
+
return "dark";
|
|
1361
|
+
}
|
|
1362
|
+
//#endregion
|
|
1363
|
+
//#region ../api/dist/generated/model/index.mjs
|
|
1364
|
+
const CreateFlow201BrandingLayout = {
|
|
1365
|
+
centered: "centered",
|
|
1366
|
+
split: "split"
|
|
1367
|
+
};
|
|
1368
|
+
//#endregion
|
|
1369
|
+
//#region src/orchestrator/branding-validator.ts
|
|
1370
|
+
/**
|
|
1371
|
+
* Lightweight paint-time validator for `Branding` payloads.
|
|
1372
|
+
*
|
|
1373
|
+
* Per `docs/design/branding/schema.md` §Shape invariants:
|
|
1374
|
+
*
|
|
1375
|
+
* 1. Every referenced URL is https.
|
|
1376
|
+
* 2. `layout` is in the documented enum.
|
|
1377
|
+
* 3. `liquid_template` shape checks happen elsewhere (security pipeline runs
|
|
1378
|
+
* server-side; structural validator is deferred until the full atom set
|
|
1379
|
+
* lands).
|
|
1380
|
+
*
|
|
1381
|
+
* Failures are non-fatal: we collect issues, log a dev-build warning, and
|
|
1382
|
+
* return a sanitised payload with the offending fields stripped (set to
|
|
1383
|
+
* `undefined`) so the orchestrator falls back to the bundled defaults.
|
|
1384
|
+
*/
|
|
1385
|
+
const VALID_LAYOUTS = new Set(Object.values(CreateFlow201BrandingLayout));
|
|
1386
|
+
function validateBranding(input) {
|
|
1387
|
+
if (!input) return {
|
|
1388
|
+
branding: void 0,
|
|
1389
|
+
issues: []
|
|
1390
|
+
};
|
|
1391
|
+
const issues = [];
|
|
1392
|
+
const out = { ...input };
|
|
1393
|
+
if (out.layout && !VALID_LAYOUTS.has(out.layout)) {
|
|
1394
|
+
issues.push(`Unknown layout "${out.layout}" — falling back to "centered".`);
|
|
1395
|
+
out.layout = CreateFlow201BrandingLayout.centered;
|
|
1396
|
+
}
|
|
1397
|
+
out.logo_url = sanitiseUrl(out.logo_url, "logo_url", issues);
|
|
1398
|
+
out.font_url = sanitiseUrl(out.font_url, "font_url", issues);
|
|
1399
|
+
out.hero_url = sanitiseUrl(out.hero_url, "hero_url", issues);
|
|
1400
|
+
if (out.assets) out.assets = {
|
|
1401
|
+
logo_dark: sanitiseUrl(out.assets.logo_dark, "assets.logo_dark", issues),
|
|
1402
|
+
favicon: sanitiseUrl(out.assets.favicon, "assets.favicon", issues),
|
|
1403
|
+
background_image: sanitiseUrl(out.assets.background_image, "assets.background_image", issues)
|
|
1404
|
+
};
|
|
1405
|
+
return {
|
|
1406
|
+
branding: out,
|
|
1407
|
+
issues
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function sanitiseUrl(value, field, issues) {
|
|
1411
|
+
if (value == null || value === "") return;
|
|
1412
|
+
try {
|
|
1413
|
+
const parsed = new URL(value);
|
|
1414
|
+
if (parsed.protocol !== "https:") {
|
|
1415
|
+
issues.push(`${field} must use https (got "${parsed.protocol}") — dropped.`);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
return parsed.toString();
|
|
1419
|
+
} catch {
|
|
1420
|
+
issues.push(`${field} is not a valid URL — dropped.`);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/orchestrator/font-loader.ts
|
|
1426
|
+
/**
|
|
1427
|
+
* Inject `branding.font_url` as `<link rel="stylesheet">` inside the
|
|
1428
|
+
* `<zitadel-login>` Shadow DOM. Per `docs/design/branding/templates.md`:
|
|
1429
|
+
*
|
|
1430
|
+
* "The `font_url` stylesheet is injected by the orchestrator; the template
|
|
1431
|
+
* does not emit the `<link>` tag itself."
|
|
1432
|
+
*
|
|
1433
|
+
* Idempotent: calling with the same URL is a no-op; calling with a new URL
|
|
1434
|
+
* replaces the previous link. Calling with `null`/`undefined` removes any
|
|
1435
|
+
* previously injected link.
|
|
1436
|
+
*/
|
|
1437
|
+
const LINK_ID = "zl-font-link";
|
|
1438
|
+
function applyFontUrl(shadowRoot, fontUrl) {
|
|
1439
|
+
const existing = shadowRoot.getElementById?.(LINK_ID);
|
|
1440
|
+
if (!fontUrl) {
|
|
1441
|
+
existing?.remove();
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (existing && existing.href === fontUrl) return;
|
|
1445
|
+
existing?.remove();
|
|
1446
|
+
const link = (shadowRoot.ownerDocument ?? document).createElement("link");
|
|
1447
|
+
link.id = LINK_ID;
|
|
1448
|
+
link.rel = "stylesheet";
|
|
1449
|
+
link.href = fontUrl;
|
|
1450
|
+
shadowRoot.prepend(link);
|
|
1451
|
+
}
|
|
1452
|
+
//#endregion
|
|
1453
|
+
//#region src/orchestrator/mandatory-gates.ts
|
|
1454
|
+
const MANDATORY_GATES_MARKER = "ZL_MANDATORY_GATES";
|
|
1455
|
+
const mandatoryGatesMarkerComment = `<!--${MANDATORY_GATES_MARKER}-->`;
|
|
1456
|
+
function patchMandatoryGates(html, step, locale) {
|
|
1457
|
+
const template = document.createElement("template");
|
|
1458
|
+
template.innerHTML = html;
|
|
1459
|
+
const fragment = template.content;
|
|
1460
|
+
const additions = collectMissingAtoms(fragment, step, locale);
|
|
1461
|
+
const marker = findMarkerComment(fragment);
|
|
1462
|
+
if (marker?.parentNode) {
|
|
1463
|
+
for (const node of additions) marker.parentNode.insertBefore(node, marker);
|
|
1464
|
+
marker.remove();
|
|
1465
|
+
} else for (const node of additions) fragment.appendChild(node);
|
|
1466
|
+
return template.innerHTML;
|
|
1467
|
+
}
|
|
1468
|
+
function collectMissingAtoms(fragment, step, locale) {
|
|
1469
|
+
const additions = [];
|
|
1470
|
+
if (step.fields) for (const [name, field] of Object.entries(step.fields)) {
|
|
1471
|
+
if (!field.required) continue;
|
|
1472
|
+
if (hasFieldFor(fragment, name)) continue;
|
|
1473
|
+
additions.push(buildField(name, field.text_key, field.type, locale, field.required));
|
|
1474
|
+
}
|
|
1475
|
+
if (step.actions && !hasPrimaryButton(fragment)) {
|
|
1476
|
+
const primary = Object.entries(step.actions).find(([, action]) => action.primary);
|
|
1477
|
+
if (primary) {
|
|
1478
|
+
const [name, action] = primary;
|
|
1479
|
+
additions.push(buildSubmit(name, action.text_key, locale));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return additions;
|
|
1483
|
+
}
|
|
1484
|
+
function hasPrimaryButton(fragment) {
|
|
1485
|
+
return Boolean(fragment.querySelector("zl-button[hierarchy=\"primary\"]"));
|
|
1486
|
+
}
|
|
1487
|
+
function hasFieldFor(fragment, name) {
|
|
1488
|
+
for (const field of fragment.querySelectorAll("zl-field")) if (field.getAttribute("name") === name) return true;
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
function findMarkerComment(fragment) {
|
|
1492
|
+
const walker = (fragment.ownerDocument ?? document).createTreeWalker(fragment, NodeFilter.SHOW_COMMENT);
|
|
1493
|
+
let node = walker.nextNode();
|
|
1494
|
+
while (node) {
|
|
1495
|
+
if (node.nodeValue?.trim() === "ZL_MANDATORY_GATES") return node;
|
|
1496
|
+
node = walker.nextNode();
|
|
1497
|
+
}
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
function buildField(name, textKey, type, locale, required) {
|
|
1501
|
+
const el = document.createElement("zl-field");
|
|
1502
|
+
el.setAttribute("name", name);
|
|
1503
|
+
el.setAttribute("label", lookup(locale, textKey ?? name));
|
|
1504
|
+
el.setAttribute("type", type);
|
|
1505
|
+
if (required) el.setAttribute("required", "");
|
|
1506
|
+
return el;
|
|
1507
|
+
}
|
|
1508
|
+
function buildSubmit(name, textKey, locale) {
|
|
1509
|
+
const el = document.createElement("zl-button");
|
|
1510
|
+
el.setAttribute("hierarchy", "primary");
|
|
1511
|
+
el.setAttribute("size", "medium");
|
|
1512
|
+
el.setAttribute("type", "submit");
|
|
1513
|
+
el.setAttribute("block", "");
|
|
1514
|
+
el.setAttribute("action", name);
|
|
1515
|
+
el.setAttribute("label", lookup(locale, textKey ?? "submit.continue"));
|
|
1516
|
+
return el;
|
|
1517
|
+
}
|
|
1518
|
+
function lookup(locale, key) {
|
|
1519
|
+
return locale[key] ?? key;
|
|
1520
|
+
}
|
|
1521
|
+
//#endregion
|
|
1522
|
+
//#region src/orchestrator/templates/auth-form.liquid.ts
|
|
1523
|
+
/**
|
|
1524
|
+
* `auth-form` partial — Sign in and Sign up screens (Figma screens file).
|
|
1525
|
+
*
|
|
1526
|
+
* Sign up 2xl frame: `6593:141741` (card `6593:141743`, attribution `6593:141767`).
|
|
1527
|
+
* Sign in 2xl frame: `6593:141983`, card `6593:141985`, stack `6593:141989`
|
|
1528
|
+
* (email + password + forgot link + CTAs on one step).
|
|
1529
|
+
* Sign-in errors: wrong credentials `6602:180268`, server alert `6594:125237`.
|
|
1530
|
+
*
|
|
1531
|
+
* Figma `Field+CTAs` stacks fields + CTAs in one column at 16px (`spacing-03`).
|
|
1532
|
+
* Primary/secondary are 48px buttons; register / sign_in are body copy with a
|
|
1533
|
+
* purple text link (24px row) — not `<zl-button hierarchy="text">`.
|
|
1534
|
+
*/
|
|
1535
|
+
const authFormTemplate = String.raw`
|
|
1536
|
+
<zl-page-shell data-zl-template-root>
|
|
1537
|
+
{% if branding.logo_url %}
|
|
1538
|
+
<img slot="header" class="zl-card-logo" src="{{ branding.logo_url }}" alt="" />
|
|
1539
|
+
{% endif %}
|
|
1540
|
+
<zl-card>
|
|
1541
|
+
<h1 slot="header" class="zl-card-title">{{ step.texts.title_key | t: identity.display_name }}</h1>
|
|
1542
|
+
{% if step.texts.description_key %}
|
|
1543
|
+
<p slot="header" class="zl-card-subtitle">{{ step.texts.description_key | t: identity.display_name }}</p>
|
|
1544
|
+
{% endif %}
|
|
1545
|
+
|
|
1546
|
+
{% if errors and errors.size > 0 %}
|
|
1547
|
+
{% for err in errors %}
|
|
1548
|
+
{% if err | formLevelError %}
|
|
1549
|
+
{% assign alert_heading = err.text_key | alertHeading %}
|
|
1550
|
+
{% assign alert_body = err.text_key | alertBody %}
|
|
1551
|
+
<zl-alert severity="error"{% if alert_heading != "" %} heading="{{ alert_heading | t }}"{% endif %}>
|
|
1552
|
+
{% if alert_body != "" %}{{ alert_body | t }}{% elsif err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}
|
|
1553
|
+
</zl-alert>
|
|
1554
|
+
{% endif %}
|
|
1555
|
+
{% endfor %}
|
|
1556
|
+
{% endif %}
|
|
1557
|
+
|
|
1558
|
+
{% for entry in fields %}
|
|
1559
|
+
{% assign field_placeholder = entry[1].text_key | fieldPlaceholder %}
|
|
1560
|
+
{% assign field_help = entry[1].text_key | fieldHelp %}
|
|
1561
|
+
{% assign field_err = entry[0] | fieldError: errors %}
|
|
1562
|
+
{% assign autocomplete_val = '' %}
|
|
1563
|
+
{% if entry[0] == 'email' %}
|
|
1564
|
+
{% assign autocomplete_val = 'email' %}
|
|
1565
|
+
{% elsif entry[1].type == 'password' and step.name == 'register' %}
|
|
1566
|
+
{% assign autocomplete_val = 'new-password' %}
|
|
1567
|
+
{% elsif entry[1].type == 'password' %}
|
|
1568
|
+
{% assign autocomplete_val = 'current-password' %}
|
|
1569
|
+
{% endif %}
|
|
1570
|
+
<zl-field
|
|
1571
|
+
name="{{ entry[0] }}"
|
|
1572
|
+
label="{{ entry[1].text_key | t }}"
|
|
1573
|
+
type="{{ entry[1].type }}"
|
|
1574
|
+
value="{{ entry[1].value }}"
|
|
1575
|
+
{% if autocomplete_val != '' %}autocomplete="{{ autocomplete_val }}"{% endif %}
|
|
1576
|
+
{% if entry[1].required %}required{% endif %}
|
|
1577
|
+
{% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
|
|
1578
|
+
{% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
|
|
1579
|
+
>
|
|
1580
|
+
{% if field_help != "" %}
|
|
1581
|
+
<span slot="help">{{ field_help }}</span>
|
|
1582
|
+
{% endif %}
|
|
1583
|
+
</zl-field>
|
|
1584
|
+
{% endfor %}
|
|
1585
|
+
|
|
1586
|
+
{% if fields.email and fields.password and step.name == 'identifier' %}
|
|
1587
|
+
<p class="zl-card-forgot">
|
|
1588
|
+
<a href="#" class="zl-card-forgot__link" data-action="recover">{{ 'action.forgot_password' | t }}</a>
|
|
1589
|
+
</p>
|
|
1590
|
+
{% endif %}
|
|
1591
|
+
|
|
1592
|
+
{% for entry in actions %}
|
|
1593
|
+
{% if entry[1].primary %}
|
|
1594
|
+
<zl-button
|
|
1595
|
+
hierarchy="primary"
|
|
1596
|
+
size="medium"
|
|
1597
|
+
type="submit"
|
|
1598
|
+
block
|
|
1599
|
+
action="{{ entry[0] }}"
|
|
1600
|
+
label="{{ entry[1].text_key | t }}"
|
|
1601
|
+
{% if loading %}loading{% endif %}
|
|
1602
|
+
></zl-button>
|
|
1603
|
+
{% endif %}
|
|
1604
|
+
{% endfor %}
|
|
1605
|
+
|
|
1606
|
+
{% if actions.passkey %}
|
|
1607
|
+
<zl-button
|
|
1608
|
+
hierarchy="secondary"
|
|
1609
|
+
size="medium"
|
|
1610
|
+
block
|
|
1611
|
+
action="passkey"
|
|
1612
|
+
label="{{ actions.passkey.text_key | t }}"
|
|
1613
|
+
></zl-button>
|
|
1614
|
+
{% endif %}
|
|
1615
|
+
|
|
1616
|
+
{% if actions.register %}
|
|
1617
|
+
<p class="zl-card-nav">
|
|
1618
|
+
{{ 'identifier.action.register.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="register">{{ 'identifier.action.register.link' | t }}</a>
|
|
1619
|
+
</p>
|
|
1620
|
+
{% endif %}
|
|
1621
|
+
|
|
1622
|
+
{% if actions.sign_in %}
|
|
1623
|
+
<p class="zl-card-nav">
|
|
1624
|
+
{{ 'register.action.sign_in.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="sign_in">{{ 'register.action.sign_in.link' | t }}</a>
|
|
1625
|
+
</p>
|
|
1626
|
+
{% endif %}
|
|
1627
|
+
|
|
1628
|
+
{% if challenge and challenge.method == "passkey" %}
|
|
1629
|
+
<zl-passkey
|
|
1630
|
+
ceremony="{{ challenge.options.ceremony | default: 'authenticate' }}"
|
|
1631
|
+
challenge-id="{{ challenge.challenge_id }}"
|
|
1632
|
+
options='{{ challenge.options | json }}'
|
|
1633
|
+
></zl-passkey>
|
|
1634
|
+
{% endif %}
|
|
1635
|
+
|
|
1636
|
+
{% mandatory_gates %}
|
|
1637
|
+
</zl-card>
|
|
1638
|
+
</zl-page-shell>
|
|
1639
|
+
`;
|
|
1640
|
+
//#endregion
|
|
1641
|
+
//#region src/orchestrator/templates/default.liquid.ts
|
|
1642
|
+
/**
|
|
1643
|
+
* Bundled `default.liquid` master template.
|
|
1644
|
+
*
|
|
1645
|
+
* Renders the four target Figma auth screens (Sign in, Sign up, Passkey
|
|
1646
|
+
* upsell, Signed in) by including the appropriate partial based on
|
|
1647
|
+
* `step.name`. Tenant-specific layouts (`branding.layout`) replace the
|
|
1648
|
+
* outer chrome only — the per-step partials stay shared so atom output
|
|
1649
|
+
* tracks the design system without each tenant re-implementing it.
|
|
1650
|
+
*
|
|
1651
|
+
* Spec: docs/design/branding/templates.md.
|
|
1652
|
+
*/
|
|
1653
|
+
const defaultTemplate = String.raw`
|
|
1654
|
+
{%- if step.name == 'signed-in' or step.complete == 'show' -%}
|
|
1655
|
+
{%- include 'signed-in' -%}
|
|
1656
|
+
{%- elsif step.name == 'passkey' or step.name == 'passkey-upsell' or step.name == 'passkey-setup' or step.name == 'passkey-login' -%}
|
|
1657
|
+
{%- include 'passkey-upsell' -%}
|
|
1658
|
+
{%- else -%}
|
|
1659
|
+
{%- include 'auth-form' -%}
|
|
1660
|
+
{%- endif -%}
|
|
1661
|
+
`;
|
|
1662
|
+
/**
|
|
1663
|
+
* Layout chrome injected as an `adoptedStyleSheet` on the orchestrator's
|
|
1664
|
+
* shadow root. Provides the page background and the attribution-footer
|
|
1665
|
+
* positioning that lives *outside* any template — the rest of the visual
|
|
1666
|
+
* surface is owned by the `<zl-page-shell>` / `<zl-card>` atoms the
|
|
1667
|
+
* templates render. Token references resolve against the
|
|
1668
|
+
* `@zitadel/design-tokens` CSS variables that are present on
|
|
1669
|
+
* `:host` via the branding stylesheet.
|
|
1670
|
+
*/
|
|
1671
|
+
const layoutChromeCss = `
|
|
1672
|
+
:host {
|
|
1673
|
+
display: block;
|
|
1674
|
+
min-height: 100%;
|
|
1675
|
+
background: var(--zl-color-surface-default-black, #0f0f11);
|
|
1676
|
+
color: var(--zl-color-text-primary-white, #f4f4f6);
|
|
1677
|
+
font-family: var(--zl-font-family-sans, system-ui, sans-serif);
|
|
1678
|
+
}
|
|
1679
|
+
.zl-mount {
|
|
1680
|
+
display: flex;
|
|
1681
|
+
flex-direction: column;
|
|
1682
|
+
min-height: 100vh;
|
|
1683
|
+
background: inherit;
|
|
1684
|
+
color: inherit;
|
|
1685
|
+
}
|
|
1686
|
+
.zl-mount > zl-page-shell,
|
|
1687
|
+
.zl-mount > [data-zl-template-root] {
|
|
1688
|
+
flex: 1 1 auto;
|
|
1689
|
+
}
|
|
1690
|
+
.zl-attribution {
|
|
1691
|
+
display: flex;
|
|
1692
|
+
justify-content: center;
|
|
1693
|
+
align-items: center;
|
|
1694
|
+
background: transparent;
|
|
1695
|
+
}
|
|
1696
|
+
.zl-attribution[hidden] {
|
|
1697
|
+
display: none;
|
|
1698
|
+
}
|
|
1699
|
+
/* Card header typography — Figma "Desktop/Headings/Small" + "Body Small
|
|
1700
|
+
Regular" pattern shared by sign-in (6593:141740), passkey upsell
|
|
1701
|
+
(6594:89142 / 6594:12796), and signed-in (6596:132845). Templates
|
|
1702
|
+
slot raw <h1>/<p> elements into <zl-card>, which keeps them in the
|
|
1703
|
+
light DOM where the orchestrator's stylesheet can reach them — the
|
|
1704
|
+
atom intentionally doesn't dictate typography. */
|
|
1705
|
+
.zl-card-title {
|
|
1706
|
+
margin: 0;
|
|
1707
|
+
font-family: var(--zl-font-family-heading, var(--zl-font-family-sans, system-ui, sans-serif));
|
|
1708
|
+
font-size: 2rem;
|
|
1709
|
+
font-weight: 400;
|
|
1710
|
+
line-height: 2.5rem;
|
|
1711
|
+
letter-spacing: -0.02em;
|
|
1712
|
+
color: var(--zl-color-text-primary-white, #f4f4f6);
|
|
1713
|
+
text-align: left;
|
|
1714
|
+
}
|
|
1715
|
+
.zl-card-subtitle {
|
|
1716
|
+
margin: 0;
|
|
1717
|
+
font-family: var(--zl-font-family-sans, system-ui, sans-serif);
|
|
1718
|
+
font-size: 0.875rem;
|
|
1719
|
+
line-height: 1.25rem;
|
|
1720
|
+
font-weight: 400;
|
|
1721
|
+
color: var(--zl-color-text-primary-white, #f4f4f6);
|
|
1722
|
+
}
|
|
1723
|
+
/* Figma 6593:141989 / 6594:12882 — left-aligned in Field+CTAs (x=0, items-start). */
|
|
1724
|
+
.zl-card-nav {
|
|
1725
|
+
margin: 0;
|
|
1726
|
+
font-family: var(--zl-font-family-sans, system-ui, sans-serif);
|
|
1727
|
+
font-size: 1rem;
|
|
1728
|
+
line-height: 1.5rem;
|
|
1729
|
+
font-weight: 400;
|
|
1730
|
+
color: var(--zl-color-text-primary-white, #f4f4f6);
|
|
1731
|
+
text-align: left;
|
|
1732
|
+
}
|
|
1733
|
+
.zl-card-nav__link {
|
|
1734
|
+
display: inline;
|
|
1735
|
+
margin: 0;
|
|
1736
|
+
padding: 0;
|
|
1737
|
+
border: none;
|
|
1738
|
+
background: transparent;
|
|
1739
|
+
font: inherit;
|
|
1740
|
+
line-height: inherit;
|
|
1741
|
+
color: var(--zl-color-icon-default-purple, #bba5e4);
|
|
1742
|
+
text-decoration: none;
|
|
1743
|
+
cursor: pointer;
|
|
1744
|
+
}
|
|
1745
|
+
.zl-card-nav__link:hover {
|
|
1746
|
+
text-decoration: underline;
|
|
1747
|
+
}
|
|
1748
|
+
.zl-card-nav__link:focus-visible {
|
|
1749
|
+
outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);
|
|
1750
|
+
outline-offset: var(--zl-focus-offset, 2px);
|
|
1751
|
+
}
|
|
1752
|
+
/* Figma 6593:141989 — forgot password row (left-aligned, before CTAs). */
|
|
1753
|
+
.zl-card-forgot {
|
|
1754
|
+
margin: 0;
|
|
1755
|
+
text-align: left;
|
|
1756
|
+
}
|
|
1757
|
+
.zl-card-forgot__link {
|
|
1758
|
+
display: inline;
|
|
1759
|
+
margin: 0;
|
|
1760
|
+
padding: 0;
|
|
1761
|
+
border: none;
|
|
1762
|
+
background: transparent;
|
|
1763
|
+
font-family: var(--zl-font-family-sans, system-ui, sans-serif);
|
|
1764
|
+
font-size: 1rem;
|
|
1765
|
+
line-height: 1.5rem;
|
|
1766
|
+
font-weight: 400;
|
|
1767
|
+
color: var(--zl-color-icon-default-purple, #bba5e4);
|
|
1768
|
+
text-decoration: none;
|
|
1769
|
+
cursor: pointer;
|
|
1770
|
+
}
|
|
1771
|
+
.zl-card-forgot__link:hover {
|
|
1772
|
+
text-decoration: underline;
|
|
1773
|
+
}
|
|
1774
|
+
.zl-card-forgot__link:focus-visible {
|
|
1775
|
+
outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);
|
|
1776
|
+
outline-offset: var(--zl-focus-offset, 2px);
|
|
1777
|
+
}
|
|
1778
|
+
`;
|
|
1779
|
+
//#endregion
|
|
1780
|
+
//#region src/orchestrator/templates/passkey-upsell.liquid.ts
|
|
1781
|
+
/**
|
|
1782
|
+
* `passkey-upsell` partial — third Figma target screen (file
|
|
1783
|
+
* `xkvBjkOJ8ENuHdTGZHXezK`, node `6594:630`).
|
|
1784
|
+
*
|
|
1785
|
+
* Differs from `auth-form`:
|
|
1786
|
+
* - heading + subtitle live in a compact card (gap 24px instead of 32px),
|
|
1787
|
+
* - primary "Set up passkey" CTA carries a trailing 24px passkey icon
|
|
1788
|
+
* (Figma slots the icon to the right of the label, not above it),
|
|
1789
|
+
* - secondary "Skip for now" uses the dark secondary hierarchy, not the
|
|
1790
|
+
* `text` hierarchy — the Figma button has a solid #252528 background.
|
|
1791
|
+
*
|
|
1792
|
+
* Server-side errors land in a `<zl-alert>` the same way as the auth form.
|
|
1793
|
+
*/
|
|
1794
|
+
const passkeyUpsellTemplate = String.raw`
|
|
1795
|
+
<zl-page-shell data-zl-template-root>
|
|
1796
|
+
{% if branding.logo_url %}
|
|
1797
|
+
<img slot="header" class="zl-card-logo" src="{{ branding.logo_url }}" alt="" />
|
|
1798
|
+
{% endif %}
|
|
1799
|
+
<zl-card compact>
|
|
1800
|
+
<h1 slot="header" class="zl-card-title">{{ step.texts.title_key | t: identity.display_name }}</h1>
|
|
1801
|
+
<p slot="header" class="zl-card-subtitle">{{ 'passkey-upsell.description.line1' | t }}</p>
|
|
1802
|
+
<p slot="header" class="zl-card-subtitle">{{ 'passkey-upsell.description.line2' | t }}</p>
|
|
1803
|
+
|
|
1804
|
+
{% if errors and errors.size > 0 %}
|
|
1805
|
+
{% for err in errors %}
|
|
1806
|
+
{% if err | formLevelError %}
|
|
1807
|
+
<zl-alert severity="error">
|
|
1808
|
+
{% if err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}
|
|
1809
|
+
</zl-alert>
|
|
1810
|
+
{% endif %}
|
|
1811
|
+
{% endfor %}
|
|
1812
|
+
{% endif %}
|
|
1813
|
+
|
|
1814
|
+
{% for entry in actions %}
|
|
1815
|
+
{% if entry[1].primary %}
|
|
1816
|
+
<zl-button
|
|
1817
|
+
slot="footer"
|
|
1818
|
+
hierarchy="primary"
|
|
1819
|
+
size="medium"
|
|
1820
|
+
type="submit"
|
|
1821
|
+
block
|
|
1822
|
+
action="{{ entry[0] }}"
|
|
1823
|
+
{% if loading %}loading disabled{% endif %}
|
|
1824
|
+
>
|
|
1825
|
+
{{ entry[1].text_key | t }}
|
|
1826
|
+
<zl-icon slot="trailing" name="passkey" size="24"></zl-icon>
|
|
1827
|
+
</zl-button>
|
|
1828
|
+
{% endif %}
|
|
1829
|
+
{% endfor %}
|
|
1830
|
+
|
|
1831
|
+
{% for entry in actions %}
|
|
1832
|
+
{% unless entry[1].primary %}
|
|
1833
|
+
<zl-button
|
|
1834
|
+
slot="footer"
|
|
1835
|
+
hierarchy="secondary"
|
|
1836
|
+
size="medium"
|
|
1837
|
+
type="button"
|
|
1838
|
+
block
|
|
1839
|
+
action="{{ entry[0] }}"
|
|
1840
|
+
label="{{ entry[1].text_key | t }}"
|
|
1841
|
+
></zl-button>
|
|
1842
|
+
{% endunless %}
|
|
1843
|
+
{% endfor %}
|
|
1844
|
+
|
|
1845
|
+
{% mandatory_gates %}
|
|
1846
|
+
|
|
1847
|
+
{% if challenge and challenge.method == "passkey" %}
|
|
1848
|
+
<zl-passkey
|
|
1849
|
+
ceremony="{{ challenge.options.ceremony | default: 'register' }}"
|
|
1850
|
+
challenge-id="{{ challenge.challenge_id }}"
|
|
1851
|
+
options='{{ challenge.options | json }}'
|
|
1852
|
+
></zl-passkey>
|
|
1853
|
+
{% endif %}
|
|
1854
|
+
</zl-card>
|
|
1855
|
+
</zl-page-shell>
|
|
1856
|
+
`;
|
|
1857
|
+
//#endregion
|
|
1858
|
+
//#region src/orchestrator/templates/signed-in.liquid.ts
|
|
1859
|
+
/**
|
|
1860
|
+
* `signed-in` partial — fourth Figma target screen (file
|
|
1861
|
+
* `xkvBjkOJ8ENuHdTGZHXezK`, node `6596:132844`).
|
|
1862
|
+
*
|
|
1863
|
+
* Figma layout:
|
|
1864
|
+
* - heading "You're signed in as" (left-aligned, full width)
|
|
1865
|
+
* - body line with the authenticated email address (Arimo Regular 16/24)
|
|
1866
|
+
* - "Continue" primary CTA
|
|
1867
|
+
* - "Logout" secondary CTA
|
|
1868
|
+
*
|
|
1869
|
+
* No avatar / identity hero glyph: the Figma frame is intentionally a
|
|
1870
|
+
* pure modal layover and dropping the avatar keeps the orchestrator
|
|
1871
|
+
* default in lockstep with the spec.
|
|
1872
|
+
*
|
|
1873
|
+
* Rendered when the flow returns a terminal step (`complete: "show"`)
|
|
1874
|
+
* and no `post-sign-in-url` is configured, so the user can confirm the
|
|
1875
|
+
* identity before the host SPA takes over.
|
|
1876
|
+
*/
|
|
1877
|
+
const signedInTemplate = String.raw`
|
|
1878
|
+
<zl-page-shell data-zl-template-root>
|
|
1879
|
+
{% if branding.logo_url %}
|
|
1880
|
+
<img slot="header" class="zl-card-logo" src="{{ branding.logo_url }}" alt="" />
|
|
1881
|
+
{% endif %}
|
|
1882
|
+
<zl-card compact>
|
|
1883
|
+
<h1 slot="header" class="zl-card-title">
|
|
1884
|
+
{{ step.texts.title_key | t: identity.display_name }}
|
|
1885
|
+
</h1>
|
|
1886
|
+
{% if identity.email_address or identity.display_name %}
|
|
1887
|
+
<p slot="header" class="zl-card-subtitle">
|
|
1888
|
+
{{ identity.email_address | default: identity.display_name }}
|
|
1889
|
+
</p>
|
|
1890
|
+
{% endif %}
|
|
1891
|
+
|
|
1892
|
+
{% if errors and errors.size > 0 %}
|
|
1893
|
+
{% for err in errors %}
|
|
1894
|
+
<zl-alert severity="error">{{ err.text_key | t }}</zl-alert>
|
|
1895
|
+
{% endfor %}
|
|
1896
|
+
{% endif %}
|
|
1897
|
+
|
|
1898
|
+
{% if actions and actions.size > 0 %}
|
|
1899
|
+
{% for entry in actions %}
|
|
1900
|
+
{% if entry[1].primary %}
|
|
1901
|
+
<zl-button
|
|
1902
|
+
slot="footer"
|
|
1903
|
+
hierarchy="primary"
|
|
1904
|
+
size="medium"
|
|
1905
|
+
type="submit"
|
|
1906
|
+
block
|
|
1907
|
+
action="{{ entry[0] }}"
|
|
1908
|
+
label="{{ entry[1].text_key | t }}"
|
|
1909
|
+
{% if loading %}loading{% endif %}
|
|
1910
|
+
></zl-button>
|
|
1911
|
+
{% endif %}
|
|
1912
|
+
{% endfor %}
|
|
1913
|
+
|
|
1914
|
+
{% for entry in actions %}
|
|
1915
|
+
{% unless entry[1].primary %}
|
|
1916
|
+
<zl-button
|
|
1917
|
+
slot="footer"
|
|
1918
|
+
hierarchy="secondary"
|
|
1919
|
+
size="medium"
|
|
1920
|
+
block
|
|
1921
|
+
action="{{ entry[0] }}"
|
|
1922
|
+
label="{{ entry[1].text_key | t }}"
|
|
1923
|
+
></zl-button>
|
|
1924
|
+
{% endunless %}
|
|
1925
|
+
{% endfor %}
|
|
1926
|
+
{% else %}
|
|
1927
|
+
{% comment %}
|
|
1928
|
+
Fallback when the API ships a terminal step with no actions —
|
|
1929
|
+
matches Figma node 6596:132844 which always renders Continue
|
|
1930
|
+
+ Logout. The orchestrator wires these to its built-in
|
|
1931
|
+
continue / logout handlers.
|
|
1932
|
+
{% endcomment %}
|
|
1933
|
+
<zl-button
|
|
1934
|
+
slot="footer"
|
|
1935
|
+
hierarchy="primary"
|
|
1936
|
+
size="medium"
|
|
1937
|
+
type="submit"
|
|
1938
|
+
block
|
|
1939
|
+
action="continue"
|
|
1940
|
+
label="{{ 'signed-in.continue' | t }}"
|
|
1941
|
+
></zl-button>
|
|
1942
|
+
<zl-button
|
|
1943
|
+
slot="footer"
|
|
1944
|
+
hierarchy="secondary"
|
|
1945
|
+
size="medium"
|
|
1946
|
+
block
|
|
1947
|
+
action="logout"
|
|
1948
|
+
label="{{ 'signed-in.logout' | t }}"
|
|
1949
|
+
></zl-button>
|
|
1950
|
+
{% endif %}
|
|
1951
|
+
|
|
1952
|
+
{% mandatory_gates %}
|
|
1953
|
+
</zl-card>
|
|
1954
|
+
</zl-page-shell>
|
|
1955
|
+
`;
|
|
1956
|
+
//#endregion
|
|
1957
|
+
//#region src/orchestrator/template-names.ts
|
|
1958
|
+
/**
|
|
1959
|
+
* Names of the bundled Liquid templates the `<zitadel-login>` orchestrator
|
|
1960
|
+
* registers.
|
|
1961
|
+
*
|
|
1962
|
+
* This lives in its own module — separate from `liquid.ts` — so the public
|
|
1963
|
+
* barrel can re-export `TEMPLATE_NAMES` without dragging LiquidJS' `Liquid`
|
|
1964
|
+
* type (and its Node ambient-type requirements) into the published
|
|
1965
|
+
* declaration surface. See `liquid.ts` / the orchestrator barrel.
|
|
1966
|
+
*/
|
|
1967
|
+
const TEMPLATE_NAMES = {
|
|
1968
|
+
default: "default",
|
|
1969
|
+
authForm: "auth-form",
|
|
1970
|
+
passkeyUpsell: "passkey-upsell",
|
|
1971
|
+
signedIn: "signed-in"
|
|
1972
|
+
};
|
|
1973
|
+
//#endregion
|
|
1974
|
+
//#region src/orchestrator/liquid.ts
|
|
1975
|
+
/**
|
|
1976
|
+
* LiquidJS engine factory for the `<zitadel-login>` orchestrator.
|
|
1977
|
+
*
|
|
1978
|
+
* Configures the engine per the security pipeline in
|
|
1979
|
+
* `docs/design/flowengine/template-security.md` and registers the
|
|
1980
|
+
* `| t` filter and `{% mandatory_gates %}` tag from
|
|
1981
|
+
* `docs/design/branding/templates.md`.
|
|
1982
|
+
*
|
|
1983
|
+
* Hard rules:
|
|
1984
|
+
*
|
|
1985
|
+
* - `{{ }}` output is HTML-escaped (LiquidJS default since v10).
|
|
1986
|
+
* - The `| raw` filter is overridden to a no-op string passthrough that still
|
|
1987
|
+
* escapes (Layer 1 of the security pipeline).
|
|
1988
|
+
* - Partials are loaded from an in-memory map; no filesystem access.
|
|
1989
|
+
*/
|
|
1990
|
+
function createLiquidEngine(options) {
|
|
1991
|
+
const engine = new Liquid({
|
|
1992
|
+
templates: {
|
|
1993
|
+
[TEMPLATE_NAMES.default]: defaultTemplate,
|
|
1994
|
+
[TEMPLATE_NAMES.authForm]: authFormTemplate,
|
|
1995
|
+
[TEMPLATE_NAMES.passkeyUpsell]: passkeyUpsellTemplate,
|
|
1996
|
+
[TEMPLATE_NAMES.signedIn]: signedInTemplate,
|
|
1997
|
+
...options.templates ?? {}
|
|
1998
|
+
},
|
|
1999
|
+
cache: true,
|
|
2000
|
+
jsTruthy: true,
|
|
2001
|
+
relativeReference: false,
|
|
2002
|
+
strictFilters: false,
|
|
2003
|
+
strictVariables: false,
|
|
2004
|
+
outputEscape: "escape"
|
|
2005
|
+
});
|
|
2006
|
+
engine.registerFilter("raw", (value) => stringify(value));
|
|
2007
|
+
engine.registerFilter("t", function tFilter(key, ...args) {
|
|
2008
|
+
const lookupKey = stringify(key);
|
|
2009
|
+
return interpolate(options.locale[lookupKey] ?? lookupKey, args.map(stringify));
|
|
2010
|
+
});
|
|
2011
|
+
/** Resolves `{text_key}.placeholder` — empty when undefined (not the raw key). */
|
|
2012
|
+
engine.registerFilter("fieldPlaceholder", (textKey) => {
|
|
2013
|
+
const lookupKey = `${stringify(textKey)}.placeholder`;
|
|
2014
|
+
return options.locale[lookupKey] ?? "";
|
|
2015
|
+
});
|
|
2016
|
+
/** Resolves `{text_key}.help` — empty when undefined. */
|
|
2017
|
+
engine.registerFilter("fieldHelp", (textKey) => {
|
|
2018
|
+
const lookupKey = `${stringify(textKey)}.help`;
|
|
2019
|
+
return options.locale[lookupKey] ?? "";
|
|
2020
|
+
});
|
|
2021
|
+
/** Maps `error.*` text keys to a field name (Figma inline-error annotations). */
|
|
2022
|
+
const fieldErrorKeys = {
|
|
2023
|
+
"error.email_required": "email",
|
|
2024
|
+
"error.email_invalid": "email",
|
|
2025
|
+
"error.email_exists": "email",
|
|
2026
|
+
"error.password_required": "password",
|
|
2027
|
+
"error.password_incorrect": "password",
|
|
2028
|
+
"error.invalid_credentials": "password"
|
|
2029
|
+
};
|
|
2030
|
+
/** Resolves `{text_key}.title` for form-level `<zl-alert heading>`. */
|
|
2031
|
+
engine.registerFilter("alertHeading", (textKey) => {
|
|
2032
|
+
const lookupKey = `${stringify(textKey)}.title`;
|
|
2033
|
+
return options.locale[lookupKey] ?? "";
|
|
2034
|
+
});
|
|
2035
|
+
/** Resolves `{text_key}.body` for form-level `<zl-alert>` message slot. */
|
|
2036
|
+
engine.registerFilter("alertBody", (textKey) => {
|
|
2037
|
+
const lookupKey = `${stringify(textKey)}.body`;
|
|
2038
|
+
return options.locale[lookupKey] ?? "";
|
|
2039
|
+
});
|
|
2040
|
+
/** Localized inline error for `fieldName`, or empty when none applies. */
|
|
2041
|
+
engine.registerFilter("fieldError", (fieldName, errors) => {
|
|
2042
|
+
const name = stringify(fieldName);
|
|
2043
|
+
if (!Array.isArray(errors)) return "";
|
|
2044
|
+
for (const item of errors) {
|
|
2045
|
+
const key = item.text_key ?? "";
|
|
2046
|
+
if (fieldErrorKeys[key] === name) return options.locale[key] ?? key;
|
|
2047
|
+
}
|
|
2048
|
+
return "";
|
|
2049
|
+
});
|
|
2050
|
+
/** True when the error should render as `<zl-alert>`, not on a field. */
|
|
2051
|
+
engine.registerFilter("formLevelError", (err) => {
|
|
2052
|
+
const key = err?.text_key ?? "";
|
|
2053
|
+
return key === "" || !(key in fieldErrorKeys);
|
|
2054
|
+
});
|
|
2055
|
+
engine.registerTag("mandatory_gates", {
|
|
2056
|
+
parse() {},
|
|
2057
|
+
render() {
|
|
2058
|
+
return mandatoryGatesMarkerComment;
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
return engine;
|
|
2062
|
+
}
|
|
2063
|
+
function stringify(value) {
|
|
2064
|
+
if (value == null) return "";
|
|
2065
|
+
if (typeof value === "string") return value;
|
|
2066
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2067
|
+
try {
|
|
2068
|
+
return JSON.stringify(value);
|
|
2069
|
+
} catch {
|
|
2070
|
+
return String(value);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
function interpolate(template, args) {
|
|
2074
|
+
if (args.length === 0) return template;
|
|
2075
|
+
return template.replace(/\{(\d+)\}/g, (match, index) => {
|
|
2076
|
+
return args[Number(index)] ?? match;
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
//#endregion
|
|
2080
|
+
//#region src/orchestrator/locales/en.ts
|
|
2081
|
+
/**
|
|
2082
|
+
* Minimal English locale dictionary.
|
|
2083
|
+
*
|
|
2084
|
+
* The `| t` filter looks up `text_key` strings here. Missing keys fall through
|
|
2085
|
+
* to the raw key (matches the spec in
|
|
2086
|
+
* `docs/design/flowengine/flow-engine-guide.md`).
|
|
2087
|
+
*
|
|
2088
|
+
* Copy aligned to Figma screens file `xkvBjkOJ8ENuHdTGZHXezK` (May 2026).
|
|
2089
|
+
* MVP only — multi-locale support is deferred.
|
|
2090
|
+
*/
|
|
2091
|
+
const en = {
|
|
2092
|
+
"identifier.title": "Sign in",
|
|
2093
|
+
"identifier.field.email": "Work email",
|
|
2094
|
+
"identifier.field.email.placeholder": "you@company.com",
|
|
2095
|
+
"identifier.field.password": "Password",
|
|
2096
|
+
"identifier.action.passkey": "Sign in with a passkey",
|
|
2097
|
+
"identifier.action.register.lead": "Don't have an account? ",
|
|
2098
|
+
"identifier.action.register.link": "Sign up",
|
|
2099
|
+
"password.title": "Sign in",
|
|
2100
|
+
"password.field.password": "Password",
|
|
2101
|
+
"password.action.passkey": "Sign in with a passkey",
|
|
2102
|
+
"password.action.register.lead": "Don't have an account? ",
|
|
2103
|
+
"password.action.register.link": "Sign up",
|
|
2104
|
+
"register.title": "Create your account",
|
|
2105
|
+
"register.field.email": "Work email",
|
|
2106
|
+
"register.field.email.placeholder": "you@company.com",
|
|
2107
|
+
"register.field.password": "Password",
|
|
2108
|
+
"register.field.password.help": "At least 8 characters, including a symbol and number.",
|
|
2109
|
+
"register.action.submit": "Sign up",
|
|
2110
|
+
"register.action.sign_in.lead": "Already have an account? ",
|
|
2111
|
+
"register.action.sign_in.link": "Sign in",
|
|
2112
|
+
"passkey-upsell.title": "Sign in faster next time",
|
|
2113
|
+
"passkey-upsell.description.line1": "No password needed ever again.",
|
|
2114
|
+
"passkey-upsell.description.line2": "Sign in with Face ID, Touch ID, or PIN.",
|
|
2115
|
+
"passkey-upsell.action.setup": "Set up passkey",
|
|
2116
|
+
"passkey-upsell.action.skip": "Skip for now",
|
|
2117
|
+
/** Figma `6594:630` setup-passkey annotations — form-level `<zl-alert>`. */
|
|
2118
|
+
"error.passkey_cancelled": "Passkey setup was cancelled",
|
|
2119
|
+
"error.passkey_not_registered": "This passkey is not registered. Please sign in with your email and password.",
|
|
2120
|
+
"error.passkey_setup_failed": "Passkey registration did not complete. Please try again.",
|
|
2121
|
+
"error.passkey_unsupported": "This device does not support passkeys",
|
|
2122
|
+
"error.passkey_failed": "Something went wrong. Please try again.",
|
|
2123
|
+
"complete.title": "You're signed in as",
|
|
2124
|
+
"signed-in.continue": "Continue",
|
|
2125
|
+
"signed-in.logout": "Logout",
|
|
2126
|
+
"recover.title": "Check your email",
|
|
2127
|
+
"recover.description": "We sent a password reset link to your email address.",
|
|
2128
|
+
"recover.action.back": "Back to sign in",
|
|
2129
|
+
"passkey-login.title": "Sign in with your passkey",
|
|
2130
|
+
"submit.continue": "Continue",
|
|
2131
|
+
"submit.signin": "Sign in",
|
|
2132
|
+
"action.forgot_password": "Forgot password?",
|
|
2133
|
+
"action.cancel": "Cancel",
|
|
2134
|
+
"sso.redirect.title": "Redirecting to your provider…",
|
|
2135
|
+
"error.email_required": "Please enter an email address",
|
|
2136
|
+
"error.email_invalid": "Please enter a valid email",
|
|
2137
|
+
"error.password_required": "Please enter a password",
|
|
2138
|
+
"error.password_incorrect": "Wrong email or password.",
|
|
2139
|
+
"error.email_exists": "An account with this email already exists.",
|
|
2140
|
+
/** Figma sign-in error `6602:180268` — inline on password field. */
|
|
2141
|
+
"error.invalid_credentials": "Wrong email or password.",
|
|
2142
|
+
"error.required": "This field is required.",
|
|
2143
|
+
"error.sign_in_server.title": "We couldn't complete your sign in.",
|
|
2144
|
+
"error.sign_in_server.body": "Please try again in a few minutes"
|
|
2145
|
+
};
|
|
2146
|
+
//#endregion
|
|
2147
|
+
//#region src/orchestrator/locales/de.ts
|
|
2148
|
+
const de = {
|
|
2149
|
+
"identifier.title": "Anmelden",
|
|
2150
|
+
"identifier.field.email": "E-Mail",
|
|
2151
|
+
"identifier.field.email.placeholder": "du@unternehmen.com",
|
|
2152
|
+
"identifier.field.password": "Passwort",
|
|
2153
|
+
"identifier.action.passkey": "Mit Passkey anmelden",
|
|
2154
|
+
"identifier.action.register.lead": "Noch kein Konto? ",
|
|
2155
|
+
"identifier.action.register.link": "Registrieren",
|
|
2156
|
+
"password.title": "Anmelden",
|
|
2157
|
+
"password.field.password": "Passwort",
|
|
2158
|
+
"password.action.passkey": "Mit Passkey anmelden",
|
|
2159
|
+
"password.action.register.lead": "Noch kein Konto? ",
|
|
2160
|
+
"password.action.register.link": "Registrieren",
|
|
2161
|
+
"register.title": "Konto erstellen",
|
|
2162
|
+
"register.field.email": "E-Mail",
|
|
2163
|
+
"register.field.email.placeholder": "du@unternehmen.com",
|
|
2164
|
+
"register.field.password": "Passwort",
|
|
2165
|
+
"register.field.password.help": "Mindestens 8 Zeichen, einschließlich eines Sonderzeichens und einer Zahl.",
|
|
2166
|
+
"register.action.submit": "Registrieren",
|
|
2167
|
+
"register.action.sign_in.lead": "Bereits ein Konto? ",
|
|
2168
|
+
"register.action.sign_in.link": "Anmelden",
|
|
2169
|
+
"passkey-upsell.title": "Nächstes Mal schneller anmelden",
|
|
2170
|
+
"passkey-upsell.description.line1": "Kein Passwort mehr nötig.",
|
|
2171
|
+
"passkey-upsell.description.line2": "Melde dich mit Face ID, Touch ID oder PIN an.",
|
|
2172
|
+
"passkey-upsell.action.setup": "Passkey einrichten",
|
|
2173
|
+
"passkey-upsell.action.skip": "Vorerst überspringen",
|
|
2174
|
+
"error.passkey_cancelled": "Passkey-Einrichtung wurde abgebrochen",
|
|
2175
|
+
"error.passkey_not_registered": "Dieser Passkey ist nicht registriert. Bitte melde dich mit E-Mail und Passwort an.",
|
|
2176
|
+
"error.passkey_setup_failed": "Passkey-Registrierung wurde nicht abgeschlossen. Bitte versuche es erneut.",
|
|
2177
|
+
"error.passkey_unsupported": "Dieses Gerät unterstützt keine Passkeys",
|
|
2178
|
+
"error.passkey_failed": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
|
|
2179
|
+
"complete.title": "Du bist angemeldet als",
|
|
2180
|
+
"signed-in.continue": "Weiter",
|
|
2181
|
+
"signed-in.logout": "Abmelden",
|
|
2182
|
+
"recover.title": "E-Mail prüfen",
|
|
2183
|
+
"recover.description": "Wir haben einen Link zum Zurücksetzen des Passworts an deine E-Mail-Adresse gesendet.",
|
|
2184
|
+
"recover.action.back": "Zurück zur Anmeldung",
|
|
2185
|
+
"passkey-login.title": "Mit Passkey anmelden",
|
|
2186
|
+
"submit.continue": "Weiter",
|
|
2187
|
+
"submit.signin": "Anmelden",
|
|
2188
|
+
"action.forgot_password": "Passwort vergessen?",
|
|
2189
|
+
"action.cancel": "Abbrechen",
|
|
2190
|
+
"sso.redirect.title": "Weiterleitung zum Anbieter…",
|
|
2191
|
+
"error.email_required": "Bitte gib eine E-Mail-Adresse ein",
|
|
2192
|
+
"error.email_invalid": "Bitte gib eine gültige E-Mail-Adresse ein",
|
|
2193
|
+
"error.password_required": "Bitte gib ein Passwort ein",
|
|
2194
|
+
"error.password_incorrect": "Falsche E-Mail oder falsches Passwort.",
|
|
2195
|
+
"error.email_exists": "Ein Konto mit dieser E-Mail-Adresse existiert bereits.",
|
|
2196
|
+
"error.invalid_credentials": "Falsche E-Mail oder falsches Passwort.",
|
|
2197
|
+
"error.required": "Dieses Feld ist erforderlich.",
|
|
2198
|
+
"error.sign_in_server.title": "Anmeldung konnte nicht abgeschlossen werden.",
|
|
2199
|
+
"error.sign_in_server.body": "Bitte versuche es in einigen Minuten erneut"
|
|
2200
|
+
};
|
|
2201
|
+
//#endregion
|
|
2202
|
+
//#region src/orchestrator/locales/index.ts
|
|
2203
|
+
const builtinLocales = {
|
|
2204
|
+
en,
|
|
2205
|
+
de
|
|
2206
|
+
};
|
|
2207
|
+
//#endregion
|
|
2208
|
+
//#region ../shared-component-styles/dist/attribution-markup.mjs
|
|
2209
|
+
/**
|
|
2210
|
+
* Figma sign-in attribution chip (`6593:141767`): "Secured with" + 65×16 logotype.
|
|
2211
|
+
* Shared by Lit orchestrator HTML, dev playground, and paired React playgrounds.
|
|
2212
|
+
*/
|
|
2213
|
+
const ZITADEL_ATTRIBUTION_LOGOTYPE_SVG = "<svg class=\"zr-pill__logotype-svg\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 67.3501 15.1111\" width=\"65\" height=\"16\" fill=\"currentColor\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M2.04839 4.265C2.06224 4.1284 2.1748 3.98591 2.32756 3.91161L8.98593 0.0494262C9.227 -0.0678242 9.45274 0.0334116 9.43089 0.248972V4.52681C9.41705 4.66341 9.30448 4.80589 9.15172 4.88019L2.49335 8.66426C2.25228 8.78151 2.02654 8.68027 2.04839 8.46471V4.265Z\"/><path d=\"M14.0086 5.25359C14.12 5.33388 14.1871 5.50261 14.1751 5.67205V13.2643C14.1561 13.5317 13.9555 13.6766 13.7798 13.5499L10.1005 11.4468C9.98908 11.3666 9.92196 11.1978 9.934 11.0284V3.32695C9.95299 3.05956 10.1535 2.91467 10.3293 3.04138L14.0086 5.25359Z\"/><path d=\"M7.37428 15.0817C7.24906 15.138 7.06938 15.1118 6.92865 15.0167L0.183465 11.1405C-0.038611 10.9903 -0.0638098 10.7442 0.133796 10.6554L3.76278 8.53833C3.888 8.48203 4.06767 8.50827 4.2084 8.60341L10.9046 12.5341C11.1267 12.6843 11.1557 12.9125 10.9543 13.0193L7.37428 15.0817Z\"/><path d=\"M65.6687 13.1043V2.00077H67.3501V13.1043H65.6687Z\"/><path d=\"M60.4255 13.2947C59.6535 13.2947 58.9715 13.1202 58.3793 12.7712C57.7871 12.4222 57.3218 11.9358 56.9834 11.3119C56.6556 10.688 56.4917 9.97418 56.4917 9.1705C56.4917 8.33509 56.6556 7.61073 56.9834 6.99739C57.3112 6.38405 57.7712 5.90819 58.3634 5.56979C58.9662 5.22083 59.6588 5.04634 60.4414 5.04634C61.2027 5.04634 61.869 5.21554 62.44 5.55393C63.011 5.88175 63.4552 6.33118 63.7724 6.90222C64.1002 7.47326 64.2642 8.12359 64.2642 8.85325C64.2642 9.0013 64.2589 9.1282 64.2483 9.23395C64.2483 9.33969 64.2377 9.46659 64.2166 9.61464H58.1731C58.2048 10.0588 58.3158 10.4553 58.5062 10.8043C58.6965 11.1427 58.9556 11.4124 59.2834 11.6133C59.6112 11.8036 59.9866 11.8988 60.4096 11.8988C60.8644 11.8988 61.2503 11.7983 61.5676 11.5974C61.8954 11.3859 62.1386 11.0898 62.2972 10.7091H64.0421C63.9046 11.185 63.672 11.6186 63.3441 12.0098C63.0269 12.4011 62.6251 12.7131 62.1386 12.9457C61.6522 13.1784 61.0811 13.2947 60.4255 13.2947ZM58.1889 8.42497H62.551C62.5299 7.81163 62.3131 7.3305 61.9007 6.98153C61.4883 6.62199 60.986 6.44222 60.3938 6.44222C59.865 6.44222 59.3839 6.60613 58.9503 6.93394C58.5168 7.26176 58.263 7.75876 58.1889 8.42497Z\"/><path d=\"M50.5389 13.2947C49.7563 13.2947 49.0796 13.1096 48.5085 12.7395C47.9481 12.3588 47.5145 11.8565 47.2078 11.2326C46.9117 10.6087 46.7637 9.91073 46.7637 9.13877C46.7637 8.37739 46.9117 7.69004 47.2078 7.0767C47.5145 6.45279 47.9481 5.96106 48.5085 5.60152C49.0796 5.2314 49.7563 5.04634 50.5389 5.04634C51.1205 5.04634 51.6492 5.16795 52.1251 5.41117C52.601 5.64382 52.9869 5.97692 53.283 6.41049V2.00077H54.9803V13.1043H53.3465L53.283 11.8829C53.0292 12.2954 52.6644 12.6337 52.1886 12.8981C51.7127 13.1625 51.1628 13.2947 50.5389 13.2947ZM50.872 11.8195C51.369 11.8195 51.7973 11.7032 52.1568 11.4705C52.5164 11.2379 52.7913 10.9206 52.9817 10.5188C53.172 10.1169 53.2672 9.66751 53.2672 9.1705C53.2672 8.59946 53.1561 8.1183 52.9341 7.72704C52.7226 7.33577 52.4318 7.0344 52.0617 6.82291C51.7021 6.61141 51.3003 6.50567 50.8561 6.50567C50.4014 6.50567 49.9943 6.6167 49.6347 6.83877C49.2752 7.06084 48.9897 7.36749 48.7782 7.75876C48.5773 8.15003 48.4768 8.61532 48.4768 9.15463C48.4768 9.68337 48.5825 10.1487 48.794 10.5505C49.0055 10.9524 49.2911 11.2643 49.6506 11.4864C50.0101 11.7085 50.4173 11.8195 50.872 11.8195Z\"/><path d=\"M40.7882 13.2947C40.2806 13.2947 39.8153 13.2048 39.3923 13.025C38.9693 12.8347 38.6309 12.5597 38.3771 12.2002C38.1233 11.8406 37.9965 11.4071 37.9965 10.8995C37.9965 10.2967 38.1445 9.81556 38.4406 9.45602C38.7473 9.0859 39.1544 8.81624 39.662 8.64704C40.1801 8.46727 40.7459 8.37739 41.3592 8.37739H43.3579C43.3579 7.92267 43.2839 7.55786 43.1358 7.28291C42.9878 6.99739 42.7868 6.79118 42.533 6.66429C42.2792 6.52681 41.9832 6.45808 41.6448 6.45808C41.2006 6.45808 40.8094 6.56383 40.471 6.77532C40.1431 6.97624 39.9369 7.29876 39.8523 7.7429H38.1551C38.2079 7.17186 38.3983 6.68544 38.7261 6.28359C39.0645 5.88175 39.4928 5.57508 40.011 5.36358C40.5291 5.15209 41.079 5.04634 41.6606 5.04634C42.422 5.04634 43.0512 5.1891 43.5482 5.47462C44.0558 5.74957 44.4312 6.13555 44.6744 6.63256C44.9282 7.12958 45.0551 7.69531 45.0551 8.3298V10.7409C45.0657 11.0475 45.1292 11.2749 45.2455 11.4229C45.3724 11.5604 45.5945 11.6397 45.9117 11.6609V13.1043C45.4887 13.1043 45.1186 13.0567 44.8013 12.9616C44.4947 12.8664 44.2409 12.7236 44.04 12.5333C43.839 12.3324 43.6804 12.0786 43.5641 11.7719C43.3103 12.1949 42.9402 12.5544 42.4537 12.8505C41.9673 13.1466 41.4121 13.2947 40.7882 13.2947ZM41.1848 11.9147C41.6183 11.9147 41.999 11.8089 42.3268 11.5974C42.6547 11.3753 42.9084 11.0898 43.0882 10.7409C43.268 10.3813 43.3579 9.99533 43.3579 9.58291V9.55119H41.4861C41.1795 9.55119 40.8887 9.5882 40.6137 9.66223C40.3493 9.73625 40.1326 9.86315 39.9634 10.0429C39.8047 10.2121 39.7254 10.45 39.7254 10.7567C39.7254 11.1586 39.8682 11.4547 40.1537 11.645C40.4392 11.8248 40.7829 11.9147 41.1848 11.9147Z\"/><path d=\"M35.5197 13.1043C34.7372 13.1043 34.1344 12.914 33.7114 12.5333C33.2884 12.1526 33.0769 11.5287 33.0769 10.6615V6.64842H31.8873V5.23669H33.0769V3.3332H34.7583V5.23669H36.757V6.64842H34.7583V10.6615C34.7583 11.0634 34.8482 11.333 35.028 11.4705C35.2078 11.608 35.488 11.6767 35.8687 11.6767H36.757V13.1043H35.5197Z\"/><path d=\"M29.6602 3.85668C29.3536 3.85668 29.0892 3.7509 28.8671 3.53941C28.6556 3.31734 28.5499 3.04768 28.5499 2.73043C28.5499 2.42376 28.6556 2.16468 28.8671 1.95319C29.0892 1.74169 29.3536 1.63594 29.6602 1.63594C29.9775 1.63594 30.2419 1.74169 30.4533 1.95319C30.6754 2.16468 30.7865 2.42376 30.7865 2.73043C30.7865 3.04768 30.6754 3.31734 30.4533 3.53941C30.2419 3.7509 29.9775 3.85668 29.6602 3.85668ZM28.8195 13.1043V5.23669H30.5009V13.1043H28.8195Z\"/><path d=\"M19.8276 13.1043V11.8036L25.1098 3.42837H19.8911V2.00077H27.0608V3.30147L21.7628 11.6609H27.1084V13.1043H19.8276Z\"/></svg>";
|
|
2214
|
+
/** Inner markup for the default Zitadel attribution pill (label + logotype). */
|
|
2215
|
+
function zitadelAttributionPillInnerHtml() {
|
|
2216
|
+
return `Secured with <span class="zr-pill__logotype">${ZITADEL_ATTRIBUTION_LOGOTYPE_SVG}</span>`;
|
|
2217
|
+
}
|
|
2218
|
+
//#endregion
|
|
2219
|
+
//#region src/orchestrator/sanitiser.ts
|
|
2220
|
+
/**
|
|
2221
|
+
* Layer 2 of the template security pipeline (`docs/design/flowengine/template-security.md`).
|
|
2222
|
+
*
|
|
2223
|
+
* Wraps DOMPurify with the configuration the auth-UI templates need:
|
|
2224
|
+
*
|
|
2225
|
+
* - Allow every `<zl-*>` custom element registered in the manifest registry,
|
|
2226
|
+
* plus the union of attributes declared on each manifest.
|
|
2227
|
+
* - Allow common HTML structural / typographic tags used by `default.liquid`.
|
|
2228
|
+
* - Strip `on*` event handlers and any `<script>` / `<style>` tags (theming is
|
|
2229
|
+
* orchestrator-owned via `adoptedStyleSheets`).
|
|
2230
|
+
*
|
|
2231
|
+
* Runs after Liquid finishes rendering and before the HTML is handed to Lit's
|
|
2232
|
+
* `unsafeHTML` directive.
|
|
2233
|
+
*/
|
|
2234
|
+
/**
|
|
2235
|
+
* Standard HTML tags that templates may use as structural chrome around
|
|
2236
|
+
* `<zl-*>` atoms. Anything outside this list and the manifest registry is
|
|
2237
|
+
* stripped at sanitise-time.
|
|
2238
|
+
*/
|
|
2239
|
+
const STRUCTURAL_TAGS = [
|
|
2240
|
+
"div",
|
|
2241
|
+
"span",
|
|
2242
|
+
"p",
|
|
2243
|
+
"small",
|
|
2244
|
+
"strong",
|
|
2245
|
+
"em",
|
|
2246
|
+
"br",
|
|
2247
|
+
"hr",
|
|
2248
|
+
"h1",
|
|
2249
|
+
"h2",
|
|
2250
|
+
"h3",
|
|
2251
|
+
"h4",
|
|
2252
|
+
"h5",
|
|
2253
|
+
"h6",
|
|
2254
|
+
"ul",
|
|
2255
|
+
"ol",
|
|
2256
|
+
"li",
|
|
2257
|
+
"a",
|
|
2258
|
+
"img",
|
|
2259
|
+
"section",
|
|
2260
|
+
"header",
|
|
2261
|
+
"footer",
|
|
2262
|
+
"main",
|
|
2263
|
+
"nav",
|
|
2264
|
+
"aside",
|
|
2265
|
+
"article",
|
|
2266
|
+
"figure",
|
|
2267
|
+
"figcaption",
|
|
2268
|
+
"label",
|
|
2269
|
+
"fieldset",
|
|
2270
|
+
"legend"
|
|
2271
|
+
];
|
|
2272
|
+
/**
|
|
2273
|
+
* Attributes templates may carry on structural tags. Per-atom attributes are
|
|
2274
|
+
* derived from the manifest registry below — manifests are the single source
|
|
2275
|
+
* of truth for "what attributes does `<zl-field>` accept?".
|
|
2276
|
+
*/
|
|
2277
|
+
const COMMON_ATTRS = [
|
|
2278
|
+
"id",
|
|
2279
|
+
"class",
|
|
2280
|
+
"role",
|
|
2281
|
+
"slot",
|
|
2282
|
+
"tabindex",
|
|
2283
|
+
"lang",
|
|
2284
|
+
"dir",
|
|
2285
|
+
"alt",
|
|
2286
|
+
"title",
|
|
2287
|
+
"src",
|
|
2288
|
+
"srcset",
|
|
2289
|
+
"sizes",
|
|
2290
|
+
"href",
|
|
2291
|
+
"target",
|
|
2292
|
+
"rel",
|
|
2293
|
+
"style",
|
|
2294
|
+
"name",
|
|
2295
|
+
"for",
|
|
2296
|
+
"data-theme",
|
|
2297
|
+
"hidden"
|
|
2298
|
+
];
|
|
2299
|
+
const CUSTOM_TAG_PATTERN = /^zl-[a-z0-9-]+$/;
|
|
2300
|
+
function buildAllowedTagList() {
|
|
2301
|
+
const set = new Set(STRUCTURAL_TAGS);
|
|
2302
|
+
for (const manifest of manifestRegistry) set.add(manifest.tag);
|
|
2303
|
+
return [...set];
|
|
2304
|
+
}
|
|
2305
|
+
function buildAllowedAttrList() {
|
|
2306
|
+
const set = new Set(COMMON_ATTRS);
|
|
2307
|
+
for (const manifest of manifestRegistry) for (const attr of manifest.attrs) set.add(attr);
|
|
2308
|
+
return [...set];
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Returns a sanitiser bound to the current manifest registry. Callers should
|
|
2312
|
+
* cache the result; rebuilding is cheap but allocates a fresh DOMPurify config.
|
|
2313
|
+
*/
|
|
2314
|
+
function createSanitiser() {
|
|
2315
|
+
const config = {
|
|
2316
|
+
USE_PROFILES: { html: true },
|
|
2317
|
+
ADD_TAGS: buildAllowedTagList(),
|
|
2318
|
+
ADD_ATTR: buildAllowedAttrList(),
|
|
2319
|
+
FORBID_TAGS: [
|
|
2320
|
+
"script",
|
|
2321
|
+
"style",
|
|
2322
|
+
"iframe",
|
|
2323
|
+
"object",
|
|
2324
|
+
"embed",
|
|
2325
|
+
"form",
|
|
2326
|
+
"input",
|
|
2327
|
+
"button"
|
|
2328
|
+
],
|
|
2329
|
+
FORBID_ATTR: [
|
|
2330
|
+
"onerror",
|
|
2331
|
+
"onclick",
|
|
2332
|
+
"onload",
|
|
2333
|
+
"onfocus",
|
|
2334
|
+
"onblur"
|
|
2335
|
+
],
|
|
2336
|
+
CUSTOM_ELEMENT_HANDLING: {
|
|
2337
|
+
tagNameCheck: CUSTOM_TAG_PATTERN,
|
|
2338
|
+
attributeNameCheck: /^(?:[a-z][a-z0-9-]*)$/,
|
|
2339
|
+
allowCustomizedBuiltInElements: false
|
|
2340
|
+
},
|
|
2341
|
+
KEEP_CONTENT: true,
|
|
2342
|
+
ALLOW_DATA_ATTR: true,
|
|
2343
|
+
ALLOW_ARIA_ATTR: true,
|
|
2344
|
+
RETURN_TRUSTED_TYPE: false
|
|
2345
|
+
};
|
|
2346
|
+
return function sanitise(html) {
|
|
2347
|
+
const result = DOMPurify.sanitize(html, config);
|
|
2348
|
+
return typeof result === "string" ? result : String(result);
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
//#endregion
|
|
2352
|
+
//#region src/orchestrator/theme-controller.ts
|
|
2353
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
2354
|
+
var ThemeController = class {
|
|
2355
|
+
host;
|
|
2356
|
+
branding;
|
|
2357
|
+
mediaQuery = null;
|
|
2358
|
+
_theme = "dark";
|
|
2359
|
+
constructor(host) {
|
|
2360
|
+
this.host = host;
|
|
2361
|
+
host.addController(this);
|
|
2362
|
+
}
|
|
2363
|
+
get theme() {
|
|
2364
|
+
return this._theme;
|
|
2365
|
+
}
|
|
2366
|
+
setBranding(branding) {
|
|
2367
|
+
this.branding = branding;
|
|
2368
|
+
this.refresh();
|
|
2369
|
+
}
|
|
2370
|
+
hostConnected() {
|
|
2371
|
+
this.refresh();
|
|
2372
|
+
}
|
|
2373
|
+
hostDisconnected() {
|
|
2374
|
+
this.detach();
|
|
2375
|
+
}
|
|
2376
|
+
refresh() {
|
|
2377
|
+
const mode = this.branding?.theme?.mode ?? "dark";
|
|
2378
|
+
if (mode === "auto") {
|
|
2379
|
+
this.attach();
|
|
2380
|
+
this.update(this.mediaQuery?.matches === false ? "light" : "dark");
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
this.detach();
|
|
2384
|
+
this.update(mode === "light" ? "light" : "dark");
|
|
2385
|
+
}
|
|
2386
|
+
attach() {
|
|
2387
|
+
if (this.mediaQuery || typeof matchMedia !== "function") return;
|
|
2388
|
+
this.mediaQuery = matchMedia(DARK_QUERY);
|
|
2389
|
+
this.mediaQuery.addEventListener("change", this.onMediaChange);
|
|
2390
|
+
}
|
|
2391
|
+
detach() {
|
|
2392
|
+
if (!this.mediaQuery) return;
|
|
2393
|
+
this.mediaQuery.removeEventListener("change", this.onMediaChange);
|
|
2394
|
+
this.mediaQuery = null;
|
|
2395
|
+
}
|
|
2396
|
+
onMediaChange = (event) => {
|
|
2397
|
+
this.update(event.matches ? "dark" : "light");
|
|
2398
|
+
};
|
|
2399
|
+
update(next) {
|
|
2400
|
+
if (this._theme === next) return;
|
|
2401
|
+
this._theme = next;
|
|
2402
|
+
this.host.requestUpdate();
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
//#endregion
|
|
2406
|
+
//#region src/orchestrator/zitadel-login.ts
|
|
2407
|
+
let ZitadelLogin = class ZitadelLogin extends LitElement {
|
|
2408
|
+
static shadowRootOptions = {
|
|
2409
|
+
...LitElement.shadowRootOptions,
|
|
2410
|
+
delegatesFocus: true
|
|
2411
|
+
};
|
|
2412
|
+
static styles = css`
|
|
2413
|
+
:host {
|
|
2414
|
+
display: block;
|
|
2415
|
+
width: 100%;
|
|
2416
|
+
min-height: 100vh;
|
|
2417
|
+
}
|
|
2418
|
+
`;
|
|
2419
|
+
#_purpose_accessor_storage = "login";
|
|
2420
|
+
get purpose() {
|
|
2421
|
+
return this.#_purpose_accessor_storage;
|
|
2422
|
+
}
|
|
2423
|
+
set purpose(value) {
|
|
2424
|
+
this.#_purpose_accessor_storage = value;
|
|
2425
|
+
}
|
|
2426
|
+
#_project_accessor_storage;
|
|
2427
|
+
get project() {
|
|
2428
|
+
return this.#_project_accessor_storage;
|
|
2429
|
+
}
|
|
2430
|
+
set project(value) {
|
|
2431
|
+
this.#_project_accessor_storage = value;
|
|
2432
|
+
}
|
|
2433
|
+
#_postSignInUrl_accessor_storage = "";
|
|
2434
|
+
get postSignInUrl() {
|
|
2435
|
+
return this.#_postSignInUrl_accessor_storage;
|
|
2436
|
+
}
|
|
2437
|
+
set postSignInUrl(value) {
|
|
2438
|
+
this.#_postSignInUrl_accessor_storage = value;
|
|
2439
|
+
}
|
|
2440
|
+
#_resumeFlowId_accessor_storage = "";
|
|
2441
|
+
get resumeFlowId() {
|
|
2442
|
+
return this.#_resumeFlowId_accessor_storage;
|
|
2443
|
+
}
|
|
2444
|
+
set resumeFlowId(value) {
|
|
2445
|
+
this.#_resumeFlowId_accessor_storage = value;
|
|
2446
|
+
}
|
|
2447
|
+
#_lang_accessor_storage = "";
|
|
2448
|
+
get lang() {
|
|
2449
|
+
return this.#_lang_accessor_storage;
|
|
2450
|
+
}
|
|
2451
|
+
set lang(value) {
|
|
2452
|
+
this.#_lang_accessor_storage = value;
|
|
2453
|
+
}
|
|
2454
|
+
#_locales_accessor_storage;
|
|
2455
|
+
get locales() {
|
|
2456
|
+
return this.#_locales_accessor_storage;
|
|
2457
|
+
}
|
|
2458
|
+
set locales(value) {
|
|
2459
|
+
this.#_locales_accessor_storage = value;
|
|
2460
|
+
}
|
|
2461
|
+
#_response_accessor_storage = null;
|
|
2462
|
+
get response() {
|
|
2463
|
+
return this.#_response_accessor_storage;
|
|
2464
|
+
}
|
|
2465
|
+
set response(value) {
|
|
2466
|
+
this.#_response_accessor_storage = value;
|
|
2467
|
+
}
|
|
2468
|
+
#_branding_accessor_storage = void 0;
|
|
2469
|
+
get branding() {
|
|
2470
|
+
return this.#_branding_accessor_storage;
|
|
2471
|
+
}
|
|
2472
|
+
set branding(value) {
|
|
2473
|
+
this.#_branding_accessor_storage = value;
|
|
2474
|
+
}
|
|
2475
|
+
#_loading_accessor_storage = false;
|
|
2476
|
+
get loading() {
|
|
2477
|
+
return this.#_loading_accessor_storage;
|
|
2478
|
+
}
|
|
2479
|
+
set loading(value) {
|
|
2480
|
+
this.#_loading_accessor_storage = value;
|
|
2481
|
+
}
|
|
2482
|
+
#_startupError_accessor_storage = null;
|
|
2483
|
+
get startupError() {
|
|
2484
|
+
return this.#_startupError_accessor_storage;
|
|
2485
|
+
}
|
|
2486
|
+
set startupError(value) {
|
|
2487
|
+
this.#_startupError_accessor_storage = value;
|
|
2488
|
+
}
|
|
2489
|
+
#_formValues_accessor_storage = {};
|
|
2490
|
+
get formValues() {
|
|
2491
|
+
return this.#_formValues_accessor_storage;
|
|
2492
|
+
}
|
|
2493
|
+
set formValues(value) {
|
|
2494
|
+
this.#_formValues_accessor_storage = value;
|
|
2495
|
+
}
|
|
2496
|
+
themeController = new ThemeController(this);
|
|
2497
|
+
engine = null;
|
|
2498
|
+
sanitise = createSanitiser();
|
|
2499
|
+
/**
|
|
2500
|
+
* Cached compiled tenant template, keyed by source string. Re-rendering on
|
|
2501
|
+
* every `formValues` change otherwise re-parses the same template.
|
|
2502
|
+
*/
|
|
2503
|
+
tenantTemplateCache = null;
|
|
2504
|
+
createRenderRoot() {
|
|
2505
|
+
const root = super.createRenderRoot();
|
|
2506
|
+
if (root instanceof ShadowRoot) {
|
|
2507
|
+
const existing = Array.isArray(root.adoptedStyleSheets) ? root.adoptedStyleSheets : [];
|
|
2508
|
+
const sheet = new CSSStyleSheet();
|
|
2509
|
+
sheet.replaceSync(layoutChromeCss);
|
|
2510
|
+
root.adoptedStyleSheets = [...existing, sheet];
|
|
2511
|
+
root.addEventListener("zl-input", this.handleAtomInput);
|
|
2512
|
+
root.addEventListener("zl-submit", this.handleAtomSubmit);
|
|
2513
|
+
root.addEventListener("click", this.handleDelegatedAction);
|
|
2514
|
+
root.addEventListener("submit", this.handleFormSubmit);
|
|
2515
|
+
root.addEventListener("zl-passkey-result", this.handlePasskeyResult);
|
|
2516
|
+
root.addEventListener("zl-passkey-error", this.handlePasskeyError);
|
|
2517
|
+
}
|
|
2518
|
+
return root;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Start the flow after the first render rather than in `connectedCallback`.
|
|
2522
|
+
* Frameworks that wrap web components (e.g. `@lit/react` in the console)
|
|
2523
|
+
* attach the element first and then assign object properties (`branding`,
|
|
2524
|
+
* `locale`) via setters. `connectedCallback` runs synchronously on attach
|
|
2525
|
+
* — before any setters from a wrapper's effects/refs — so reading those
|
|
2526
|
+
* properties there sees stale defaults. `firstUpdated` runs after Lit's
|
|
2527
|
+
* first render, by which time setters from the wrapping framework have
|
|
2528
|
+
* fired.
|
|
2529
|
+
*/
|
|
2530
|
+
firstUpdated() {
|
|
2531
|
+
queueMicrotask(() => void this.startFlow());
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* Resolves the effective locale dictionary. The built-in dictionary for the
|
|
2535
|
+
* resolved language is used as the base; entries from the `locales` map (if
|
|
2536
|
+
* set) are spread on top so partial overrides work without importing and
|
|
2537
|
+
* spreading the full base dictionary.
|
|
2538
|
+
*/
|
|
2539
|
+
resolveLocale() {
|
|
2540
|
+
const primary = ((this.lang || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "")).split("-")[0] ?? "").toLowerCase();
|
|
2541
|
+
const builtin = builtinLocales[primary] ?? en;
|
|
2542
|
+
const custom = this.locales?.[primary];
|
|
2543
|
+
return custom ? {
|
|
2544
|
+
...builtin,
|
|
2545
|
+
...custom
|
|
2546
|
+
} : builtin;
|
|
2547
|
+
}
|
|
2548
|
+
willUpdate(changed) {
|
|
2549
|
+
if (!this.engine || changed.has("locales") || changed.has("lang")) this.engine = createLiquidEngine({ locale: this.resolveLocale() });
|
|
2550
|
+
const root = this.shadowRoot;
|
|
2551
|
+
if (root) {
|
|
2552
|
+
applyBaseTokens(root);
|
|
2553
|
+
applyBrandingTokens(root, this.branding, this.themeController.theme);
|
|
2554
|
+
applyFontUrl(root, this.branding?.font_url ?? null);
|
|
2555
|
+
}
|
|
2556
|
+
this.dataset.theme = this.themeController.theme;
|
|
2557
|
+
this.toggleAttribute("data-theme-dark", this.themeController.theme === "dark");
|
|
2558
|
+
this.setAttribute("aria-busy", this.loading ? "true" : "false");
|
|
2559
|
+
}
|
|
2560
|
+
updated(changed) {
|
|
2561
|
+
if (!changed.has("response")) return;
|
|
2562
|
+
requestAnimationFrame(() => {
|
|
2563
|
+
this.applyValuesToFields();
|
|
2564
|
+
this.moveFocusToFirstField();
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
render() {
|
|
2568
|
+
if (this.startupError) return html`<form class="zl-mount" novalidate>
|
|
2569
|
+
<zl-alert severity="error">${this.startupError}</zl-alert>
|
|
2570
|
+
</form>`;
|
|
2571
|
+
if (!this.response || !this.engine) return html`<slot name="loader"></slot>`;
|
|
2572
|
+
const rendered = this.injectAttribution(this.renderStep(this.response.step, this.engine));
|
|
2573
|
+
return html`<form
|
|
2574
|
+
class="zl-mount"
|
|
2575
|
+
part="form"
|
|
2576
|
+
novalidate
|
|
2577
|
+
aria-busy=${this.loading ? "true" : "false"}
|
|
2578
|
+
>
|
|
2579
|
+
${unsafeHTML(rendered)}
|
|
2580
|
+
</form>`;
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Inject the attribution badge into the rendered template's
|
|
2584
|
+
* `<zl-page-shell>` footer slot. The attribution must live INSIDE the
|
|
2585
|
+
* page-shell so it sits within the 100vh viewport rhythm (matching the
|
|
2586
|
+
* Figma sign-in frame where the pill sits 24px below the card, both
|
|
2587
|
+
* centred on the page). It can't be a sibling of the page-shell because
|
|
2588
|
+
* the page-shell already occupies the full viewport height.
|
|
2589
|
+
*/
|
|
2590
|
+
injectAttribution(rendered) {
|
|
2591
|
+
const html = this.renderAttributionHtml();
|
|
2592
|
+
if (!html) return rendered;
|
|
2593
|
+
if (rendered.includes("</zl-page-shell>")) return rendered.replace("</zl-page-shell>", `${html}</zl-page-shell>`);
|
|
2594
|
+
return rendered + html;
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* "Secured with Zitadel" attribution chrome injected into every
|
|
2598
|
+
* template's page-shell footer slot. Controlled by
|
|
2599
|
+
* `branding.attribution.show_zitadel` — defaults to `true` for
|
|
2600
|
+
* community / OSS deployments. Licensed tenants can suppress the badge
|
|
2601
|
+
* entirely or swap it for a `custom_link` value.
|
|
2602
|
+
*/
|
|
2603
|
+
renderAttributionHtml() {
|
|
2604
|
+
const attribution = this.branding?.attribution;
|
|
2605
|
+
const show = attribution?.show_zitadel !== false;
|
|
2606
|
+
const custom = attribution?.custom_link;
|
|
2607
|
+
if (!show && !custom) return "";
|
|
2608
|
+
if (custom) return `<div slot="footer" part="attribution" class="zl-attribution"><zl-pill tone="neutral" href="${String(custom.href).replace(/"/g, """)}">${String(custom.label).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</zl-pill></div>`;
|
|
2609
|
+
return `<div slot="footer" part="attribution" class="zl-attribution"><zl-pill tone="neutral" href="https://zitadel.com" part="attribution-pill" aria-label="Secured with Zitadel">${zitadelAttributionPillInnerHtml()}</zl-pill></div>`;
|
|
2610
|
+
}
|
|
2611
|
+
async startFlow() {
|
|
2612
|
+
const cfg = this.project ?? getZitadelConfig();
|
|
2613
|
+
const projectId = cfg?.projectId || "";
|
|
2614
|
+
if (!cfg) throw new Error("<zitadel-login> requires a `config` prop (from configureZitadel()) or configureZitadel() must be called before use.");
|
|
2615
|
+
const api = getApi(cfg);
|
|
2616
|
+
this.loading = true;
|
|
2617
|
+
this.startupError = null;
|
|
2618
|
+
try {
|
|
2619
|
+
let wire;
|
|
2620
|
+
if (this.resumeFlowId) wire = await getCurrentStep(api, this.resumeFlowId);
|
|
2621
|
+
else {
|
|
2622
|
+
if (!projectId) throw new Error("<zitadel-login> requires a `project-id` attribute (or configureZitadel()) to start a flow.");
|
|
2623
|
+
wire = await startFlow(api, {
|
|
2624
|
+
project_id: projectId,
|
|
2625
|
+
purpose: this.purpose
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
this.applyResponse(wire);
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
this.handleTransportError(error);
|
|
2631
|
+
} finally {
|
|
2632
|
+
this.loading = false;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
applyResponse(wire) {
|
|
2636
|
+
this.response = wire;
|
|
2637
|
+
const { branding, issues } = validateBranding(wire.branding);
|
|
2638
|
+
this.branding = branding;
|
|
2639
|
+
this.themeController.setBranding(branding);
|
|
2640
|
+
if (issues.length > 0) console.warn("[zitadel-login] branding payload has issues:", issues);
|
|
2641
|
+
this.formValues = {
|
|
2642
|
+
...collectInitialValues(wire.step),
|
|
2643
|
+
...this.formValues
|
|
2644
|
+
};
|
|
2645
|
+
this.maybeCompleteFlow(wire);
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Acts on terminal flow steps. The wire surfaces two kinds of completion:
|
|
2649
|
+
*
|
|
2650
|
+
* - `step.complete === "redirect"` — navigate the browser to
|
|
2651
|
+
* `response.redirect_uri` (OIDC/SAML `auth_request_id` resolved). This
|
|
2652
|
+
* takes precedence over `post-sign-in-url`.
|
|
2653
|
+
* - `step.complete === "show"` — when `post-sign-in-url` is set, exchange
|
|
2654
|
+
* the `handoff_token` for a session cookie and navigate there.
|
|
2655
|
+
*
|
|
2656
|
+
* `zitadel-flow-complete` is always emitted so hosts with custom post-sign-in
|
|
2657
|
+
* flows can handle the handoff themselves when `post-sign-in-url` is omitted.
|
|
2658
|
+
*/
|
|
2659
|
+
async maybeCompleteFlow(response) {
|
|
2660
|
+
const behavior = response.step.complete;
|
|
2661
|
+
if (!behavior) return;
|
|
2662
|
+
this.dispatchEvent(new CustomEvent("zitadel-flow-complete", {
|
|
2663
|
+
bubbles: true,
|
|
2664
|
+
composed: true,
|
|
2665
|
+
detail: {
|
|
2666
|
+
behavior,
|
|
2667
|
+
redirect_uri: response.redirect_uri,
|
|
2668
|
+
handoff_token: response.handoff_token,
|
|
2669
|
+
handoff_token_expires_at: response.handoff_token_expires_at
|
|
2670
|
+
}
|
|
2671
|
+
}));
|
|
2672
|
+
if (typeof window === "undefined") return;
|
|
2673
|
+
if (behavior === "redirect" && response.redirect_uri) {
|
|
2674
|
+
window.location.assign(response.redirect_uri);
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
const handoffToken = response.handoff_token;
|
|
2678
|
+
if (behavior === "show" && handoffToken && this.postSignInUrl) {
|
|
2679
|
+
this.loading = true;
|
|
2680
|
+
try {
|
|
2681
|
+
const cfg = this.project ?? getZitadelConfig();
|
|
2682
|
+
if (!cfg) throw new Error("<zitadel-login> config is required for exchange.");
|
|
2683
|
+
await exchangeSession(getApi(cfg), { handoff_token: handoffToken }, { project_id: cfg.projectId });
|
|
2684
|
+
window.location.assign(this.postSignInUrl);
|
|
2685
|
+
} catch (error) {
|
|
2686
|
+
this.handleTransportError(error);
|
|
2687
|
+
} finally {
|
|
2688
|
+
this.loading = false;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
renderStep(step, engine) {
|
|
2693
|
+
const tenantSource = typeof this.branding?.liquid_template === "string" && this.branding.liquid_template.length > 0 ? this.branding.liquid_template : null;
|
|
2694
|
+
const errors = step.error ? step.error.startsWith("error.") ? [{ text_key: step.error }] : [{ message: step.error }] : [];
|
|
2695
|
+
const context = {
|
|
2696
|
+
step: {
|
|
2697
|
+
name: step.name,
|
|
2698
|
+
complete: step.complete,
|
|
2699
|
+
texts: step.texts ?? {}
|
|
2700
|
+
},
|
|
2701
|
+
fields: step.fields ?? {},
|
|
2702
|
+
actions: step.actions ?? {},
|
|
2703
|
+
gates: step.gates ?? {},
|
|
2704
|
+
sso_providers: step.sso_providers ?? [],
|
|
2705
|
+
challenge: step.challenge ?? null,
|
|
2706
|
+
messages: [],
|
|
2707
|
+
identity: this.deriveIdentity(),
|
|
2708
|
+
errors,
|
|
2709
|
+
branding: this.branding ?? {},
|
|
2710
|
+
loading: this.loading
|
|
2711
|
+
};
|
|
2712
|
+
let raw;
|
|
2713
|
+
try {
|
|
2714
|
+
if (tenantSource) {
|
|
2715
|
+
const compiled = this.tenantTemplateCache?.source === tenantSource ? this.tenantTemplateCache.template : engine.parse(tenantSource);
|
|
2716
|
+
this.tenantTemplateCache = {
|
|
2717
|
+
source: tenantSource,
|
|
2718
|
+
template: compiled
|
|
2719
|
+
};
|
|
2720
|
+
raw = engine.renderSync(compiled, context);
|
|
2721
|
+
} else raw = engine.renderFileSync(TEMPLATE_NAMES.default, context);
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
console.error("[zitadel-login] Liquid render failed:", error);
|
|
2724
|
+
try {
|
|
2725
|
+
raw = engine.renderFileSync(TEMPLATE_NAMES.default, context);
|
|
2726
|
+
} catch {
|
|
2727
|
+
return `<zl-alert severity="error">We couldn't render this step.</zl-alert>`;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
const patched = patchMandatoryGates(raw, step, this.resolveLocale());
|
|
2731
|
+
return this.sanitise(patched);
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Build a `FlowIdentity` from the orchestrator's captured form values so
|
|
2735
|
+
* the signed-in template can greet the user by email without the API
|
|
2736
|
+
* having to round-trip identity claims. Email comes from the
|
|
2737
|
+
* identifier step; display name composes from `given_name` /
|
|
2738
|
+
* `family_name` when the register step ran.
|
|
2739
|
+
*/
|
|
2740
|
+
deriveIdentity() {
|
|
2741
|
+
const email = this.formValues.email?.trim();
|
|
2742
|
+
const display = [this.formValues.given_name?.trim(), this.formValues.family_name?.trim()].filter(Boolean).join(" ").trim();
|
|
2743
|
+
if (!email && !display) return null;
|
|
2744
|
+
return {
|
|
2745
|
+
...email ? { email_address: email } : {},
|
|
2746
|
+
...display ? { display_name: display } : email ? { display_name: email } : {}
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
applyValuesToFields() {
|
|
2750
|
+
const root = this.shadowRoot;
|
|
2751
|
+
if (!root) return;
|
|
2752
|
+
const fields = root.querySelectorAll("zl-field");
|
|
2753
|
+
if (fields.length === 0) return;
|
|
2754
|
+
for (const field of fields) {
|
|
2755
|
+
const name = field.getAttribute("name");
|
|
2756
|
+
if (!name || !(name in this.formValues)) continue;
|
|
2757
|
+
const next = this.formValues[name];
|
|
2758
|
+
if (field.value !== next) field.value = next;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
handleAtomInput = (event) => {
|
|
2762
|
+
if (!event.detail) return;
|
|
2763
|
+
const { name, value } = event.detail;
|
|
2764
|
+
if (!name) return;
|
|
2765
|
+
this.formValues = {
|
|
2766
|
+
...this.formValues,
|
|
2767
|
+
[name]: value
|
|
2768
|
+
};
|
|
2769
|
+
this.dispatchEvent(new CustomEvent("zitadel-flow-input", {
|
|
2770
|
+
bubbles: true,
|
|
2771
|
+
composed: true,
|
|
2772
|
+
detail: {
|
|
2773
|
+
name,
|
|
2774
|
+
value
|
|
2775
|
+
}
|
|
2776
|
+
}));
|
|
2777
|
+
};
|
|
2778
|
+
handleAtomSubmit = (event) => {
|
|
2779
|
+
if (this.loading) return;
|
|
2780
|
+
this.submit(event.detail?.action ?? null);
|
|
2781
|
+
};
|
|
2782
|
+
/** Secondary navigation rows (`data-action` on `.zl-card-nav__link`). */
|
|
2783
|
+
handleDelegatedAction = (event) => {
|
|
2784
|
+
const target = event.target?.closest("[data-action]");
|
|
2785
|
+
if (!target || target.closest("zl-button") || this.loading) return;
|
|
2786
|
+
const action = target.getAttribute("data-action");
|
|
2787
|
+
if (!action) return;
|
|
2788
|
+
event.preventDefault();
|
|
2789
|
+
this.submit(action);
|
|
2790
|
+
};
|
|
2791
|
+
handleFormSubmit = (event) => {
|
|
2792
|
+
event.preventDefault();
|
|
2793
|
+
if (this.loading) return;
|
|
2794
|
+
const action = event.submitter?.getAttribute?.("action") ?? null ?? this.findPrimaryAction();
|
|
2795
|
+
this.submit(action);
|
|
2796
|
+
};
|
|
2797
|
+
/**
|
|
2798
|
+
* Handle a successful WebAuthn ceremony. Auto-submit with the proof
|
|
2799
|
+
* as `challenge_response` so the flow advances without extra user
|
|
2800
|
+
* interaction — the ceremony IS the factor verification (ADR 013).
|
|
2801
|
+
*/
|
|
2802
|
+
handlePasskeyResult = (event) => {
|
|
2803
|
+
if (this.loading) return;
|
|
2804
|
+
const { challenge_id, method, proof } = event.detail;
|
|
2805
|
+
this.submit("submit", {
|
|
2806
|
+
challenge_id,
|
|
2807
|
+
method,
|
|
2808
|
+
proof
|
|
2809
|
+
});
|
|
2810
|
+
};
|
|
2811
|
+
/**
|
|
2812
|
+
* Handle a WebAuthn ceremony error. Re-render the current step with
|
|
2813
|
+
* an error message so the user sees feedback and can retry or skip.
|
|
2814
|
+
*
|
|
2815
|
+
* Guard: if the step already carries the same error key, skip the update.
|
|
2816
|
+
* Mutating `this.response` triggers `unsafeHTML` to replace the DOM tree,
|
|
2817
|
+
* which reconnects a fresh `<zl-passkey>` that immediately re-starts the
|
|
2818
|
+
* ceremony — creating an infinite loop. The guard breaks the cycle.
|
|
2819
|
+
*
|
|
2820
|
+
* We also strip the `challenge` from the step so the template does not
|
|
2821
|
+
* render a new `<zl-passkey>` on re-render. Without this, the first
|
|
2822
|
+
* cancel would trigger a second ceremony (the guard prevents a third).
|
|
2823
|
+
*/
|
|
2824
|
+
handlePasskeyError = (event) => {
|
|
2825
|
+
if (!this.response) return;
|
|
2826
|
+
const { error: message, aborted } = event.detail;
|
|
2827
|
+
const errorKey = aborted ? "error.passkey_cancelled" : "error.passkey_failed";
|
|
2828
|
+
if (this.response.step.error === errorKey) return;
|
|
2829
|
+
const { challenge: _dropped, ...stepWithoutChallenge } = this.response.step;
|
|
2830
|
+
this.response = {
|
|
2831
|
+
...this.response,
|
|
2832
|
+
step: {
|
|
2833
|
+
...stepWithoutChallenge,
|
|
2834
|
+
error: errorKey
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
console.warn(`[zitadel-login] passkey ceremony ${aborted ? "cancelled" : "failed"}: ${message}`);
|
|
2838
|
+
};
|
|
2839
|
+
findPrimaryAction() {
|
|
2840
|
+
const root = this.shadowRoot;
|
|
2841
|
+
if (!root) return null;
|
|
2842
|
+
return (root.querySelector("zl-button[hierarchy=\"primary\"][type=\"submit\"]") ?? root.querySelector("zl-button[hierarchy=\"primary\"]"))?.getAttribute("action") || null;
|
|
2843
|
+
}
|
|
2844
|
+
moveFocusToFirstField() {
|
|
2845
|
+
const root = this.shadowRoot;
|
|
2846
|
+
if (!root) return;
|
|
2847
|
+
requestAnimationFrame(() => {
|
|
2848
|
+
const focusables = root.querySelectorAll("zl-field, zl-button");
|
|
2849
|
+
Array.from(focusables).find((el) => !el.hasAttribute("disabled"))?.focus();
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
async submit(action, challengeResponse) {
|
|
2853
|
+
if (!this.response || this.loading) return;
|
|
2854
|
+
const { id, session_token } = this.response;
|
|
2855
|
+
this.loading = true;
|
|
2856
|
+
try {
|
|
2857
|
+
const stepFieldKeys = Object.keys(this.response.step.fields ?? {});
|
|
2858
|
+
const fields = {};
|
|
2859
|
+
for (const key of stepFieldKeys) {
|
|
2860
|
+
const value = this.formValues[key];
|
|
2861
|
+
if (value !== void 0) fields[key] = value;
|
|
2862
|
+
}
|
|
2863
|
+
const body = {
|
|
2864
|
+
session_token,
|
|
2865
|
+
action: action ?? "submit",
|
|
2866
|
+
fields,
|
|
2867
|
+
...challengeResponse ? { challenge_response: challengeResponse } : {}
|
|
2868
|
+
};
|
|
2869
|
+
const cfg = this.project ?? getZitadelConfig();
|
|
2870
|
+
if (!cfg) throw new Error("<zitadel-login> config is required for submit.");
|
|
2871
|
+
const wire = await submitStep(getApi(cfg), id, body);
|
|
2872
|
+
this.applyResponse(wire);
|
|
2873
|
+
this.dispatchEvent(new CustomEvent("zitadel-flow-step", {
|
|
2874
|
+
bubbles: true,
|
|
2875
|
+
composed: true,
|
|
2876
|
+
detail: { step: wire.step }
|
|
2877
|
+
}));
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
this.handleTransportError(error);
|
|
2880
|
+
} finally {
|
|
2881
|
+
this.loading = false;
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
handleTransportError(error) {
|
|
2885
|
+
const message = error instanceof Error ? error.message : "Unexpected error contacting the Flow API.";
|
|
2886
|
+
this.startupError = message;
|
|
2887
|
+
console.error("[zitadel-login]", error);
|
|
2888
|
+
this.dispatchEvent(new CustomEvent("zitadel-flow-error", {
|
|
2889
|
+
bubbles: true,
|
|
2890
|
+
composed: true,
|
|
2891
|
+
detail: { message }
|
|
2892
|
+
}));
|
|
2893
|
+
}
|
|
2894
|
+
};
|
|
2895
|
+
__decorate([property({ type: String })], ZitadelLogin.prototype, "purpose", null);
|
|
2896
|
+
__decorate([property({ attribute: false })], ZitadelLogin.prototype, "project", null);
|
|
2897
|
+
__decorate([property({
|
|
2898
|
+
type: String,
|
|
2899
|
+
attribute: "post-sign-in-url"
|
|
2900
|
+
})], ZitadelLogin.prototype, "postSignInUrl", null);
|
|
2901
|
+
__decorate([property({
|
|
2902
|
+
type: String,
|
|
2903
|
+
attribute: "resume-flow-id"
|
|
2904
|
+
})], ZitadelLogin.prototype, "resumeFlowId", null);
|
|
2905
|
+
__decorate([property({ type: String })], ZitadelLogin.prototype, "lang", null);
|
|
2906
|
+
__decorate([property({ attribute: false })], ZitadelLogin.prototype, "locales", null);
|
|
2907
|
+
__decorate([state()], ZitadelLogin.prototype, "response", null);
|
|
2908
|
+
__decorate([state()], ZitadelLogin.prototype, "branding", null);
|
|
2909
|
+
__decorate([state()], ZitadelLogin.prototype, "loading", null);
|
|
2910
|
+
__decorate([state()], ZitadelLogin.prototype, "startupError", null);
|
|
2911
|
+
__decorate([state()], ZitadelLogin.prototype, "formValues", null);
|
|
2912
|
+
ZitadelLogin = __decorate([customElement("zitadel-login")], ZitadelLogin);
|
|
2913
|
+
function collectInitialValues(step) {
|
|
2914
|
+
const values = {};
|
|
2915
|
+
if (!step.fields) return values;
|
|
2916
|
+
for (const [name, field] of Object.entries(step.fields)) values[name] = typeof field.value === "string" ? field.value : "";
|
|
2917
|
+
return values;
|
|
2918
|
+
}
|
|
2919
|
+
//#endregion
|
|
2920
|
+
//#region src/orchestrator/zitadel-logout.ts
|
|
2921
|
+
const DISPLAY_COOKIE_NAME = "__nextgen_display";
|
|
2922
|
+
let ZitadelLogout = class ZitadelLogout extends LitElement {
|
|
2923
|
+
static styles = [baseHostStyles, css`
|
|
2924
|
+
:host {
|
|
2925
|
+
display: inline-block;
|
|
2926
|
+
position: relative;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
.trigger {
|
|
2930
|
+
all: unset;
|
|
2931
|
+
cursor: pointer;
|
|
2932
|
+
width: 2.5rem;
|
|
2933
|
+
height: 2.5rem;
|
|
2934
|
+
border-radius: 9999px;
|
|
2935
|
+
background: ${t.color.surface.defaultWhite};
|
|
2936
|
+
color: ${t.color.text.buttonDefault};
|
|
2937
|
+
font-size: 0.875rem;
|
|
2938
|
+
font-weight: 600;
|
|
2939
|
+
display: inline-flex;
|
|
2940
|
+
align-items: center;
|
|
2941
|
+
justify-content: center;
|
|
2942
|
+
letter-spacing: 0.02em;
|
|
2943
|
+
user-select: none;
|
|
2944
|
+
transition: box-shadow ${t.motion.duration.fast} ${t.motion.easing.standard};
|
|
2945
|
+
}
|
|
2946
|
+
.trigger:focus-visible {
|
|
2947
|
+
${focusVisibleStyles};
|
|
2948
|
+
}
|
|
2949
|
+
.trigger[aria-expanded="true"] {
|
|
2950
|
+
box-shadow: 0 0 0 2px ${t.focus.color};
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
.dropdown {
|
|
2954
|
+
position: absolute;
|
|
2955
|
+
top: calc(100% + ${t.spacing["02"]});
|
|
2956
|
+
right: 0;
|
|
2957
|
+
width: 14rem;
|
|
2958
|
+
background: ${t.color.surface.defaultPrimaryGray};
|
|
2959
|
+
border: 1px solid ${t.color.border.defaultGray100};
|
|
2960
|
+
border-radius: ${t.radius.m};
|
|
2961
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32);
|
|
2962
|
+
z-index: 9999;
|
|
2963
|
+
overflow: hidden;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
.preview {
|
|
2967
|
+
display: flex;
|
|
2968
|
+
align-items: center;
|
|
2969
|
+
gap: ${t.spacing["03"]};
|
|
2970
|
+
padding: ${t.spacing["03"]};
|
|
2971
|
+
border-bottom: 1px solid ${t.color.border.defaultGray100};
|
|
2972
|
+
}
|
|
2973
|
+
.preview-avatar {
|
|
2974
|
+
flex-shrink: 0;
|
|
2975
|
+
width: 2.5rem;
|
|
2976
|
+
height: 2.5rem;
|
|
2977
|
+
border-radius: 9999px;
|
|
2978
|
+
background: ${t.color.surface.defaultWhite};
|
|
2979
|
+
color: ${t.color.text.buttonDefault};
|
|
2980
|
+
font-size: 0.875rem;
|
|
2981
|
+
font-weight: 600;
|
|
2982
|
+
display: inline-flex;
|
|
2983
|
+
align-items: center;
|
|
2984
|
+
justify-content: center;
|
|
2985
|
+
}
|
|
2986
|
+
.preview-info {
|
|
2987
|
+
min-width: 0;
|
|
2988
|
+
flex: 1;
|
|
2989
|
+
}
|
|
2990
|
+
.preview-name {
|
|
2991
|
+
font-size: 0.875rem;
|
|
2992
|
+
font-weight: 600;
|
|
2993
|
+
color: ${t.color.text.primaryWhite};
|
|
2994
|
+
white-space: nowrap;
|
|
2995
|
+
overflow: hidden;
|
|
2996
|
+
text-overflow: ellipsis;
|
|
2997
|
+
}
|
|
2998
|
+
.preview-email {
|
|
2999
|
+
font-size: 0.75rem;
|
|
3000
|
+
color: ${t.color.text.secondaryGray};
|
|
3001
|
+
white-space: nowrap;
|
|
3002
|
+
overflow: hidden;
|
|
3003
|
+
text-overflow: ellipsis;
|
|
3004
|
+
margin-top: 2px;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
.actions {
|
|
3008
|
+
padding: ${t.spacing["02"]};
|
|
3009
|
+
}
|
|
3010
|
+
.signout-btn {
|
|
3011
|
+
all: unset;
|
|
3012
|
+
cursor: pointer;
|
|
3013
|
+
display: flex;
|
|
3014
|
+
align-items: center;
|
|
3015
|
+
gap: ${t.spacing["02"]};
|
|
3016
|
+
width: 100%;
|
|
3017
|
+
padding: ${t.spacing["02"]} ${t.spacing["03"]};
|
|
3018
|
+
border-radius: ${t.radius.s};
|
|
3019
|
+
color: ${t.color.text.error};
|
|
3020
|
+
font-size: 0.875rem;
|
|
3021
|
+
font-weight: 500;
|
|
3022
|
+
box-sizing: border-box;
|
|
3023
|
+
}
|
|
3024
|
+
.signout-btn:hover:not([disabled]) {
|
|
3025
|
+
background: color-mix(in srgb, ${t.color.text.error} 12%, transparent);
|
|
3026
|
+
}
|
|
3027
|
+
.signout-btn:focus-visible {
|
|
3028
|
+
${focusVisibleStyles};
|
|
3029
|
+
}
|
|
3030
|
+
.signout-btn[disabled] {
|
|
3031
|
+
cursor: not-allowed;
|
|
3032
|
+
opacity: 0.6;
|
|
3033
|
+
}
|
|
3034
|
+
.signout-btn svg {
|
|
3035
|
+
flex-shrink: 0;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
.spinner {
|
|
3039
|
+
width: 1em;
|
|
3040
|
+
height: 1em;
|
|
3041
|
+
border-radius: 9999px;
|
|
3042
|
+
border: 2px solid currentColor;
|
|
3043
|
+
border-top-color: transparent;
|
|
3044
|
+
animation: zl-logout-spin 600ms linear infinite;
|
|
3045
|
+
}
|
|
3046
|
+
@keyframes zl-logout-spin {
|
|
3047
|
+
to {
|
|
3048
|
+
transform: rotate(360deg);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
.error-bar {
|
|
3053
|
+
padding: ${t.spacing["02"]} ${t.spacing["03"]};
|
|
3054
|
+
font-size: 0.75rem;
|
|
3055
|
+
color: ${t.color.text.error};
|
|
3056
|
+
background: color-mix(in srgb, ${t.color.text.error} 12%, transparent);
|
|
3057
|
+
border-top: 1px solid ${t.color.border.defaultGray100};
|
|
3058
|
+
}
|
|
3059
|
+
`];
|
|
3060
|
+
#_project_accessor_storage;
|
|
3061
|
+
get project() {
|
|
3062
|
+
return this.#_project_accessor_storage;
|
|
3063
|
+
}
|
|
3064
|
+
set project(value) {
|
|
3065
|
+
this.#_project_accessor_storage = value;
|
|
3066
|
+
}
|
|
3067
|
+
#_postSignOutUrl_accessor_storage = "";
|
|
3068
|
+
get postSignOutUrl() {
|
|
3069
|
+
return this.#_postSignOutUrl_accessor_storage;
|
|
3070
|
+
}
|
|
3071
|
+
set postSignOutUrl(value) {
|
|
3072
|
+
this.#_postSignOutUrl_accessor_storage = value;
|
|
3073
|
+
}
|
|
3074
|
+
#_clientId_accessor_storage = "";
|
|
3075
|
+
get clientId() {
|
|
3076
|
+
return this.#_clientId_accessor_storage;
|
|
3077
|
+
}
|
|
3078
|
+
set clientId(value) {
|
|
3079
|
+
this.#_clientId_accessor_storage = value;
|
|
3080
|
+
}
|
|
3081
|
+
#_displayName_accessor_storage = "";
|
|
3082
|
+
get displayName() {
|
|
3083
|
+
return this.#_displayName_accessor_storage;
|
|
3084
|
+
}
|
|
3085
|
+
set displayName(value) {
|
|
3086
|
+
this.#_displayName_accessor_storage = value;
|
|
3087
|
+
}
|
|
3088
|
+
#_displayEmail_accessor_storage = "";
|
|
3089
|
+
get displayEmail() {
|
|
3090
|
+
return this.#_displayEmail_accessor_storage;
|
|
3091
|
+
}
|
|
3092
|
+
set displayEmail(value) {
|
|
3093
|
+
this.#_displayEmail_accessor_storage = value;
|
|
3094
|
+
}
|
|
3095
|
+
#_open_accessor_storage = false;
|
|
3096
|
+
get open() {
|
|
3097
|
+
return this.#_open_accessor_storage;
|
|
3098
|
+
}
|
|
3099
|
+
set open(value) {
|
|
3100
|
+
this.#_open_accessor_storage = value;
|
|
3101
|
+
}
|
|
3102
|
+
#_loading_accessor_storage = false;
|
|
3103
|
+
get loading() {
|
|
3104
|
+
return this.#_loading_accessor_storage;
|
|
3105
|
+
}
|
|
3106
|
+
set loading(value) {
|
|
3107
|
+
this.#_loading_accessor_storage = value;
|
|
3108
|
+
}
|
|
3109
|
+
#_errorMessage_accessor_storage = "";
|
|
3110
|
+
get errorMessage() {
|
|
3111
|
+
return this.#_errorMessage_accessor_storage;
|
|
3112
|
+
}
|
|
3113
|
+
set errorMessage(value) {
|
|
3114
|
+
this.#_errorMessage_accessor_storage = value;
|
|
3115
|
+
}
|
|
3116
|
+
templateMode = false;
|
|
3117
|
+
connectedCallback() {
|
|
3118
|
+
super.connectedCallback();
|
|
3119
|
+
this.dataset.theme = "dark";
|
|
3120
|
+
this.readDisplayCookie();
|
|
3121
|
+
const tmpl = this.querySelector("template");
|
|
3122
|
+
if (tmpl instanceof HTMLTemplateElement) {
|
|
3123
|
+
this.templateMode = true;
|
|
3124
|
+
this.renderTemplate(tmpl);
|
|
3125
|
+
}
|
|
3126
|
+
document.addEventListener("click", this.handleDocumentClick);
|
|
3127
|
+
document.addEventListener("keydown", this.handleDocumentKeydown);
|
|
3128
|
+
}
|
|
3129
|
+
disconnectedCallback() {
|
|
3130
|
+
super.disconnectedCallback();
|
|
3131
|
+
document.removeEventListener("click", this.handleDocumentClick);
|
|
3132
|
+
document.removeEventListener("keydown", this.handleDocumentKeydown);
|
|
3133
|
+
}
|
|
3134
|
+
updated() {
|
|
3135
|
+
const root = this.shadowRoot;
|
|
3136
|
+
if (root && !this.templateMode) applyBaseTokens(root);
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Decodes the `__nextgen_display` cookie (base64-encoded JSON, set by the
|
|
3140
|
+
* auth backend on sign-in) and populates `displayName` / `displayEmail`.
|
|
3141
|
+
* A missing or malformed cookie is intentionally non-fatal — the dropdown
|
|
3142
|
+
* still renders, just with empty values.
|
|
3143
|
+
*/
|
|
3144
|
+
readDisplayCookie() {
|
|
3145
|
+
if (typeof document === "undefined") return;
|
|
3146
|
+
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${DISPLAY_COOKIE_NAME}=([^;]+)`));
|
|
3147
|
+
if (!match || !match[1]) return;
|
|
3148
|
+
try {
|
|
3149
|
+
const data = JSON.parse(atob(match[1]));
|
|
3150
|
+
this.displayName = typeof data.name === "string" ? data.name : "";
|
|
3151
|
+
this.displayEmail = typeof data.email === "string" ? data.email : "";
|
|
3152
|
+
} catch {}
|
|
3153
|
+
}
|
|
3154
|
+
get initial() {
|
|
3155
|
+
const source = this.displayName || this.displayEmail;
|
|
3156
|
+
return source ? source.charAt(0).toUpperCase() : "?";
|
|
3157
|
+
}
|
|
3158
|
+
/**
|
|
3159
|
+
* Clones a consumer-supplied `<template>` into the light DOM, fills the
|
|
3160
|
+
* `{{name}}`, `{{email}}`, and `{{initial}}` tokens via a TreeWalker, and
|
|
3161
|
+
* wires every element with `data-action="logout"` to trigger sign-out.
|
|
3162
|
+
* Light-DOM mounting is deliberate so the consumer's existing CSS applies.
|
|
3163
|
+
*/
|
|
3164
|
+
renderTemplate(tmpl) {
|
|
3165
|
+
const clone = tmpl.content.cloneNode(true);
|
|
3166
|
+
fillTemplateTokens(clone, this.displayName, this.displayEmail, this.initial);
|
|
3167
|
+
const container = document.createElement("span");
|
|
3168
|
+
container.appendChild(clone);
|
|
3169
|
+
this.appendChild(container);
|
|
3170
|
+
container.querySelectorAll("[data-action=\"logout\"]").forEach((el) => {
|
|
3171
|
+
el.addEventListener("click", (event) => {
|
|
3172
|
+
event.preventDefault();
|
|
3173
|
+
this.doLogout();
|
|
3174
|
+
});
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
handleDocumentClick = (event) => {
|
|
3178
|
+
if (!this.open) return;
|
|
3179
|
+
if (event.composedPath().includes(this)) return;
|
|
3180
|
+
this.open = false;
|
|
3181
|
+
};
|
|
3182
|
+
handleDocumentKeydown = (event) => {
|
|
3183
|
+
if (!this.open) return;
|
|
3184
|
+
if (event.key !== "Escape") return;
|
|
3185
|
+
this.open = false;
|
|
3186
|
+
this.shadowRoot?.querySelector(".trigger")?.focus();
|
|
3187
|
+
};
|
|
3188
|
+
toggleOpen() {
|
|
3189
|
+
this.open = !this.open;
|
|
3190
|
+
this.errorMessage = "";
|
|
3191
|
+
}
|
|
3192
|
+
/**
|
|
3193
|
+
* Calls the typed `endSession` operation (`GET /auth/end-session`) with
|
|
3194
|
+
* `credentials: "include"`. The server clears the session cookie via
|
|
3195
|
+
* `Set-Cookie: Max-Age=0`; on success this element fires `zitadel-signout`
|
|
3196
|
+
* and optionally navigates to `postSignOutUrl`.
|
|
3197
|
+
*/
|
|
3198
|
+
async doLogout() {
|
|
3199
|
+
this.loading = true;
|
|
3200
|
+
this.errorMessage = "";
|
|
3201
|
+
const params = {
|
|
3202
|
+
...this.clientId ? { client_id: this.clientId } : {},
|
|
3203
|
+
...this.postSignOutUrl ? { post_logout_redirect_uri: this.postSignOutUrl } : {}
|
|
3204
|
+
};
|
|
3205
|
+
try {
|
|
3206
|
+
const cfg = this.project ?? getZitadelConfig();
|
|
3207
|
+
if (!cfg) throw new Error("<zitadel-logout> requires a `config` prop (from configureZitadel()).");
|
|
3208
|
+
await getApi(cfg).endSession(params, { credentials: "include" });
|
|
3209
|
+
} catch (error) {
|
|
3210
|
+
const message = error instanceof Error ? error.message : "";
|
|
3211
|
+
this.errorMessage = message || "Sign-out failed. Please try again.";
|
|
3212
|
+
this.loading = false;
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
this.open = false;
|
|
3216
|
+
this.loading = false;
|
|
3217
|
+
const detail = {
|
|
3218
|
+
name: this.displayName,
|
|
3219
|
+
email: this.displayEmail
|
|
3220
|
+
};
|
|
3221
|
+
this.dispatchEvent(new CustomEvent("zitadel-signout", {
|
|
3222
|
+
bubbles: true,
|
|
3223
|
+
composed: true,
|
|
3224
|
+
detail
|
|
3225
|
+
}));
|
|
3226
|
+
if (this.postSignOutUrl && typeof window !== "undefined") window.location.href = this.postSignOutUrl;
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Returns the absolute URL the end-session request will hit. Useful for
|
|
3230
|
+
* test assertions and for consumers that prefer to navigate the browser
|
|
3231
|
+
* directly (instead of fetching) so the OIDC session-end redirect is
|
|
3232
|
+
* driven by the user agent.
|
|
3233
|
+
*/
|
|
3234
|
+
getEndSessionUrl() {
|
|
3235
|
+
const params = {
|
|
3236
|
+
...this.clientId ? { client_id: this.clientId } : {},
|
|
3237
|
+
...this.postSignOutUrl ? { post_logout_redirect_uri: this.postSignOutUrl } : {}
|
|
3238
|
+
};
|
|
3239
|
+
const cfg = this.project ?? getZitadelConfig();
|
|
3240
|
+
if (!cfg) throw new Error("<zitadel-logout> requires a `config` prop (from configureZitadel()).");
|
|
3241
|
+
return getApi(cfg).getEndSessionUrl(params);
|
|
3242
|
+
}
|
|
3243
|
+
handleSignOutClick(event) {
|
|
3244
|
+
event.preventDefault();
|
|
3245
|
+
this.doLogout();
|
|
3246
|
+
}
|
|
3247
|
+
render() {
|
|
3248
|
+
if (this.templateMode) return nothing;
|
|
3249
|
+
return html`
|
|
3250
|
+
<button
|
|
3251
|
+
class="trigger"
|
|
3252
|
+
type="button"
|
|
3253
|
+
aria-label=${this.open ? "Close user menu" : "Open user menu"}
|
|
3254
|
+
aria-expanded=${this.open ? "true" : "false"}
|
|
3255
|
+
aria-haspopup="dialog"
|
|
3256
|
+
@click=${this.toggleOpen}
|
|
3257
|
+
>
|
|
3258
|
+
${this.initial}
|
|
3259
|
+
</button>
|
|
3260
|
+
|
|
3261
|
+
${this.open ? html`
|
|
3262
|
+
<div class="dropdown" role="dialog" aria-label="User menu">
|
|
3263
|
+
<div class="preview">
|
|
3264
|
+
<div class="preview-avatar" aria-hidden="true">${this.initial}</div>
|
|
3265
|
+
<div class="preview-info">
|
|
3266
|
+
<div class="preview-name">${this.displayName || this.displayEmail}</div>
|
|
3267
|
+
${this.displayName ? html`<div class="preview-email">${this.displayEmail}</div>` : nothing}
|
|
3268
|
+
</div>
|
|
3269
|
+
</div>
|
|
3270
|
+
|
|
3271
|
+
<div class="actions">
|
|
3272
|
+
<button
|
|
3273
|
+
class="signout-btn"
|
|
3274
|
+
type="button"
|
|
3275
|
+
?disabled=${this.loading}
|
|
3276
|
+
@click=${this.handleSignOutClick}
|
|
3277
|
+
>
|
|
3278
|
+
${this.loading ? html`<span class="spinner" aria-hidden="true"></span>` : html`
|
|
3279
|
+
<svg
|
|
3280
|
+
width="14"
|
|
3281
|
+
height="14"
|
|
3282
|
+
viewBox="0 0 24 24"
|
|
3283
|
+
fill="none"
|
|
3284
|
+
stroke="currentColor"
|
|
3285
|
+
stroke-width="2"
|
|
3286
|
+
stroke-linecap="round"
|
|
3287
|
+
stroke-linejoin="round"
|
|
3288
|
+
aria-hidden="true"
|
|
3289
|
+
>
|
|
3290
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
3291
|
+
<polyline points="16 17 21 12 16 7" />
|
|
3292
|
+
<line x1="21" y1="12" x2="9" y2="12" />
|
|
3293
|
+
</svg>
|
|
3294
|
+
`}
|
|
3295
|
+
<span>${this.loading ? "Signing out…" : "Sign out"}</span>
|
|
3296
|
+
</button>
|
|
3297
|
+
</div>
|
|
3298
|
+
|
|
3299
|
+
${this.errorMessage ? html`<div class="error-bar" role="alert">${this.errorMessage}</div>` : nothing}
|
|
3300
|
+
</div>
|
|
3301
|
+
` : nothing}
|
|
3302
|
+
`;
|
|
3303
|
+
}
|
|
3304
|
+
};
|
|
3305
|
+
__decorate([property({ attribute: false })], ZitadelLogout.prototype, "project", null);
|
|
3306
|
+
__decorate([property({
|
|
3307
|
+
type: String,
|
|
3308
|
+
attribute: "post-sign-out-url"
|
|
3309
|
+
})], ZitadelLogout.prototype, "postSignOutUrl", null);
|
|
3310
|
+
__decorate([property({
|
|
3311
|
+
type: String,
|
|
3312
|
+
attribute: "client-id"
|
|
3313
|
+
})], ZitadelLogout.prototype, "clientId", null);
|
|
3314
|
+
__decorate([state()], ZitadelLogout.prototype, "displayName", null);
|
|
3315
|
+
__decorate([state()], ZitadelLogout.prototype, "displayEmail", null);
|
|
3316
|
+
__decorate([state()], ZitadelLogout.prototype, "open", null);
|
|
3317
|
+
__decorate([state()], ZitadelLogout.prototype, "loading", null);
|
|
3318
|
+
__decorate([state()], ZitadelLogout.prototype, "errorMessage", null);
|
|
3319
|
+
ZitadelLogout = __decorate([customElement("zitadel-logout")], ZitadelLogout);
|
|
3320
|
+
/**
|
|
3321
|
+
* Substitutes `{{name}}`, `{{email}}`, and `{{initial}}` placeholders inside
|
|
3322
|
+
* a fragment's text nodes. Walking text nodes (rather than running a regex
|
|
3323
|
+
* over `outerHTML`) keeps attributes and structural markup untouched.
|
|
3324
|
+
*/
|
|
3325
|
+
function fillTemplateTokens(fragment, name, email, initial) {
|
|
3326
|
+
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_TEXT);
|
|
3327
|
+
let node = walker.nextNode();
|
|
3328
|
+
while (node) {
|
|
3329
|
+
if (node.textContent) node.textContent = node.textContent.replace(/\{\{name\}\}/g, name).replace(/\{\{email\}\}/g, email).replace(/\{\{initial\}\}/g, initial);
|
|
3330
|
+
node = walker.nextNode();
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
//#endregion
|
|
3334
|
+
export { startFlow as C, getCurrentStep as S, applyFontUrl as _, builtinLocales as a, buildBrandingStylesheet as b, TEMPLATE_NAMES as c, defaultTemplate as d, layoutChromeCss as f, patchMandatoryGates as g, mandatoryGatesMarkerComment as h, createSanitiser as i, signedInTemplate as l, MANDATORY_GATES_MARKER as m, ZitadelLogin as n, de as o, authFormTemplate as p, ThemeController as r, en as s, ZitadelLogout as t, passkeyUpsellTemplate as u, validateBranding as v, submitStep as w, resolveTheme as x, applyBrandingTokens as y };
|
|
3335
|
+
|
|
3336
|
+
//# sourceMappingURL=orchestrator-CNKo2WQG.mjs.map
|