@rayspec/auth-core 1.5.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 +105 -0
- package/dist/api-key.d.ts +28 -0
- package/dist/api-key.d.ts.map +1 -0
- package/dist/api-key.js +56 -0
- package/dist/api-key.js.map +1 -0
- package/dist/audit.d.ts +26 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +11 -0
- package/dist/audit.js.map +1 -0
- package/dist/authz.d.ts +66 -0
- package/dist/authz.d.ts.map +1 -0
- package/dist/authz.js +112 -0
- package/dist/authz.js.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/deferrals.d.ts +33 -0
- package/dist/deferrals.d.ts.map +1 -0
- package/dist/deferrals.js +38 -0
- package/dist/deferrals.js.map +1 -0
- package/dist/dto.d.ts +337 -0
- package/dist/dto.d.ts.map +1 -0
- package/dist/dto.js +221 -0
- package/dist/dto.js.map +1 -0
- package/dist/email.d.ts +24 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +46 -0
- package/dist/email.js.map +1 -0
- package/dist/errors.d.ts +87 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +104 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/invite.d.ts +18 -0
- package/dist/invite.d.ts.map +1 -0
- package/dist/invite.js +33 -0
- package/dist/invite.js.map +1 -0
- package/dist/password.d.ts +17 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +46 -0
- package/dist/password.js.map +1 -0
- package/dist/rate-limit.d.ts +93 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +156 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/session.d.ts +18 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +41 -0
- package/dist/session.js.map +1 -0
- package/dist/tokens.d.ts +117 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +150 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +33 -0
package/dist/dto.d.ts
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RaySpec HTTP DTOs (Zod) — the ONLY shapes that cross the public HTTP boundary. No
|
|
3
|
+
* node-oidc-provider / jose / argon2 / Drizzle type appears here; handlers translate to/from
|
|
4
|
+
* these. Mass-assignment defense: org_id / role / tenant_id are NEVER body-bindable — they are
|
|
5
|
+
* server-derived. password_hash / key_hash / token_hash NEVER appear in any response DTO.
|
|
6
|
+
*
|
|
7
|
+
* The ONE deliberate, gated secret-in-a-response exception is `TokenResponse.refreshToken`
|
|
8
|
+
* — the rotated refresh secret, returned to a NON-browser client ONLY on the
|
|
9
|
+
* operator-gated + per-request opt-in path so it can store it in OS-secure storage (precedent:
|
|
10
|
+
* `MintApiKeyResponse.plaintext`). It is never returned to a browser flow and never logged/audited.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { API_KEY_GRANTABLE } from './authz.js';
|
|
14
|
+
/** Membership roles (owner|admin|member). */
|
|
15
|
+
export declare const Role: z.ZodEnum<{
|
|
16
|
+
owner: "owner";
|
|
17
|
+
admin: "admin";
|
|
18
|
+
member: "member";
|
|
19
|
+
}>;
|
|
20
|
+
export type Role = z.infer<typeof Role>;
|
|
21
|
+
export declare const RegisterRequest: z.ZodObject<{
|
|
22
|
+
email: z.ZodString;
|
|
23
|
+
password: z.ZodString;
|
|
24
|
+
orgName: z.ZodOptional<z.ZodString>;
|
|
25
|
+
deliverRefreshTokenInBody: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
export type RegisterRequest = z.infer<typeof RegisterRequest>;
|
|
28
|
+
export declare const LoginRequest: z.ZodObject<{
|
|
29
|
+
email: z.ZodString;
|
|
30
|
+
password: z.ZodString;
|
|
31
|
+
deliverRefreshTokenInBody: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type LoginRequest = z.infer<typeof LoginRequest>;
|
|
34
|
+
/**
|
|
35
|
+
* The access-token + (optional) active-org envelope returned by login / refresh / switch.
|
|
36
|
+
*
|
|
37
|
+
* `refreshToken` is a DELIBERATE, GATED exception to the "no secret in a response DTO" rule
|
|
38
|
+
* — populated ONLY when the deployment enables `RAYSPEC_BODY_REFRESH_ENABLED`
|
|
39
|
+
* AND the request opted in (`deliverRefreshTokenInBody`), i.e. a non-browser client storing the
|
|
40
|
+
* secret in OS-secure storage. Precedent: `MintApiKeyResponse.plaintext` (a secret returned once
|
|
41
|
+
* to the client by design). The secret is short-lived, rotates every refresh, and is
|
|
42
|
+
* family-revocable; a browser flow never receives it (it never opts in), and it is never logged.
|
|
43
|
+
*/
|
|
44
|
+
export declare const TokenResponse: z.ZodObject<{
|
|
45
|
+
accessToken: z.ZodString;
|
|
46
|
+
tokenType: z.ZodLiteral<"Bearer">;
|
|
47
|
+
expiresIn: z.ZodNumber;
|
|
48
|
+
activeOrgId: z.ZodNullable<z.ZodString>;
|
|
49
|
+
refreshToken: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
export type TokenResponse = z.infer<typeof TokenResponse>;
|
|
52
|
+
/** Refresh may carry the secret in the body (desktop/CLI) instead of the cookie. */
|
|
53
|
+
export declare const RefreshRequest: z.ZodObject<{
|
|
54
|
+
refreshToken: z.ZodOptional<z.ZodString>;
|
|
55
|
+
deliverRefreshTokenInBody: z.ZodOptional<z.ZodBoolean>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
export type RefreshRequest = z.infer<typeof RefreshRequest>;
|
|
58
|
+
export declare const MembershipView: z.ZodObject<{
|
|
59
|
+
orgId: z.ZodString;
|
|
60
|
+
role: z.ZodEnum<{
|
|
61
|
+
owner: "owner";
|
|
62
|
+
admin: "admin";
|
|
63
|
+
member: "member";
|
|
64
|
+
}>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export type MembershipView = z.infer<typeof MembershipView>;
|
|
67
|
+
export declare const MeResponse: z.ZodObject<{
|
|
68
|
+
userId: z.ZodString;
|
|
69
|
+
email: z.ZodString;
|
|
70
|
+
emailVerified: z.ZodBoolean;
|
|
71
|
+
memberships: z.ZodArray<z.ZodObject<{
|
|
72
|
+
orgId: z.ZodString;
|
|
73
|
+
role: z.ZodEnum<{
|
|
74
|
+
owner: "owner";
|
|
75
|
+
admin: "admin";
|
|
76
|
+
member: "member";
|
|
77
|
+
}>;
|
|
78
|
+
}, z.core.$strip>>;
|
|
79
|
+
activeOrgId: z.ZodNullable<z.ZodString>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
export type MeResponse = z.infer<typeof MeResponse>;
|
|
82
|
+
export declare const CreateOrgRequest: z.ZodObject<{
|
|
83
|
+
name: z.ZodString;
|
|
84
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strip>;
|
|
86
|
+
export type CreateOrgRequest = z.infer<typeof CreateOrgRequest>;
|
|
87
|
+
export declare const OrgView: z.ZodObject<{
|
|
88
|
+
id: z.ZodString;
|
|
89
|
+
name: z.ZodString;
|
|
90
|
+
slug: z.ZodString;
|
|
91
|
+
role: z.ZodEnum<{
|
|
92
|
+
owner: "owner";
|
|
93
|
+
admin: "admin";
|
|
94
|
+
member: "member";
|
|
95
|
+
}>;
|
|
96
|
+
}, z.core.$strip>;
|
|
97
|
+
export type OrgView = z.infer<typeof OrgView>;
|
|
98
|
+
export declare const OrgListResponse: z.ZodObject<{
|
|
99
|
+
orgs: z.ZodArray<z.ZodObject<{
|
|
100
|
+
id: z.ZodString;
|
|
101
|
+
name: z.ZodString;
|
|
102
|
+
slug: z.ZodString;
|
|
103
|
+
role: z.ZodEnum<{
|
|
104
|
+
owner: "owner";
|
|
105
|
+
admin: "admin";
|
|
106
|
+
member: "member";
|
|
107
|
+
}>;
|
|
108
|
+
}, z.core.$strip>>;
|
|
109
|
+
}, z.core.$strip>;
|
|
110
|
+
export type OrgListResponse = z.infer<typeof OrgListResponse>;
|
|
111
|
+
/** Change a member's role within an org (owner|admin only; last-owner demotion is blocked). */
|
|
112
|
+
export declare const ChangeMemberRoleRequest: z.ZodObject<{
|
|
113
|
+
role: z.ZodEnum<{
|
|
114
|
+
owner: "owner";
|
|
115
|
+
admin: "admin";
|
|
116
|
+
member: "member";
|
|
117
|
+
}>;
|
|
118
|
+
}, z.core.$strip>;
|
|
119
|
+
export type ChangeMemberRoleRequest = z.infer<typeof ChangeMemberRoleRequest>;
|
|
120
|
+
/**
|
|
121
|
+
* Add a user to an org by email (owner-only). `org_id`/`role`/`user_id` are NEVER body-bindable —
|
|
122
|
+
* the org is the server-derived tenant and the added user always joins as a plain `member`. Only
|
|
123
|
+
* the email is client-supplied; it is normalized server-side before any lookup/write.
|
|
124
|
+
*/
|
|
125
|
+
export declare const AddOrgMemberRequest: z.ZodObject<{
|
|
126
|
+
email: z.ZodString;
|
|
127
|
+
}, z.core.$strip>;
|
|
128
|
+
export type AddOrgMemberRequest = z.infer<typeof AddOrgMemberRequest>;
|
|
129
|
+
/** One member of an org (the id + email + role) — never a hash/secret. */
|
|
130
|
+
export declare const OrgMemberView: z.ZodObject<{
|
|
131
|
+
userId: z.ZodString;
|
|
132
|
+
email: z.ZodString;
|
|
133
|
+
role: z.ZodEnum<{
|
|
134
|
+
owner: "owner";
|
|
135
|
+
admin: "admin";
|
|
136
|
+
member: "member";
|
|
137
|
+
}>;
|
|
138
|
+
}, z.core.$strip>;
|
|
139
|
+
export type OrgMemberView = z.infer<typeof OrgMemberView>;
|
|
140
|
+
/**
|
|
141
|
+
* The add-member result. `oneTimePassword` is a DELIBERATE, single-shot secret-in-a-response — it
|
|
142
|
+
* is populated ONLY when the added email had no existing user, so a fresh account is provisioned
|
|
143
|
+
* with a random initial password the operator conveys out-of-band (core has no outbound mail). It
|
|
144
|
+
* is shown EXACTLY ONCE, never stored in plaintext, and never returned again (precedent:
|
|
145
|
+
* `MintApiKeyResponse.plaintext`). Absent when an existing user was added.
|
|
146
|
+
*/
|
|
147
|
+
export declare const AddOrgMemberResponse: z.ZodObject<{
|
|
148
|
+
userId: z.ZodString;
|
|
149
|
+
email: z.ZodString;
|
|
150
|
+
role: z.ZodEnum<{
|
|
151
|
+
owner: "owner";
|
|
152
|
+
admin: "admin";
|
|
153
|
+
member: "member";
|
|
154
|
+
}>;
|
|
155
|
+
oneTimePassword: z.ZodOptional<z.ZodString>;
|
|
156
|
+
}, z.core.$strip>;
|
|
157
|
+
export type AddOrgMemberResponse = z.infer<typeof AddOrgMemberResponse>;
|
|
158
|
+
export declare const OrgMemberListResponse: z.ZodObject<{
|
|
159
|
+
members: z.ZodArray<z.ZodObject<{
|
|
160
|
+
userId: z.ZodString;
|
|
161
|
+
email: z.ZodString;
|
|
162
|
+
role: z.ZodEnum<{
|
|
163
|
+
owner: "owner";
|
|
164
|
+
admin: "admin";
|
|
165
|
+
member: "member";
|
|
166
|
+
}>;
|
|
167
|
+
}, z.core.$strip>>;
|
|
168
|
+
}, z.core.$strip>;
|
|
169
|
+
export type OrgMemberListResponse = z.infer<typeof OrgMemberListResponse>;
|
|
170
|
+
/**
|
|
171
|
+
* Issue an out-of-band org invite (owner-only). `org_id`/`tenant_id` are NEVER body-bindable — the
|
|
172
|
+
* org is the server-derived tenant. `email` is normalized server-side. `role` defaults to `member`.
|
|
173
|
+
* `expiresInSeconds` is an OPTIONAL requested lifetime clamped server-side to a floor/ceiling.
|
|
174
|
+
*
|
|
175
|
+
* The issue path deliberately does NOT look up whether the email has an account, so the response +
|
|
176
|
+
* timing are IDENTICAL whether or not it does — no account-existence oracle (the account-existence
|
|
177
|
+
* check happens only at redeem, performed by the invitee).
|
|
178
|
+
*/
|
|
179
|
+
export declare const IssueInviteRequest: z.ZodObject<{
|
|
180
|
+
email: z.ZodString;
|
|
181
|
+
role: z.ZodDefault<z.ZodEnum<{
|
|
182
|
+
owner: "owner";
|
|
183
|
+
admin: "admin";
|
|
184
|
+
member: "member";
|
|
185
|
+
}>>;
|
|
186
|
+
expiresInSeconds: z.ZodOptional<z.ZodNumber>;
|
|
187
|
+
}, z.core.$strip>;
|
|
188
|
+
export type IssueInviteRequest = z.infer<typeof IssueInviteRequest>;
|
|
189
|
+
/**
|
|
190
|
+
* The issued invite. `inviteToken` is a DELIBERATE, single-shot secret-in-a-response — the opaque
|
|
191
|
+
* token is shown EXACTLY ONCE (the owner conveys it out-of-band; only its hash is stored), never
|
|
192
|
+
* returned again (precedent: `MintApiKeyResponse.plaintext`). The response carries NO account-existence
|
|
193
|
+
* signal (it is identical for any email).
|
|
194
|
+
*/
|
|
195
|
+
export declare const IssueInviteResponse: z.ZodObject<{
|
|
196
|
+
inviteToken: z.ZodString;
|
|
197
|
+
email: z.ZodString;
|
|
198
|
+
role: z.ZodEnum<{
|
|
199
|
+
owner: "owner";
|
|
200
|
+
admin: "admin";
|
|
201
|
+
member: "member";
|
|
202
|
+
}>;
|
|
203
|
+
expiresAt: z.ZodString;
|
|
204
|
+
}, z.core.$strip>;
|
|
205
|
+
export type IssueInviteResponse = z.infer<typeof IssueInviteResponse>;
|
|
206
|
+
/**
|
|
207
|
+
* Redeem an invite (the INVITEE acts, not the owner). The org is resolved FROM the token, never a URL.
|
|
208
|
+
* `password` is REQUIRED only when the invited email has no account yet (the invitee sets their own
|
|
209
|
+
* initial credential at accept); it is IGNORED when the email already has an account (the invitee must
|
|
210
|
+
* instead be authenticated as that account — a token bearer can never set/reset an existing user's
|
|
211
|
+
* password). `password` shares the register/login shape policy.
|
|
212
|
+
*/
|
|
213
|
+
export declare const AcceptInviteRequest: z.ZodObject<{
|
|
214
|
+
token: z.ZodString;
|
|
215
|
+
password: z.ZodOptional<z.ZodString>;
|
|
216
|
+
deliverRefreshTokenInBody: z.ZodOptional<z.ZodBoolean>;
|
|
217
|
+
}, z.core.$strip>;
|
|
218
|
+
export type AcceptInviteRequest = z.infer<typeof AcceptInviteRequest>;
|
|
219
|
+
/**
|
|
220
|
+
* The accept result — a token envelope scoped to the joined org (so the response is immediately
|
|
221
|
+
* usable), plus the membership. `refreshToken` is the gated+opt-in body-delivered refresh secret,
|
|
222
|
+
* present ONLY on the new-account provisioning path (the invitee is freshly logged in) when the
|
|
223
|
+
* deployment enables it; an authenticated existing-account accept receives only a fresh org-scoped
|
|
224
|
+
* access token (its refresh session is untouched).
|
|
225
|
+
*/
|
|
226
|
+
export declare const AcceptInviteResponse: z.ZodObject<{
|
|
227
|
+
accessToken: z.ZodString;
|
|
228
|
+
tokenType: z.ZodLiteral<"Bearer">;
|
|
229
|
+
expiresIn: z.ZodNumber;
|
|
230
|
+
activeOrgId: z.ZodString;
|
|
231
|
+
userId: z.ZodString;
|
|
232
|
+
role: z.ZodEnum<{
|
|
233
|
+
owner: "owner";
|
|
234
|
+
admin: "admin";
|
|
235
|
+
member: "member";
|
|
236
|
+
}>;
|
|
237
|
+
refreshToken: z.ZodOptional<z.ZodString>;
|
|
238
|
+
}, z.core.$strip>;
|
|
239
|
+
export type AcceptInviteResponse = z.infer<typeof AcceptInviteResponse>;
|
|
240
|
+
/**
|
|
241
|
+
* The known api-key scopes (closed set; intersected with requested on mint). `store:read`/
|
|
242
|
+
* `store:write` let a programmatic/agency consumer (a desktop app / automation) READ
|
|
243
|
+
* and WRITE declared-store data via an org-scoped api-key — the deployer grants the write scope
|
|
244
|
+
* EXPLICITLY at mint (it is not implicit).
|
|
245
|
+
*
|
|
246
|
+
* DERIVED from `API_KEY_GRANTABLE` (authz.ts) — the SINGLE source of truth shared with `authorize()`.
|
|
247
|
+
* This is not a parallel literal that can silently drift from the runtime gate: a scope minted here
|
|
248
|
+
* that authorize() would never grant (or vice-versa) is structurally impossible, because both read
|
|
249
|
+
* the same array. (The authz.test.ts derived-invariant test additionally pins the equality.)
|
|
250
|
+
*/
|
|
251
|
+
export declare const ApiKeyScope: z.ZodEnum<{ [K in (typeof API_KEY_GRANTABLE)[number]]: K; }>;
|
|
252
|
+
export type ApiKeyScope = z.infer<typeof ApiKeyScope>;
|
|
253
|
+
export declare const MintApiKeyRequest: z.ZodObject<{
|
|
254
|
+
name: z.ZodOptional<z.ZodString>;
|
|
255
|
+
scopes: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
256
|
+
"agent:run": "agent:run";
|
|
257
|
+
"agent:read": "agent:read";
|
|
258
|
+
"store:read": "store:read";
|
|
259
|
+
"store:write": "store:write";
|
|
260
|
+
"org:read": "org:read";
|
|
261
|
+
"org:member:add": "org:member:add";
|
|
262
|
+
"org:member:change": "org:member:change";
|
|
263
|
+
"org:switch": "org:switch";
|
|
264
|
+
"apikey:read": "apikey:read";
|
|
265
|
+
"apikey:mint": "apikey:mint";
|
|
266
|
+
"apikey:revoke": "apikey:revoke";
|
|
267
|
+
}>>>;
|
|
268
|
+
}, z.core.$strip>;
|
|
269
|
+
export type MintApiKeyRequest = z.infer<typeof MintApiKeyRequest>;
|
|
270
|
+
/** The mint response is the ONLY place plaintext is returned — ONCE. */
|
|
271
|
+
export declare const MintApiKeyResponse: z.ZodObject<{
|
|
272
|
+
id: z.ZodString;
|
|
273
|
+
keyPrefix: z.ZodString;
|
|
274
|
+
plaintext: z.ZodString;
|
|
275
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
276
|
+
"agent:run": "agent:run";
|
|
277
|
+
"agent:read": "agent:read";
|
|
278
|
+
"store:read": "store:read";
|
|
279
|
+
"store:write": "store:write";
|
|
280
|
+
"org:read": "org:read";
|
|
281
|
+
"org:member:add": "org:member:add";
|
|
282
|
+
"org:member:change": "org:member:change";
|
|
283
|
+
"org:switch": "org:switch";
|
|
284
|
+
"apikey:read": "apikey:read";
|
|
285
|
+
"apikey:mint": "apikey:mint";
|
|
286
|
+
"apikey:revoke": "apikey:revoke";
|
|
287
|
+
}>>;
|
|
288
|
+
}, z.core.$strip>;
|
|
289
|
+
export type MintApiKeyResponse = z.infer<typeof MintApiKeyResponse>;
|
|
290
|
+
/**
|
|
291
|
+
* The REDACTED mint snapshot — the ONLY shape persisted in idempotency_keys.snapshot and the
|
|
292
|
+
* shape returned on an Idempotency-Key REPLAY. It NEVER carries
|
|
293
|
+
* `plaintext`: the secret is shown EXACTLY ONCE on the original mint; a retry returns only the
|
|
294
|
+
* non-secret metadata so a DB dump of the no-TTL snapshot column yields no usable `<prefix>.<secret>`
|
|
295
|
+
* credential. `replayed: true` signals the caller that the plaintext is not available again.
|
|
296
|
+
*/
|
|
297
|
+
export declare const MintApiKeyReplay: z.ZodObject<{
|
|
298
|
+
id: z.ZodString;
|
|
299
|
+
keyPrefix: z.ZodString;
|
|
300
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
301
|
+
"agent:run": "agent:run";
|
|
302
|
+
"agent:read": "agent:read";
|
|
303
|
+
"store:read": "store:read";
|
|
304
|
+
"store:write": "store:write";
|
|
305
|
+
"org:read": "org:read";
|
|
306
|
+
"org:member:add": "org:member:add";
|
|
307
|
+
"org:member:change": "org:member:change";
|
|
308
|
+
"org:switch": "org:switch";
|
|
309
|
+
"apikey:read": "apikey:read";
|
|
310
|
+
"apikey:mint": "apikey:mint";
|
|
311
|
+
"apikey:revoke": "apikey:revoke";
|
|
312
|
+
}>>;
|
|
313
|
+
replayed: z.ZodLiteral<true>;
|
|
314
|
+
}, z.core.$strip>;
|
|
315
|
+
export type MintApiKeyReplay = z.infer<typeof MintApiKeyReplay>;
|
|
316
|
+
/** Listing never includes plaintext or the hash. */
|
|
317
|
+
export declare const ApiKeyView: z.ZodObject<{
|
|
318
|
+
id: z.ZodString;
|
|
319
|
+
keyPrefix: z.ZodString;
|
|
320
|
+
scopes: z.ZodArray<z.ZodString>;
|
|
321
|
+
lastUsedAt: z.ZodNullable<z.ZodString>;
|
|
322
|
+
revokedAt: z.ZodNullable<z.ZodString>;
|
|
323
|
+
createdAt: z.ZodString;
|
|
324
|
+
}, z.core.$strip>;
|
|
325
|
+
export type ApiKeyView = z.infer<typeof ApiKeyView>;
|
|
326
|
+
export declare const ApiKeyListResponse: z.ZodObject<{
|
|
327
|
+
keys: z.ZodArray<z.ZodObject<{
|
|
328
|
+
id: z.ZodString;
|
|
329
|
+
keyPrefix: z.ZodString;
|
|
330
|
+
scopes: z.ZodArray<z.ZodString>;
|
|
331
|
+
lastUsedAt: z.ZodNullable<z.ZodString>;
|
|
332
|
+
revokedAt: z.ZodNullable<z.ZodString>;
|
|
333
|
+
createdAt: z.ZodString;
|
|
334
|
+
}, z.core.$strip>>;
|
|
335
|
+
}, z.core.$strip>;
|
|
336
|
+
export type ApiKeyListResponse = z.infer<typeof ApiKeyListResponse>;
|
|
337
|
+
//# sourceMappingURL=dto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../src/dto.ts"],"names":[],"mappings":"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"}
|
package/dist/dto.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RaySpec HTTP DTOs (Zod) — the ONLY shapes that cross the public HTTP boundary. No
|
|
3
|
+
* node-oidc-provider / jose / argon2 / Drizzle type appears here; handlers translate to/from
|
|
4
|
+
* these. Mass-assignment defense: org_id / role / tenant_id are NEVER body-bindable — they are
|
|
5
|
+
* server-derived. password_hash / key_hash / token_hash NEVER appear in any response DTO.
|
|
6
|
+
*
|
|
7
|
+
* The ONE deliberate, gated secret-in-a-response exception is `TokenResponse.refreshToken`
|
|
8
|
+
* — the rotated refresh secret, returned to a NON-browser client ONLY on the
|
|
9
|
+
* operator-gated + per-request opt-in path so it can store it in OS-secure storage (precedent:
|
|
10
|
+
* `MintApiKeyResponse.plaintext`). It is never returned to a browser flow and never logged/audited.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { API_KEY_GRANTABLE } from './authz.js';
|
|
14
|
+
/** Membership roles (owner|admin|member). */
|
|
15
|
+
export const Role = z.enum(['owner', 'admin', 'member']);
|
|
16
|
+
/** Email + password are validated for SHAPE here; normalization happens in the service. */
|
|
17
|
+
const passwordField = z.string().min(8).max(1024);
|
|
18
|
+
const emailField = z.string().min(3).max(254);
|
|
19
|
+
// ---- auth ----------------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* A NON-browser client (desktop/CLI) opts in to receiving the rotated refresh
|
|
22
|
+
* secret in the JSON response body (so it can store it in OS-secure storage and refresh without the
|
|
23
|
+
* httpOnly cookie). Absent/false ⇒ today's behavior. Only honored when the deployment ALSO enables
|
|
24
|
+
* the operator gate (`bodyRefreshEnabled`); a browser flow never sets it. Shared by register/login/
|
|
25
|
+
* refresh.
|
|
26
|
+
*/
|
|
27
|
+
const deliverRefreshTokenInBody = z.boolean().optional();
|
|
28
|
+
export const RegisterRequest = z.object({
|
|
29
|
+
email: emailField,
|
|
30
|
+
password: passwordField,
|
|
31
|
+
/** Optional: auto-create an org + owner membership for the new user. */
|
|
32
|
+
orgName: z.string().min(1).max(200).optional(),
|
|
33
|
+
deliverRefreshTokenInBody,
|
|
34
|
+
});
|
|
35
|
+
export const LoginRequest = z.object({
|
|
36
|
+
email: emailField,
|
|
37
|
+
password: passwordField,
|
|
38
|
+
deliverRefreshTokenInBody,
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* The access-token + (optional) active-org envelope returned by login / refresh / switch.
|
|
42
|
+
*
|
|
43
|
+
* `refreshToken` is a DELIBERATE, GATED exception to the "no secret in a response DTO" rule
|
|
44
|
+
* — populated ONLY when the deployment enables `RAYSPEC_BODY_REFRESH_ENABLED`
|
|
45
|
+
* AND the request opted in (`deliverRefreshTokenInBody`), i.e. a non-browser client storing the
|
|
46
|
+
* secret in OS-secure storage. Precedent: `MintApiKeyResponse.plaintext` (a secret returned once
|
|
47
|
+
* to the client by design). The secret is short-lived, rotates every refresh, and is
|
|
48
|
+
* family-revocable; a browser flow never receives it (it never opts in), and it is never logged.
|
|
49
|
+
*/
|
|
50
|
+
export const TokenResponse = z.object({
|
|
51
|
+
accessToken: z.string(),
|
|
52
|
+
tokenType: z.literal('Bearer'),
|
|
53
|
+
expiresIn: z.number().int().positive(),
|
|
54
|
+
activeOrgId: z.string().uuid().nullable(),
|
|
55
|
+
/** The rotated refresh secret — gated+opt-in only (see the type doc above). Absent otherwise. */
|
|
56
|
+
refreshToken: z.string().optional(),
|
|
57
|
+
});
|
|
58
|
+
/** Refresh may carry the secret in the body (desktop/CLI) instead of the cookie. */
|
|
59
|
+
export const RefreshRequest = z.object({
|
|
60
|
+
refreshToken: z.string().min(1).optional(),
|
|
61
|
+
deliverRefreshTokenInBody,
|
|
62
|
+
});
|
|
63
|
+
export const MembershipView = z.object({
|
|
64
|
+
orgId: z.string().uuid(),
|
|
65
|
+
role: Role,
|
|
66
|
+
});
|
|
67
|
+
export const MeResponse = z.object({
|
|
68
|
+
userId: z.string().uuid(),
|
|
69
|
+
email: z.string(),
|
|
70
|
+
emailVerified: z.boolean(),
|
|
71
|
+
memberships: z.array(MembershipView),
|
|
72
|
+
activeOrgId: z.string().uuid().nullable(),
|
|
73
|
+
});
|
|
74
|
+
// ---- orgs ----------------------------------------------------------------------------------
|
|
75
|
+
export const CreateOrgRequest = z.object({
|
|
76
|
+
name: z.string().min(1).max(200),
|
|
77
|
+
/** Optional slug; defaults to a normalized form of name. Lowercased + uniqueness server-side. */
|
|
78
|
+
slug: z.string().min(1).max(200).optional(),
|
|
79
|
+
});
|
|
80
|
+
export const OrgView = z.object({
|
|
81
|
+
id: z.string().uuid(),
|
|
82
|
+
name: z.string(),
|
|
83
|
+
slug: z.string(),
|
|
84
|
+
role: Role,
|
|
85
|
+
});
|
|
86
|
+
export const OrgListResponse = z.object({ orgs: z.array(OrgView) });
|
|
87
|
+
/** Change a member's role within an org (owner|admin only; last-owner demotion is blocked). */
|
|
88
|
+
export const ChangeMemberRoleRequest = z.object({ role: Role });
|
|
89
|
+
/**
|
|
90
|
+
* Add a user to an org by email (owner-only). `org_id`/`role`/`user_id` are NEVER body-bindable —
|
|
91
|
+
* the org is the server-derived tenant and the added user always joins as a plain `member`. Only
|
|
92
|
+
* the email is client-supplied; it is normalized server-side before any lookup/write.
|
|
93
|
+
*/
|
|
94
|
+
export const AddOrgMemberRequest = z.object({ email: emailField });
|
|
95
|
+
/** One member of an org (the id + email + role) — never a hash/secret. */
|
|
96
|
+
export const OrgMemberView = z.object({
|
|
97
|
+
userId: z.string().uuid(),
|
|
98
|
+
email: z.string(),
|
|
99
|
+
role: Role,
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* The add-member result. `oneTimePassword` is a DELIBERATE, single-shot secret-in-a-response — it
|
|
103
|
+
* is populated ONLY when the added email had no existing user, so a fresh account is provisioned
|
|
104
|
+
* with a random initial password the operator conveys out-of-band (core has no outbound mail). It
|
|
105
|
+
* is shown EXACTLY ONCE, never stored in plaintext, and never returned again (precedent:
|
|
106
|
+
* `MintApiKeyResponse.plaintext`). Absent when an existing user was added.
|
|
107
|
+
*/
|
|
108
|
+
export const AddOrgMemberResponse = z.object({
|
|
109
|
+
userId: z.string().uuid(),
|
|
110
|
+
email: z.string(),
|
|
111
|
+
role: Role,
|
|
112
|
+
oneTimePassword: z.string().optional(),
|
|
113
|
+
});
|
|
114
|
+
export const OrgMemberListResponse = z.object({ members: z.array(OrgMemberView) });
|
|
115
|
+
// ---- org invites (out-of-band invite-token flow) -------------------------------------------
|
|
116
|
+
/**
|
|
117
|
+
* Issue an out-of-band org invite (owner-only). `org_id`/`tenant_id` are NEVER body-bindable — the
|
|
118
|
+
* org is the server-derived tenant. `email` is normalized server-side. `role` defaults to `member`.
|
|
119
|
+
* `expiresInSeconds` is an OPTIONAL requested lifetime clamped server-side to a floor/ceiling.
|
|
120
|
+
*
|
|
121
|
+
* The issue path deliberately does NOT look up whether the email has an account, so the response +
|
|
122
|
+
* timing are IDENTICAL whether or not it does — no account-existence oracle (the account-existence
|
|
123
|
+
* check happens only at redeem, performed by the invitee).
|
|
124
|
+
*/
|
|
125
|
+
export const IssueInviteRequest = z.object({
|
|
126
|
+
email: emailField,
|
|
127
|
+
role: Role.default('member'),
|
|
128
|
+
expiresInSeconds: z.number().int().positive().optional(),
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* The issued invite. `inviteToken` is a DELIBERATE, single-shot secret-in-a-response — the opaque
|
|
132
|
+
* token is shown EXACTLY ONCE (the owner conveys it out-of-band; only its hash is stored), never
|
|
133
|
+
* returned again (precedent: `MintApiKeyResponse.plaintext`). The response carries NO account-existence
|
|
134
|
+
* signal (it is identical for any email).
|
|
135
|
+
*/
|
|
136
|
+
export const IssueInviteResponse = z.object({
|
|
137
|
+
/** The opaque invite token, shown EXACTLY ONCE; never stored in plaintext, never returned again. */
|
|
138
|
+
inviteToken: z.string(),
|
|
139
|
+
email: z.string(),
|
|
140
|
+
role: Role,
|
|
141
|
+
/** The hard expiry (ISO 8601). */
|
|
142
|
+
expiresAt: z.string(),
|
|
143
|
+
});
|
|
144
|
+
/**
|
|
145
|
+
* Redeem an invite (the INVITEE acts, not the owner). The org is resolved FROM the token, never a URL.
|
|
146
|
+
* `password` is REQUIRED only when the invited email has no account yet (the invitee sets their own
|
|
147
|
+
* initial credential at accept); it is IGNORED when the email already has an account (the invitee must
|
|
148
|
+
* instead be authenticated as that account — a token bearer can never set/reset an existing user's
|
|
149
|
+
* password). `password` shares the register/login shape policy.
|
|
150
|
+
*/
|
|
151
|
+
export const AcceptInviteRequest = z.object({
|
|
152
|
+
token: z.string().min(1),
|
|
153
|
+
password: passwordField.optional(),
|
|
154
|
+
deliverRefreshTokenInBody,
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* The accept result — a token envelope scoped to the joined org (so the response is immediately
|
|
158
|
+
* usable), plus the membership. `refreshToken` is the gated+opt-in body-delivered refresh secret,
|
|
159
|
+
* present ONLY on the new-account provisioning path (the invitee is freshly logged in) when the
|
|
160
|
+
* deployment enables it; an authenticated existing-account accept receives only a fresh org-scoped
|
|
161
|
+
* access token (its refresh session is untouched).
|
|
162
|
+
*/
|
|
163
|
+
export const AcceptInviteResponse = z.object({
|
|
164
|
+
accessToken: z.string(),
|
|
165
|
+
tokenType: z.literal('Bearer'),
|
|
166
|
+
expiresIn: z.number().int().positive(),
|
|
167
|
+
activeOrgId: z.string().uuid(),
|
|
168
|
+
userId: z.string().uuid(),
|
|
169
|
+
role: Role,
|
|
170
|
+
/** The rotated refresh secret — gated+opt-in, new-account path only (see the type doc above). */
|
|
171
|
+
refreshToken: z.string().optional(),
|
|
172
|
+
});
|
|
173
|
+
// ---- api keys ------------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* The known api-key scopes (closed set; intersected with requested on mint). `store:read`/
|
|
176
|
+
* `store:write` let a programmatic/agency consumer (a desktop app / automation) READ
|
|
177
|
+
* and WRITE declared-store data via an org-scoped api-key — the deployer grants the write scope
|
|
178
|
+
* EXPLICITLY at mint (it is not implicit).
|
|
179
|
+
*
|
|
180
|
+
* DERIVED from `API_KEY_GRANTABLE` (authz.ts) — the SINGLE source of truth shared with `authorize()`.
|
|
181
|
+
* This is not a parallel literal that can silently drift from the runtime gate: a scope minted here
|
|
182
|
+
* that authorize() would never grant (or vice-versa) is structurally impossible, because both read
|
|
183
|
+
* the same array. (The authz.test.ts derived-invariant test additionally pins the equality.)
|
|
184
|
+
*/
|
|
185
|
+
export const ApiKeyScope = z.enum(API_KEY_GRANTABLE);
|
|
186
|
+
export const MintApiKeyRequest = z.object({
|
|
187
|
+
name: z.string().min(1).max(200).optional(),
|
|
188
|
+
scopes: z.array(ApiKeyScope).max(32).default([]),
|
|
189
|
+
});
|
|
190
|
+
/** The mint response is the ONLY place plaintext is returned — ONCE. */
|
|
191
|
+
export const MintApiKeyResponse = z.object({
|
|
192
|
+
id: z.string().uuid(),
|
|
193
|
+
keyPrefix: z.string(),
|
|
194
|
+
/** The full plaintext key, shown EXACTLY ONCE; never stored, never returned again. */
|
|
195
|
+
plaintext: z.string(),
|
|
196
|
+
scopes: z.array(ApiKeyScope),
|
|
197
|
+
});
|
|
198
|
+
/**
|
|
199
|
+
* The REDACTED mint snapshot — the ONLY shape persisted in idempotency_keys.snapshot and the
|
|
200
|
+
* shape returned on an Idempotency-Key REPLAY. It NEVER carries
|
|
201
|
+
* `plaintext`: the secret is shown EXACTLY ONCE on the original mint; a retry returns only the
|
|
202
|
+
* non-secret metadata so a DB dump of the no-TTL snapshot column yields no usable `<prefix>.<secret>`
|
|
203
|
+
* credential. `replayed: true` signals the caller that the plaintext is not available again.
|
|
204
|
+
*/
|
|
205
|
+
export const MintApiKeyReplay = z.object({
|
|
206
|
+
id: z.string().uuid(),
|
|
207
|
+
keyPrefix: z.string(),
|
|
208
|
+
scopes: z.array(ApiKeyScope),
|
|
209
|
+
replayed: z.literal(true),
|
|
210
|
+
});
|
|
211
|
+
/** Listing never includes plaintext or the hash. */
|
|
212
|
+
export const ApiKeyView = z.object({
|
|
213
|
+
id: z.string().uuid(),
|
|
214
|
+
keyPrefix: z.string(),
|
|
215
|
+
scopes: z.array(z.string()),
|
|
216
|
+
lastUsedAt: z.string().nullable(),
|
|
217
|
+
revokedAt: z.string().nullable(),
|
|
218
|
+
createdAt: z.string(),
|
|
219
|
+
});
|
|
220
|
+
export const ApiKeyListResponse = z.object({ keys: z.array(ApiKeyView) });
|
|
221
|
+
//# sourceMappingURL=dto.js.map
|
package/dist/dto.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto.js","sourceRoot":"","sources":["../src/dto.ts"],"names":[],"mappings":"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"}
|
package/dist/email.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email normalization — applied BEFORE every users.email write so the partial unique index on
|
|
3
|
+
* lower(email) cannot be bypassed by compatibility/whitespace/invisible-character variants, and
|
|
4
|
+
* the dummy-hash enumeration defense operates on a single canonical form.
|
|
5
|
+
*
|
|
6
|
+
* Steps: trim, Unicode NFKC fold, lowercase, cap at 254 chars (RFC 5321), then reject any
|
|
7
|
+
* embedded Unicode Control/Format (\p{C}) or Separator/whitespace (\p{Z}) character anywhere in
|
|
8
|
+
* the address. NOT a deliverability validator — a normalization + basic shape guard. Throws on
|
|
9
|
+
* a structurally invalid address so a bad value never reaches the DB.
|
|
10
|
+
*
|
|
11
|
+
* SCOPE / LIMITATION (honest): NFKC collapses COMPATIBILITY variants (e.g. fullwidth ALICE ->
|
|
12
|
+
* alice) and the \p{C}/\p{Z}/\p{Default_Ignorable_Code_Point} reject closes the invisible-character
|
|
13
|
+
* class (zero-width space/joiner/non-joiner, word-joiner, soft-hyphen, BOM, LRM/RLO bidi marks,
|
|
14
|
+
* NBSP, AND the Default_Ignorable code points \p{C} misses — variation selectors U+FE00–FE0F /
|
|
15
|
+
* U+E0100–E01EF, Mongolian free variation selectors, etc., which are category Mn/format-invisible
|
|
16
|
+
* and would otherwise survive NFKC) — these all PASSED the previous ASCII-only guard and the
|
|
17
|
+
* earlier \p{C}/\p{Z}-only guard. It does NOT defend against cross-script HOMOGLYPHS: a Cyrillic
|
|
18
|
+
* "а" (U+0430) is a distinct letter that NFKC does not fold and \p{C}/\p{Z} does not catch, so
|
|
19
|
+
* "аlice@x.com" and "alice@x.com" remain different canonical strings. Full TR39 confusable-
|
|
20
|
+
* skeleton / IDNA defense is deliberately out of scope (it needs a confusables table);
|
|
21
|
+
* the homograph test below documents this boundary so the limitation is explicit, not implied.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeEmail(raw: string): string;
|
|
24
|
+
//# sourceMappingURL=email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAYlD"}
|
package/dist/email.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email normalization — applied BEFORE every users.email write so the partial unique index on
|
|
3
|
+
* lower(email) cannot be bypassed by compatibility/whitespace/invisible-character variants, and
|
|
4
|
+
* the dummy-hash enumeration defense operates on a single canonical form.
|
|
5
|
+
*
|
|
6
|
+
* Steps: trim, Unicode NFKC fold, lowercase, cap at 254 chars (RFC 5321), then reject any
|
|
7
|
+
* embedded Unicode Control/Format (\p{C}) or Separator/whitespace (\p{Z}) character anywhere in
|
|
8
|
+
* the address. NOT a deliverability validator — a normalization + basic shape guard. Throws on
|
|
9
|
+
* a structurally invalid address so a bad value never reaches the DB.
|
|
10
|
+
*
|
|
11
|
+
* SCOPE / LIMITATION (honest): NFKC collapses COMPATIBILITY variants (e.g. fullwidth ALICE ->
|
|
12
|
+
* alice) and the \p{C}/\p{Z}/\p{Default_Ignorable_Code_Point} reject closes the invisible-character
|
|
13
|
+
* class (zero-width space/joiner/non-joiner, word-joiner, soft-hyphen, BOM, LRM/RLO bidi marks,
|
|
14
|
+
* NBSP, AND the Default_Ignorable code points \p{C} misses — variation selectors U+FE00–FE0F /
|
|
15
|
+
* U+E0100–E01EF, Mongolian free variation selectors, etc., which are category Mn/format-invisible
|
|
16
|
+
* and would otherwise survive NFKC) — these all PASSED the previous ASCII-only guard and the
|
|
17
|
+
* earlier \p{C}/\p{Z}-only guard. It does NOT defend against cross-script HOMOGLYPHS: a Cyrillic
|
|
18
|
+
* "а" (U+0430) is a distinct letter that NFKC does not fold and \p{C}/\p{Z} does not catch, so
|
|
19
|
+
* "аlice@x.com" and "alice@x.com" remain different canonical strings. Full TR39 confusable-
|
|
20
|
+
* skeleton / IDNA defense is deliberately out of scope (it needs a confusables table);
|
|
21
|
+
* the homograph test below documents this boundary so the limitation is explicit, not implied.
|
|
22
|
+
*/
|
|
23
|
+
// Reject Unicode Control/Format chars (\p{C} — incl. zero-width/format/bidi marks), any
|
|
24
|
+
// Separator/whitespace (\p{Z} + ASCII control/whitespace via \s), AND every other
|
|
25
|
+
// Default_Ignorable code point — notably the variation selectors U+FE00–FE0F and U+E0100–E01EF,
|
|
26
|
+
// which are category Mn (a Mark, NOT \p{C}) yet render invisibly and survive NFKC, so a
|
|
27
|
+
// \p{C}/\p{Z}-only guard let them through and they could defeat the lower(email) unique index.
|
|
28
|
+
// NFKC keeps all of these (it does not strip them), so they must be rejected explicitly. The /u
|
|
29
|
+
// flag is required for \p{...}. Built so the source carries no literal invisible bytes.
|
|
30
|
+
const DISALLOWED = /[\p{C}\p{Z}\p{Default_Ignorable_Code_Point}\s]/u;
|
|
31
|
+
export function normalizeEmail(raw) {
|
|
32
|
+
const normalized = raw.trim().normalize('NFKC').toLowerCase();
|
|
33
|
+
if (normalized.length === 0)
|
|
34
|
+
throw new Error('email is empty');
|
|
35
|
+
if (normalized.length > 254)
|
|
36
|
+
throw new Error('email exceeds 254 characters');
|
|
37
|
+
if (DISALLOWED.test(normalized)) {
|
|
38
|
+
throw new Error('email contains control, whitespace, or invisible characters');
|
|
39
|
+
}
|
|
40
|
+
const at = normalized.indexOf('@');
|
|
41
|
+
if (at <= 0 || at !== normalized.lastIndexOf('@') || at === normalized.length - 1) {
|
|
42
|
+
throw new Error('email is not a valid address');
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.js","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,wFAAwF;AACxF,kFAAkF;AAClF,gGAAgG;AAChG,wFAAwF;AACxF,+FAA+F;AAC/F,gGAAgG;AAChG,wFAAwF;AACxF,MAAM,UAAU,GAAG,iDAAiD,CAAC;AAErE,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC9D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAC/D,IAAI,UAAU,CAAC,MAAM,GAAG,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC7E,IAAI,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC"}
|