@entifix/zitadel 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 r10c Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,93 @@
1
+ import { EntifixLogicError } from '@entifix/core';
2
+ import { Context, Effect, Layer } from 'effect';
3
+ /**
4
+ * The header Zitadel signs an Actions v2 payload with.
5
+ *
6
+ * Read from the sender, not from the docs: `internal/execution/execution.go`
7
+ * sets `actions.SigningHeader`, which is this exact spelling. The `CreateTarget`
8
+ * proto comment claims `X-ZITADEL-Signature` and is wrong for v4.16.2 — we pin
9
+ * the one the running instance actually sends rather than accepting both, since
10
+ * a second accepted spelling is a second thing to get wrong later.
11
+ */
12
+ export declare const ACTION_SIGNATURE_HEADER = "zitadel-signature";
13
+ /**
14
+ * How far the signature's own timestamp may be from now, matching Zitadel's
15
+ * `DefaultTolerance`. It is what bounds replay: the payload is signed over the
16
+ * timestamp, so an old capture cannot be re-dated without breaking the MAC.
17
+ */
18
+ export declare const ACTION_SIGNATURE_TOLERANCE_SECONDS = 300;
19
+ /**
20
+ * The user-lifecycle events that must end an r10c session.
21
+ *
22
+ * All three are `user` aggregate events, so `aggregateID` is the Zitadel user id
23
+ * in every case — the same value that arrives as `sub` in an `id_token` and is
24
+ * stored as the account's `external-subject` identifier. That is what lets one
25
+ * handler serve all three without knowing anything about them individually.
26
+ */
27
+ export declare const PROVIDER_USER_LIFECYCLE_EVENTS: readonly ["user.deactivated", "user.locked", "user.removed"];
28
+ export type ProviderUserLifecycleEvent = (typeof PROVIDER_USER_LIFECYCLE_EVENTS)[number];
29
+ /** A verified Actions v2 event, reduced to what session revocation needs. */
30
+ export interface ProviderUserEvent {
31
+ /** e.g. `user.deactivated`. Kept for logging and for the no-op branch. */
32
+ readonly eventType: string;
33
+ /** The `aggregateID`: the provider's user id, i.e. the OIDC `sub`. */
34
+ readonly subject: string;
35
+ /**
36
+ * True when {@link eventType} is one this platform acts on. An execution can be
37
+ * added at the provider without a deploy here, so an unrecognised type must be
38
+ * a no-op rather than an error.
39
+ */
40
+ readonly revokesSessions: boolean;
41
+ }
42
+ /** How the verifier is pointed at a target. Resolved from config-service at boot. */
43
+ export interface ZitadelActionsConfig {
44
+ /**
45
+ * The key Zitadel minted when the target was created. It is returned **only**
46
+ * from `CreateTarget` and can never be read back, which is why
47
+ * `tools/zitadel-seed.mjs` stamps it into `.generated.env` and carries it
48
+ * forward across re-seeds.
49
+ */
50
+ readonly signingKey: string;
51
+ }
52
+ export interface ZitadelActions {
53
+ /**
54
+ * Verify a webhook call and say what it asks for.
55
+ *
56
+ * Takes the **raw** body, not a parsed object: the MAC covers the bytes
57
+ * Zitadel sent, and a re-serialised object is a different sequence of bytes.
58
+ */
59
+ verifyEvent(rawBody: string, signatureHeader: string | undefined): Effect.Effect<ProviderUserEvent, EntifixLogicError>;
60
+ }
61
+ declare const ZitadelActionsTag_base: Context.TagClass<ZitadelActionsTag, "ZitadelActionsTag", ZitadelActions>;
62
+ export declare class ZitadelActionsTag extends ZitadelActionsTag_base {
63
+ }
64
+ /**
65
+ * The security boundary of this file: whether a request that named a user
66
+ * deactivation really came from the identity provider.
67
+ *
68
+ * The endpoint it guards is unauthenticated by necessity — the caller is a
69
+ * server holding no cookie — exactly as the back-channel logout route is
70
+ * (ADR 0017). There the token's own signature is the authentication; here it is
71
+ * this HMAC, computed the way Zitadel's `pkg/actions/signing.go` computes it:
72
+ * `HMAC-SHA256(signingKey, "<unix timestamp>.<raw body>")`, hex.
73
+ *
74
+ * An empty key **fails closed**. A fleet whose signing key never arrived (the
75
+ * seed did not run, the config row is blank) must reject every call rather than
76
+ * accept anonymous requests to revoke anyone's sessions — the one failure mode
77
+ * that would be strictly worse than the bug this closes.
78
+ */
79
+ export declare const verifyActionSignature: (rawBody: string, signatureHeader: string | undefined, signingKey: string, nowSeconds?: number) => boolean;
80
+ /**
81
+ * A verifier for the Actions v2 webhook a Zitadel event execution calls.
82
+ *
83
+ * Deliberately separate from {@link ZitadelOidc}: that one speaks OIDC and
84
+ * verifies asymmetric provider JWTs against a published key set, this one
85
+ * verifies a shared-secret MAC over an arbitrary JSON body. Folding them
86
+ * together would put a symmetric key next to the pinned-`algorithms` verifier
87
+ * whose whole purpose is that no symmetric key is ever accepted there.
88
+ */
89
+ export declare const makeZitadelActions: (config: ZitadelActionsConfig) => ZitadelActions;
90
+ /** Binds {@link ZitadelActionsTag} from a resolved configuration. */
91
+ export declare const ZitadelActionsLayer: (config: ZitadelActionsConfig) => Layer.Layer<ZitadelActionsTag>;
92
+ export {};
93
+ //# sourceMappingURL=action-webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-webhook.d.ts","sourceRoot":"","sources":["../../src/actions/action-webhook.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,8DAIjC,CAAC;AAEX,MAAM,MAAM,0BAA0B,GACpC,CAAC,OAAO,8BAA8B,CAAC,CAAC,MAAM,CAAC,CAAC;AAElD,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,WAAW,CACT,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;CACxD;;AAED,qBAAa,iBAAkB,SAAQ,sBAGpC;CAAG;AAkCN;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,EACf,iBAAiB,MAAM,GAAG,SAAS,EACnC,YAAY,MAAM,EAClB,aAAY,MAAsC,KACjD,OAeF,CAAC;AA8BF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,oBAAoB,KAC3B,cAiBD,CAAC;AAEH,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,oBAAoB,KAC3B,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAC8B,CAAC"}
@@ -0,0 +1,126 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { EntifixLogicError } from '@entifix/core';
3
+ import { Context, Effect, Layer } from 'effect';
4
+ /**
5
+ * The header Zitadel signs an Actions v2 payload with.
6
+ *
7
+ * Read from the sender, not from the docs: `internal/execution/execution.go`
8
+ * sets `actions.SigningHeader`, which is this exact spelling. The `CreateTarget`
9
+ * proto comment claims `X-ZITADEL-Signature` and is wrong for v4.16.2 — we pin
10
+ * the one the running instance actually sends rather than accepting both, since
11
+ * a second accepted spelling is a second thing to get wrong later.
12
+ */ export const ACTION_SIGNATURE_HEADER = 'zitadel-signature';
13
+ /**
14
+ * How far the signature's own timestamp may be from now, matching Zitadel's
15
+ * `DefaultTolerance`. It is what bounds replay: the payload is signed over the
16
+ * timestamp, so an old capture cannot be re-dated without breaking the MAC.
17
+ */ export const ACTION_SIGNATURE_TOLERANCE_SECONDS = 300;
18
+ /**
19
+ * The user-lifecycle events that must end an r10c session.
20
+ *
21
+ * All three are `user` aggregate events, so `aggregateID` is the Zitadel user id
22
+ * in every case — the same value that arrives as `sub` in an `id_token` and is
23
+ * stored as the account's `external-subject` identifier. That is what lets one
24
+ * handler serve all three without knowing anything about them individually.
25
+ */ export const PROVIDER_USER_LIFECYCLE_EVENTS = [
26
+ 'user.deactivated',
27
+ 'user.locked',
28
+ 'user.removed'
29
+ ];
30
+ export class ZitadelActionsTag extends Context.Tag('ZitadelActionsTag')() {
31
+ }
32
+ /**
33
+ * `t=<unix>,v1=<hex>` — and there may be more than one `v1=`, because Zitadel's
34
+ * `ComputeSignatureHeader` takes a list of keys and appends one part per key.
35
+ * Accepting only the first would break the moment a key is rotated with an
36
+ * overlap window.
37
+ */ const parseSignatureHeader = (header)=>{
38
+ let timestamp;
39
+ const signatures = [];
40
+ for (const part of header.split(',')){
41
+ const [name, value] = part.trim().split('=', 2);
42
+ if (value === undefined || value === '') continue;
43
+ if (name === 't') timestamp = Number(value);
44
+ if (name === 'v1') signatures.push(value);
45
+ }
46
+ if (timestamp === undefined || !Number.isFinite(timestamp)) return undefined;
47
+ if (signatures.length === 0) return undefined;
48
+ return {
49
+ timestamp,
50
+ signatures
51
+ };
52
+ };
53
+ const signaturesMatch = (expected, candidate)=>{
54
+ const actual = Buffer.from(candidate, 'hex');
55
+ // `timingSafeEqual` throws on a length mismatch, and a wrong-length signature
56
+ // is a mismatch we can answer without looking at the bytes anyway.
57
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
58
+ };
59
+ /**
60
+ * The security boundary of this file: whether a request that named a user
61
+ * deactivation really came from the identity provider.
62
+ *
63
+ * The endpoint it guards is unauthenticated by necessity — the caller is a
64
+ * server holding no cookie — exactly as the back-channel logout route is
65
+ * (ADR 0017). There the token's own signature is the authentication; here it is
66
+ * this HMAC, computed the way Zitadel's `pkg/actions/signing.go` computes it:
67
+ * `HMAC-SHA256(signingKey, "<unix timestamp>.<raw body>")`, hex.
68
+ *
69
+ * An empty key **fails closed**. A fleet whose signing key never arrived (the
70
+ * seed did not run, the config row is blank) must reject every call rather than
71
+ * accept anonymous requests to revoke anyone's sessions — the one failure mode
72
+ * that would be strictly worse than the bug this closes.
73
+ */ export const verifyActionSignature = (rawBody, signatureHeader, signingKey, nowSeconds = Math.floor(Date.now() / 1000))=>{
74
+ if (signingKey === '' || signatureHeader === undefined) return false;
75
+ const parsed = parseSignatureHeader(signatureHeader);
76
+ if (parsed === undefined) return false;
77
+ if (Math.abs(nowSeconds - parsed.timestamp) > ACTION_SIGNATURE_TOLERANCE_SECONDS) {
78
+ return false;
79
+ }
80
+ const expected = createHmac('sha256', signingKey).update(`${String(parsed.timestamp)}.${rawBody}`).digest();
81
+ return parsed.signatures.some((candidate)=>signaturesMatch(expected, candidate));
82
+ };
83
+ const isLifecycleEvent = (eventType)=>PROVIDER_USER_LIFECYCLE_EVENTS.includes(eventType);
84
+ /**
85
+ * Zitadel's event payload, of which only two fields matter here. `event_type` is
86
+ * snake_case in the JSON while `aggregateID` is not — that asymmetry is the
87
+ * provider's, not a typo.
88
+ */ const readEvent = (rawBody)=>{
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(rawBody);
92
+ } catch (unused) {
93
+ return undefined;
94
+ }
95
+ if (typeof parsed !== 'object' || parsed === null) return undefined;
96
+ const body = parsed;
97
+ const eventType = body['event_type'];
98
+ const subject = body['aggregateID'];
99
+ if (typeof eventType !== 'string' || eventType === '') return undefined;
100
+ if (typeof subject !== 'string' || subject === '') return undefined;
101
+ return {
102
+ eventType,
103
+ subject,
104
+ revokesSessions: isLifecycleEvent(eventType)
105
+ };
106
+ };
107
+ /**
108
+ * A verifier for the Actions v2 webhook a Zitadel event execution calls.
109
+ *
110
+ * Deliberately separate from {@link ZitadelOidc}: that one speaks OIDC and
111
+ * verifies asymmetric provider JWTs against a published key set, this one
112
+ * verifies a shared-secret MAC over an arbitrary JSON body. Folding them
113
+ * together would put a symmetric key next to the pinned-`algorithms` verifier
114
+ * whose whole purpose is that no symmetric key is ever accepted there.
115
+ */ export const makeZitadelActions = (config)=>({
116
+ verifyEvent: (rawBody, signatureHeader)=>Effect.suspend(()=>{
117
+ if (!verifyActionSignature(rawBody, signatureHeader, config.signingKey)) {
118
+ return Effect.fail(new EntifixLogicError('the action payload signature did not verify'));
119
+ }
120
+ const event = readEvent(rawBody);
121
+ return event === undefined ? Effect.fail(new EntifixLogicError('the action payload is not a user event: no event_type or aggregateID')) : Effect.succeed(event);
122
+ })
123
+ });
124
+ /** Binds {@link ZitadelActionsTag} from a resolved configuration. */ export const ZitadelActionsLayer = (config)=>Layer.succeed(ZitadelActionsTag, makeZitadelActions(config));
125
+
126
+ //# sourceMappingURL=action-webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/actions/action-webhook.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport { EntifixLogicError } from '@entifix/core';\nimport { Context, Effect, Layer } from 'effect';\n\n/**\n * The header Zitadel signs an Actions v2 payload with.\n *\n * Read from the sender, not from the docs: `internal/execution/execution.go`\n * sets `actions.SigningHeader`, which is this exact spelling. The `CreateTarget`\n * proto comment claims `X-ZITADEL-Signature` and is wrong for v4.16.2 — we pin\n * the one the running instance actually sends rather than accepting both, since\n * a second accepted spelling is a second thing to get wrong later.\n */\nexport const ACTION_SIGNATURE_HEADER = 'zitadel-signature';\n\n/**\n * How far the signature's own timestamp may be from now, matching Zitadel's\n * `DefaultTolerance`. It is what bounds replay: the payload is signed over the\n * timestamp, so an old capture cannot be re-dated without breaking the MAC.\n */\nexport const ACTION_SIGNATURE_TOLERANCE_SECONDS = 300;\n\n/**\n * The user-lifecycle events that must end an r10c session.\n *\n * All three are `user` aggregate events, so `aggregateID` is the Zitadel user id\n * in every case — the same value that arrives as `sub` in an `id_token` and is\n * stored as the account's `external-subject` identifier. That is what lets one\n * handler serve all three without knowing anything about them individually.\n */\nexport const PROVIDER_USER_LIFECYCLE_EVENTS = [\n 'user.deactivated',\n 'user.locked',\n 'user.removed',\n] as const;\n\nexport type ProviderUserLifecycleEvent =\n (typeof PROVIDER_USER_LIFECYCLE_EVENTS)[number];\n\n/** A verified Actions v2 event, reduced to what session revocation needs. */\nexport interface ProviderUserEvent {\n /** e.g. `user.deactivated`. Kept for logging and for the no-op branch. */\n readonly eventType: string;\n /** The `aggregateID`: the provider's user id, i.e. the OIDC `sub`. */\n readonly subject: string;\n /**\n * True when {@link eventType} is one this platform acts on. An execution can be\n * added at the provider without a deploy here, so an unrecognised type must be\n * a no-op rather than an error.\n */\n readonly revokesSessions: boolean;\n}\n\n/** How the verifier is pointed at a target. Resolved from config-service at boot. */\nexport interface ZitadelActionsConfig {\n /**\n * The key Zitadel minted when the target was created. It is returned **only**\n * from `CreateTarget` and can never be read back, which is why\n * `tools/zitadel-seed.mjs` stamps it into `.generated.env` and carries it\n * forward across re-seeds.\n */\n readonly signingKey: string;\n}\n\nexport interface ZitadelActions {\n /**\n * Verify a webhook call and say what it asks for.\n *\n * Takes the **raw** body, not a parsed object: the MAC covers the bytes\n * Zitadel sent, and a re-serialised object is a different sequence of bytes.\n */\n verifyEvent(\n rawBody: string,\n signatureHeader: string | undefined,\n ): Effect.Effect<ProviderUserEvent, EntifixLogicError>;\n}\n\nexport class ZitadelActionsTag extends Context.Tag('ZitadelActionsTag')<\n ZitadelActionsTag,\n ZitadelActions\n>() {}\n\ninterface ParsedSignature {\n readonly timestamp: number;\n readonly signatures: readonly string[];\n}\n\n/**\n * `t=<unix>,v1=<hex>` — and there may be more than one `v1=`, because Zitadel's\n * `ComputeSignatureHeader` takes a list of keys and appends one part per key.\n * Accepting only the first would break the moment a key is rotated with an\n * overlap window.\n */\nconst parseSignatureHeader = (header: string): ParsedSignature | undefined => {\n let timestamp: number | undefined;\n const signatures: string[] = [];\n for (const part of header.split(',')) {\n const [name, value] = part.trim().split('=', 2);\n if (value === undefined || value === '') continue;\n if (name === 't') timestamp = Number(value);\n if (name === 'v1') signatures.push(value);\n }\n if (timestamp === undefined || !Number.isFinite(timestamp)) return undefined;\n if (signatures.length === 0) return undefined;\n return { timestamp, signatures };\n};\n\nconst signaturesMatch = (expected: Buffer, candidate: string): boolean => {\n const actual = Buffer.from(candidate, 'hex');\n // `timingSafeEqual` throws on a length mismatch, and a wrong-length signature\n // is a mismatch we can answer without looking at the bytes anyway.\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n};\n\n/**\n * The security boundary of this file: whether a request that named a user\n * deactivation really came from the identity provider.\n *\n * The endpoint it guards is unauthenticated by necessity — the caller is a\n * server holding no cookie — exactly as the back-channel logout route is\n * (ADR 0017). There the token's own signature is the authentication; here it is\n * this HMAC, computed the way Zitadel's `pkg/actions/signing.go` computes it:\n * `HMAC-SHA256(signingKey, \"<unix timestamp>.<raw body>\")`, hex.\n *\n * An empty key **fails closed**. A fleet whose signing key never arrived (the\n * seed did not run, the config row is blank) must reject every call rather than\n * accept anonymous requests to revoke anyone's sessions — the one failure mode\n * that would be strictly worse than the bug this closes.\n */\nexport const verifyActionSignature = (\n rawBody: string,\n signatureHeader: string | undefined,\n signingKey: string,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): boolean => {\n if (signingKey === '' || signatureHeader === undefined) return false;\n const parsed = parseSignatureHeader(signatureHeader);\n if (parsed === undefined) return false;\n if (\n Math.abs(nowSeconds - parsed.timestamp) > ACTION_SIGNATURE_TOLERANCE_SECONDS\n ) {\n return false;\n }\n const expected = createHmac('sha256', signingKey)\n .update(`${String(parsed.timestamp)}.${rawBody}`)\n .digest();\n return parsed.signatures.some(candidate =>\n signaturesMatch(expected, candidate),\n );\n};\n\nconst isLifecycleEvent = (eventType: string): boolean =>\n (PROVIDER_USER_LIFECYCLE_EVENTS as readonly string[]).includes(eventType);\n\n/**\n * Zitadel's event payload, of which only two fields matter here. `event_type` is\n * snake_case in the JSON while `aggregateID` is not — that asymmetry is the\n * provider's, not a typo.\n */\nconst readEvent = (rawBody: string): ProviderUserEvent | undefined => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawBody);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const body = parsed as Record<string, unknown>;\n const eventType = body['event_type'];\n const subject = body['aggregateID'];\n if (typeof eventType !== 'string' || eventType === '') return undefined;\n if (typeof subject !== 'string' || subject === '') return undefined;\n return {\n eventType,\n subject,\n revokesSessions: isLifecycleEvent(eventType),\n };\n};\n\n/**\n * A verifier for the Actions v2 webhook a Zitadel event execution calls.\n *\n * Deliberately separate from {@link ZitadelOidc}: that one speaks OIDC and\n * verifies asymmetric provider JWTs against a published key set, this one\n * verifies a shared-secret MAC over an arbitrary JSON body. Folding them\n * together would put a symmetric key next to the pinned-`algorithms` verifier\n * whose whole purpose is that no symmetric key is ever accepted there.\n */\nexport const makeZitadelActions = (\n config: ZitadelActionsConfig,\n): ZitadelActions => ({\n verifyEvent: (rawBody, signatureHeader) =>\n Effect.suspend(() => {\n if (!verifyActionSignature(rawBody, signatureHeader, config.signingKey)) {\n return Effect.fail(\n new EntifixLogicError('the action payload signature did not verify'),\n );\n }\n const event = readEvent(rawBody);\n return event === undefined\n ? Effect.fail(\n new EntifixLogicError(\n 'the action payload is not a user event: no event_type or aggregateID',\n ),\n )\n : Effect.succeed(event);\n }),\n});\n\n/** Binds {@link ZitadelActionsTag} from a resolved configuration. */\nexport const ZitadelActionsLayer = (\n config: ZitadelActionsConfig,\n): Layer.Layer<ZitadelActionsTag> =>\n Layer.succeed(ZitadelActionsTag, makeZitadelActions(config));\n"],"names":["createHmac","timingSafeEqual","EntifixLogicError","Context","Effect","Layer","ACTION_SIGNATURE_HEADER","ACTION_SIGNATURE_TOLERANCE_SECONDS","PROVIDER_USER_LIFECYCLE_EVENTS","ZitadelActionsTag","Tag","parseSignatureHeader","header","timestamp","signatures","part","split","name","value","trim","undefined","Number","push","isFinite","length","signaturesMatch","expected","candidate","actual","Buffer","from","verifyActionSignature","rawBody","signatureHeader","signingKey","nowSeconds","Math","floor","Date","now","parsed","abs","update","String","digest","some","isLifecycleEvent","eventType","includes","readEvent","JSON","parse","body","subject","revokesSessions","makeZitadelActions","config","verifyEvent","suspend","fail","event","succeed","ZitadelActionsLayer"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { HealthRegistryTag } from '@entifix/business';
2
+ import { Layer } from 'effect';
3
+ /** Probe name reported by `/api/health/ready` when Zitadel is unreachable. */
4
+ export declare const ZITADEL_PROBE_NAME = "zitadel";
5
+ /**
6
+ * Registers a readiness probe against the instance's `/debug/ready`.
7
+ *
8
+ * A service that cannot reach Zitadel can still answer liveness — its process
9
+ * is fine — while readiness reports the truth, which is the split Kubernetes
10
+ * acts on: drain traffic, do not restart.
11
+ *
12
+ * The endpoint is unauthenticated on purpose: a readiness check must not spend
13
+ * a credential, and `/debug/ready` reveals nothing beyond "this instance is
14
+ * serving". The deadline matters as much as the check, because an instance that
15
+ * is mid-init accepts the connection and then simply does not answer — which
16
+ * would leave readiness hanging at the one moment it must reply.
17
+ */
18
+ export declare const ZitadelHealthProbeLayer: (issuer: string) => Layer.Layer<never, never, HealthRegistryTag>;
19
+ //# sourceMappingURL=zitadel-health-probe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zitadel-health-probe.d.ts","sourceRoot":"","sources":["../../src/health/zitadel-health-probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAK5C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,KACb,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,iBAAiB,CAsB3C,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { HealthRegistryTag } from '@entifix/business';
2
+ import { Effect, Layer } from 'effect';
3
+ /** Probe name reported by `/api/health/ready` when Zitadel is unreachable. */ export const ZITADEL_PROBE_NAME = 'zitadel';
4
+ /** How long the probe waits before calling the instance unreachable. */ const PROBE_TIMEOUT_MILLIS = 2000;
5
+ /**
6
+ * Registers a readiness probe against the instance's `/debug/ready`.
7
+ *
8
+ * A service that cannot reach Zitadel can still answer liveness — its process
9
+ * is fine — while readiness reports the truth, which is the split Kubernetes
10
+ * acts on: drain traffic, do not restart.
11
+ *
12
+ * The endpoint is unauthenticated on purpose: a readiness check must not spend
13
+ * a credential, and `/debug/ready` reveals nothing beyond "this instance is
14
+ * serving". The deadline matters as much as the check, because an instance that
15
+ * is mid-init accepts the connection and then simply does not answer — which
16
+ * would leave readiness hanging at the one moment it must reply.
17
+ */ export const ZitadelHealthProbeLayer = (issuer)=>Layer.effectDiscard(Effect.gen(function*() {
18
+ const registry = yield* HealthRegistryTag;
19
+ const url = `${issuer.replace(/\/$/, '')}/debug/ready`;
20
+ yield* registry.register({
21
+ name: ZITADEL_PROBE_NAME,
22
+ kind: 'upstream',
23
+ // A foreign system, deliberately not a Store (`docs/_shared/planes.md`):
24
+ // it has an owner, and the owner is not us.
25
+ targets: [
26
+ ZITADEL_PROBE_NAME
27
+ ],
28
+ check: Effect.tryPromise(()=>fetch(url, {
29
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MILLIS)
30
+ })).pipe(Effect.map((response)=>response.ok), Effect.catchAll(()=>Effect.succeed(false)))
31
+ });
32
+ }));
33
+
34
+ //# sourceMappingURL=zitadel-health-probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/health/zitadel-health-probe.ts"],"sourcesContent":["import { HealthRegistryTag } from '@entifix/business';\nimport { Effect, Layer } from 'effect';\n\n/** Probe name reported by `/api/health/ready` when Zitadel is unreachable. */\nexport const ZITADEL_PROBE_NAME = 'zitadel';\n\n/** How long the probe waits before calling the instance unreachable. */\nconst PROBE_TIMEOUT_MILLIS = 2_000;\n\n/**\n * Registers a readiness probe against the instance's `/debug/ready`.\n *\n * A service that cannot reach Zitadel can still answer liveness — its process\n * is fine — while readiness reports the truth, which is the split Kubernetes\n * acts on: drain traffic, do not restart.\n *\n * The endpoint is unauthenticated on purpose: a readiness check must not spend\n * a credential, and `/debug/ready` reveals nothing beyond \"this instance is\n * serving\". The deadline matters as much as the check, because an instance that\n * is mid-init accepts the connection and then simply does not answer — which\n * would leave readiness hanging at the one moment it must reply.\n */\nexport const ZitadelHealthProbeLayer = (\n issuer: string,\n): Layer.Layer<never, never, HealthRegistryTag> =>\n Layer.effectDiscard(\n Effect.gen(function* () {\n const registry = yield* HealthRegistryTag;\n const url = `${issuer.replace(/\\/$/, '')}/debug/ready`;\n\n yield* registry.register({\n name: ZITADEL_PROBE_NAME,\n kind: 'upstream',\n // A foreign system, deliberately not a Store (`docs/_shared/planes.md`):\n // it has an owner, and the owner is not us.\n targets: [ZITADEL_PROBE_NAME],\n check: Effect.tryPromise(() =>\n fetch(url, {\n signal: AbortSignal.timeout(PROBE_TIMEOUT_MILLIS),\n }),\n ).pipe(\n Effect.map(response => response.ok),\n Effect.catchAll(() => Effect.succeed(false)),\n ),\n });\n }),\n );\n"],"names":["HealthRegistryTag","Effect","Layer","ZITADEL_PROBE_NAME","PROBE_TIMEOUT_MILLIS","ZitadelHealthProbeLayer","issuer","effectDiscard","gen","registry","url","replace","register","name","kind","targets","check","tryPromise","fetch","signal","AbortSignal","timeout","pipe","map","response","ok","catchAll","succeed"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,oBAAoB;AACtD,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAEvC,4EAA4E,GAC5E,OAAO,MAAMC,qBAAqB,UAAU;AAE5C,sEAAsE,GACtE,MAAMC,uBAAuB;AAE7B;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,0BAA0B,CACrCC,SAEAJ,MAAMK,aAAa,CACjBN,OAAOO,GAAG,CAAC;QACT,MAAMC,WAAW,OAAOT;QACxB,MAAMU,MAAM,GAAGJ,OAAOK,OAAO,CAAC,OAAO,IAAI,YAAY,CAAC;QAEtD,OAAOF,SAASG,QAAQ,CAAC;YACvBC,MAAMV;YACNW,MAAM;YACN,yEAAyE;YACzE,4CAA4C;YAC5CC,SAAS;gBAACZ;aAAmB;YAC7Ba,OAAOf,OAAOgB,UAAU,CAAC,IACvBC,MAAMR,KAAK;oBACTS,QAAQC,YAAYC,OAAO,CAACjB;gBAC9B,IACAkB,IAAI,CACJrB,OAAOsB,GAAG,CAACC,CAAAA,WAAYA,SAASC,EAAE,GAClCxB,OAAOyB,QAAQ,CAAC,IAAMzB,OAAO0B,OAAO,CAAC;QAEzC;IACF,IACA"}
@@ -0,0 +1,7 @@
1
+ export * from './actions/action-webhook.js';
2
+ export * from './health/zitadel-health-probe.js';
3
+ export * from './management/management-client.js';
4
+ export * from './oidc/discovery.js';
5
+ export * from './oidc/oidc-client.js';
6
+ export * from './oidc/pkce.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './actions/action-webhook.js';
2
+ export * from './health/zitadel-health-probe.js';
3
+ export * from './management/management-client.js';
4
+ export * from './oidc/discovery.js';
5
+ export * from './oidc/oidc-client.js';
6
+ export * from './oidc/pkce.js';
7
+
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './actions/action-webhook.js';\nexport * from './health/zitadel-health-probe.js';\nexport * from './management/management-client.js';\nexport * from './oidc/discovery.js';\nexport * from './oidc/oidc-client.js';\nexport * from './oidc/pkce.js';\n"],"names":[],"mappings":"AAAA,cAAc,8BAA8B;AAC5C,cAAc,mCAAmC;AACjD,cAAc,oCAAoC;AAClD,cAAc,sBAAsB;AACpC,cAAc,wBAAwB;AACtC,cAAc,iBAAiB"}
@@ -0,0 +1,102 @@
1
+ import { EntifixConnError } from '@entifix/core';
2
+ import { Context, Effect, Layer } from 'effect';
3
+ /** How the management client reaches an instance. */
4
+ export interface ZitadelManagementConfig {
5
+ readonly issuer: string;
6
+ /**
7
+ * Personal access token of the seed machine user. Zitadel mints it at first
8
+ * init and `infra/local/ensure.sh` extracts it, so it is per-instance and
9
+ * disposable rather than a configured constant.
10
+ */
11
+ readonly personalAccessToken: string;
12
+ }
13
+ /** A human user as this client cares about it. */
14
+ export interface ZitadelUser {
15
+ readonly userId: string;
16
+ readonly username: string;
17
+ readonly email?: string;
18
+ readonly emailVerified: boolean;
19
+ readonly displayName?: string;
20
+ readonly active: boolean;
21
+ }
22
+ /** What it takes to create one. */
23
+ export interface ZitadelHumanInput {
24
+ readonly username: string;
25
+ readonly email: string;
26
+ readonly emailVerified?: boolean;
27
+ readonly givenName: string;
28
+ readonly familyName: string;
29
+ readonly displayName?: string;
30
+ /** Optional: a user created without one signs in through a social provider. */
31
+ readonly password?: string;
32
+ readonly passwordChangeRequired?: boolean;
33
+ }
34
+ /** What to ask the event store for. */
35
+ export interface ZitadelEventQuery {
36
+ /** Event types to match, e.g. `user.deactivated`. */
37
+ readonly eventTypes: readonly string[];
38
+ /** Oldest event to return, as an ISO timestamp. */
39
+ readonly since: string;
40
+ /** Page size. The caller sweeps on an interval, so this is a ceiling. */
41
+ readonly limit: number;
42
+ }
43
+ /**
44
+ * One event, reduced to what a reconciler needs.
45
+ *
46
+ * ⚠️ **`payload` is deliberately dropped.** A `user.human.added` event carries
47
+ * the user's `encodedHash` — a bcrypt hash of their password — and this fleet
48
+ * has no reason to hold one, log one, or move one across a process boundary.
49
+ * What a reconciler acts on is *which user* and *when*, which is the whole of
50
+ * this shape.
51
+ */
52
+ export interface ZitadelEvent {
53
+ /** The aggregate the event is about — for a user event, the user id. */
54
+ readonly subject: string;
55
+ /** The event's own type, e.g. `user.locked`. */
56
+ readonly type: string;
57
+ /** When the instance recorded it, as an ISO timestamp. */
58
+ readonly createdAt: string;
59
+ }
60
+ export interface ZitadelManagement {
61
+ findUserByEmail(email: string): Effect.Effect<ZitadelUser | null, EntifixConnError>;
62
+ getUser(userId: string): Effect.Effect<ZitadelUser | null, EntifixConnError>;
63
+ createHuman(input: ZitadelHumanInput): Effect.Effect<string, EntifixConnError>;
64
+ updateProfile(userId: string, profile: {
65
+ readonly givenName: string;
66
+ readonly familyName: string;
67
+ readonly displayName?: string;
68
+ }): Effect.Effect<void, EntifixConnError>;
69
+ setActive(userId: string, active: boolean): Effect.Effect<void, EntifixConnError>;
70
+ /** Compensating delete. Used when a provisioning half-write must be undone. */
71
+ deleteUser(userId: string): Effect.Effect<void, EntifixConnError>;
72
+ /**
73
+ * Lifecycle events the instance recorded, oldest first, from a cursor.
74
+ *
75
+ * The one read here that is not about a single user: it answers "what
76
+ * happened while I was not listening". Zitadel's Actions v2 target is
77
+ * `restAsync` — it sends the call and ignores the response, and it does not
78
+ * retry — so an event fired while auth-service is down is gone for good
79
+ * ([ADR 0019](../../../../../../docs/adr/0019-provider-user-lifecycle-events-revoke-sessions.md)).
80
+ */
81
+ searchEvents(query: ZitadelEventQuery): Effect.Effect<readonly ZitadelEvent[], EntifixConnError>;
82
+ }
83
+ declare const ZitadelManagementTag_base: Context.TagClass<ZitadelManagementTag, "ZitadelManagementTag", ZitadelManagement>;
84
+ export declare class ZitadelManagementTag extends ZitadelManagementTag_base {
85
+ }
86
+ /**
87
+ * The slice of Zitadel's management API r10c drives.
88
+ *
89
+ * Only user lifecycle: everything about *credentials* — passwords, TOTP, linked
90
+ * social accounts, recovery — is Zitadel's alone and has no method here on
91
+ * purpose. r10c must not be able to read or set a credential, because the whole
92
+ * point of the swap is that it no longer holds any.
93
+ *
94
+ * Endpoints are a deliberate mix of API versions, matching what Zitadel v4
95
+ * actually offers: search is v1 (the v2 surface has no equivalent email query),
96
+ * everything else is v2.
97
+ */
98
+ export declare const makeZitadelManagement: (config: ZitadelManagementConfig) => ZitadelManagement;
99
+ /** Binds {@link ZitadelManagementTag} from a resolved configuration. */
100
+ export declare const ZitadelManagementLayer: (config: ZitadelManagementConfig) => Layer.Layer<ZitadelManagementTag>;
101
+ export {};
102
+ //# sourceMappingURL=management-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"management-client.d.ts","sourceRoot":"","sources":["../../src/management/management-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,152 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { EntifixConnError } from '@entifix/core';
3
+ import { Context, Effect, Layer } from 'effect';
4
+ export class ZitadelManagementTag extends Context.Tag('ZitadelManagementTag')() {
5
+ }
6
+ const ACTIVE = 'USER_STATE_ACTIVE';
7
+ /**
8
+ * The slice of Zitadel's management API r10c drives.
9
+ *
10
+ * Only user lifecycle: everything about *credentials* — passwords, TOTP, linked
11
+ * social accounts, recovery — is Zitadel's alone and has no method here on
12
+ * purpose. r10c must not be able to read or set a credential, because the whole
13
+ * point of the swap is that it no longer holds any.
14
+ *
15
+ * Endpoints are a deliberate mix of API versions, matching what Zitadel v4
16
+ * actually offers: search is v1 (the v2 surface has no equivalent email query),
17
+ * everything else is v2.
18
+ */ export const makeZitadelManagement = (config)=>{
19
+ const base = config.issuer.replace(/\/$/, '');
20
+ const call = (method, path, body)=>Effect.tryPromise({
21
+ try: async ()=>{
22
+ const response = await fetch(`${base}${path}`, _extends({
23
+ method,
24
+ headers: {
25
+ authorization: `Bearer ${config.personalAccessToken}`,
26
+ 'content-type': 'application/json'
27
+ }
28
+ }, body === undefined ? {} : {
29
+ body: JSON.stringify(body)
30
+ }));
31
+ const text = await response.text();
32
+ const payload = text === '' ? {} : JSON.parse(text);
33
+ if (!response.ok) {
34
+ var _payload_message;
35
+ const message = (_payload_message = payload.message) != null ? _payload_message : `${method} ${path} answered ${String(response.status)}`;
36
+ throw new Error(message);
37
+ }
38
+ return payload;
39
+ },
40
+ catch: (error)=>new EntifixConnError(`zitadel management call failed (${method} ${path}): ${String(error)}`)
41
+ });
42
+ const findUserByEmail = (email)=>call('POST', '/management/v1/users/_search', {
43
+ queries: [
44
+ {
45
+ emailQuery: {
46
+ emailAddress: email,
47
+ method: 'TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE'
48
+ }
49
+ }
50
+ ]
51
+ }).pipe(Effect.map((payload)=>{
52
+ var _payload_result, _row_userName;
53
+ var _row_human_email, _row_human, _row_human_email1, _row_human1, _row_human_profile, _row_human2;
54
+ const row = ((_payload_result = payload.result) != null ? _payload_result : [])[0];
55
+ if ((row == null ? void 0 : row.id) === undefined) return null;
56
+ return {
57
+ userId: row.id,
58
+ username: (_row_userName = row.userName) != null ? _row_userName : '',
59
+ email: (_row_human = row.human) == null ? void 0 : (_row_human_email = _row_human.email) == null ? void 0 : _row_human_email.email,
60
+ emailVerified: ((_row_human1 = row.human) == null ? void 0 : (_row_human_email1 = _row_human1.email) == null ? void 0 : _row_human_email1.isEmailVerified) === true,
61
+ displayName: (_row_human2 = row.human) == null ? void 0 : (_row_human_profile = _row_human2.profile) == null ? void 0 : _row_human_profile.displayName,
62
+ active: row.state === ACTIVE
63
+ };
64
+ }));
65
+ const getUser = (userId)=>call('GET', `/v2/users/${userId}`).pipe(Effect.map((payload)=>{
66
+ var _user_username;
67
+ var _user_human_email, _user_human, _user_human_email1, _user_human1, _user_human_profile, _user_human2;
68
+ const user = payload.user;
69
+ if ((user == null ? void 0 : user.userId) === undefined) return null;
70
+ return {
71
+ userId: user.userId,
72
+ username: (_user_username = user.username) != null ? _user_username : '',
73
+ email: (_user_human = user.human) == null ? void 0 : (_user_human_email = _user_human.email) == null ? void 0 : _user_human_email.email,
74
+ emailVerified: ((_user_human1 = user.human) == null ? void 0 : (_user_human_email1 = _user_human1.email) == null ? void 0 : _user_human_email1.isVerified) === true,
75
+ displayName: (_user_human2 = user.human) == null ? void 0 : (_user_human_profile = _user_human2.profile) == null ? void 0 : _user_human_profile.displayName,
76
+ active: user.state === ACTIVE
77
+ };
78
+ }), // A deleted or never-existing id is a `404`, which is an answer rather
79
+ // than a failure: callers ask precisely because they do not know.
80
+ Effect.catchAll((error)=>/404|NotFound|doesn't exist/i.test(error.message) ? Effect.succeed(null) : Effect.fail(error)));
81
+ const createHuman = (input)=>call('POST', '/v2/users/human', _extends({
82
+ username: input.username,
83
+ profile: _extends({
84
+ givenName: input.givenName,
85
+ familyName: input.familyName
86
+ }, input.displayName === undefined ? {} : {
87
+ displayName: input.displayName
88
+ }),
89
+ email: {
90
+ email: input.email,
91
+ isVerified: input.emailVerified === true
92
+ }
93
+ }, input.password === undefined ? {} : {
94
+ password: {
95
+ password: input.password,
96
+ changeRequired: input.passwordChangeRequired === true
97
+ }
98
+ })).pipe(Effect.flatMap((payload)=>payload.userId === undefined ? Effect.fail(new EntifixConnError('zitadel accepted the user but returned no id')) : Effect.succeed(payload.userId)));
99
+ const updateProfile = (userId, profile)=>call('PUT', `/v2/users/human/${userId}`, {
100
+ profile
101
+ }).pipe(Effect.asVoid);
102
+ const setActive = (userId, active)=>call('POST', `/v2/users/${userId}/${active ? 'reactivate' : 'deactivate'}`, {}).pipe(Effect.asVoid);
103
+ const deleteUser = (userId)=>call('DELETE', `/v2/users/${userId}`).pipe(Effect.asVoid);
104
+ /**
105
+ * ⚠️ **Snake_case, and that is the API rather than a slip.** The admin event
106
+ * search takes `event_types`, `creation_date` and `asc` while every other
107
+ * call in this module takes camelCase — the v1 surface and the v2 surface do
108
+ * not agree, and this is the one v1 endpoint with a body.
109
+ *
110
+ * `asc: true` because a reconciler advances a cursor: processing newest-first
111
+ * and then storing the newest timestamp would skip everything behind a page
112
+ * boundary.
113
+ *
114
+ * A row missing its aggregate id is dropped rather than failing the sweep.
115
+ * There is nothing to act on, and one malformed row must not stop the ones
116
+ * behind it from being reconciled.
117
+ */ const searchEvents = (query)=>call('POST', '/admin/v1/events/_search', {
118
+ asc: true,
119
+ limit: query.limit,
120
+ creation_date: query.since,
121
+ event_types: query.eventTypes
122
+ }).pipe(Effect.map((payload)=>{
123
+ var _payload_events;
124
+ return ((_payload_events = payload.events) != null ? _payload_events : []).flatMap((row)=>{
125
+ var _row_aggregate, _row_type;
126
+ const subject = (_row_aggregate = row.aggregate) == null ? void 0 : _row_aggregate.id;
127
+ const type = (_row_type = row.type) == null ? void 0 : _row_type.type;
128
+ if (typeof subject !== 'string' || typeof type !== 'string' || typeof row.creationDate !== 'string') {
129
+ return [];
130
+ }
131
+ return [
132
+ {
133
+ subject,
134
+ type,
135
+ createdAt: row.creationDate
136
+ }
137
+ ];
138
+ });
139
+ }));
140
+ return {
141
+ findUserByEmail,
142
+ getUser,
143
+ createHuman,
144
+ updateProfile,
145
+ setActive,
146
+ deleteUser,
147
+ searchEvents
148
+ };
149
+ };
150
+ /** Binds {@link ZitadelManagementTag} from a resolved configuration. */ export const ZitadelManagementLayer = (config)=>Layer.succeed(ZitadelManagementTag, makeZitadelManagement(config));
151
+
152
+ //# sourceMappingURL=management-client.js.map