@oxy.so/contracts 2.2.0 → 4.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.
Files changed (41) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/accountEmail.js +21 -25
  3. package/dist/cjs/deviceBoot.js +2 -2
  4. package/dist/cjs/deviceSession.js +73 -16
  5. package/dist/cjs/externalIdentity.js +9 -3
  6. package/dist/cjs/identity.js +1 -3
  7. package/dist/cjs/identityLink.js +22 -19
  8. package/dist/cjs/identityProof.js +2 -2
  9. package/dist/cjs/index.js +59 -28
  10. package/dist/cjs/reputation.js +10 -55
  11. package/dist/cjs/signIn.js +304 -0
  12. package/dist/esm/.tsbuildinfo +1 -1
  13. package/dist/esm/accountEmail.js +21 -25
  14. package/dist/esm/deviceBoot.js +2 -2
  15. package/dist/esm/deviceSession.js +72 -15
  16. package/dist/esm/externalIdentity.js +8 -2
  17. package/dist/esm/identity.js +1 -3
  18. package/dist/esm/identityLink.js +21 -18
  19. package/dist/esm/identityProof.js +2 -2
  20. package/dist/esm/index.js +11 -11
  21. package/dist/esm/reputation.js +9 -54
  22. package/dist/esm/signIn.js +299 -0
  23. package/dist/types/.tsbuildinfo +1 -1
  24. package/dist/types/accountEmail.d.ts +20 -28
  25. package/dist/types/accountGraph.d.ts +4 -4
  26. package/dist/types/deviceBoot.d.ts +2 -2
  27. package/dist/types/deviceSession.d.ts +127 -15
  28. package/dist/types/externalIdentity.d.ts +53 -46
  29. package/dist/types/identity.d.ts +4 -8
  30. package/dist/types/identityLink.d.ts +54 -118
  31. package/dist/types/identityProof.d.ts +3 -3
  32. package/dist/types/index.d.ts +8 -8
  33. package/dist/types/inference/entitlement.d.ts +2 -2
  34. package/dist/types/oauth.d.ts +16 -16
  35. package/dist/types/reputation.d.ts +32 -127
  36. package/dist/types/signIn.d.ts +717 -0
  37. package/dist/types/userResponse.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/cjs/webauthn.js +0 -114
  40. package/dist/esm/webauthn.js +0 -111
  41. package/dist/types/webauthn.d.ts +0 -173
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Signing in: an email code or link, an optional password,
3
+ * and an optional authenticator app (TOTP) as a second factor.
4
+ *
5
+ * Every Oxy app runs these from its own account dialog (official apps and
6
+ * auth.oxy.so only; third parties sign in through OAuth on auth.oxy.so):
7
+ *
8
+ * 1. `POST /auth/signin/email/start` with a username or email. One email
9
+ * carries a 6-digit code AND a one-use link to auth.oxy.so; the answer is the
10
+ * same whether or not the account exists. The dialog keeps the
11
+ * `requestSecret` it is given: only it can collect the session.
12
+ * 2. Either the code (`POST /auth/signin/email/confirm`), or the link opened in
13
+ * the SAME browser (`POST /auth/signin/email/link` from auth.oxy.so, proving
14
+ * the browser's shared device) followed by the dialog's
15
+ * `POST /auth/signin/email/collect`.
16
+ * 3. Or a password (`POST /auth/signin/password`), when the account set one.
17
+ * 4. When the account has an authenticator, every first factor answers a
18
+ * one-use second-factor challenge instead of a session, and
19
+ * `POST /auth/signin/second-factor` takes the TOTP (or a backup code).
20
+ *
21
+ * `POST /auth/signup` creates an account from a username and an email confirmed
22
+ * with `POST /auth/email/verify/{start,confirm}` (purpose `signup`).
23
+ *
24
+ * Every body that ends in a session may carry `device`, the proof of the
25
+ * browser's shared device (ADR 0029 D2), so the account lands on it.
26
+ */
27
+ import { z } from 'zod';
28
+ import { EMAIL_CODE_LENGTH, emailAddressSchema, emailTicketSchema } from './accountEmail.js';
29
+ import { deviceProofSchema } from './deviceSession.js';
30
+ /** How long the link in a sign-in email can be opened. */
31
+ export const EMAIL_SIGNIN_LINK_TTL_MS = 15 * 60 * 1000;
32
+ /** How long a second-factor challenge can be answered. */
33
+ export const SIGNIN_SECOND_FACTOR_TTL_MS = 5 * 60 * 1000;
34
+ /** Wrong second-factor codes one challenge accepts. */
35
+ export const SIGNIN_SECOND_FACTOR_MAX_ATTEMPTS = 5;
36
+ /** Shortest password Oxy accepts when one is set. */
37
+ export const PASSWORD_MIN_LENGTH = 10;
38
+ /** Longest password Oxy accepts (characters). */
39
+ export const PASSWORD_MAX_LENGTH = 256;
40
+ /** Digits in an authenticator code. */
41
+ export const TOTP_DIGITS = 6;
42
+ /** Seconds each authenticator code lasts. */
43
+ export const TOTP_PERIOD_SECONDS = 30;
44
+ /** Backup codes issued when an authenticator is enabled or they are regenerated. */
45
+ export const TOTP_BACKUP_CODE_COUNT = 10;
46
+ /**
47
+ * The long sign-in code: ten characters of Crockford base32 without the
48
+ * look-alikes (no 0, O, 1, I, L, U), shown as `XXXXX-XXXXX`. An account whose
49
+ * sign-in codes were guessed at too often today (the per-account ceiling) is
50
+ * sent this instead of 6 digits for the rest of the day, so its owner can still
51
+ * type a code while guessing one is infeasible.
52
+ */
53
+ export const EMAIL_SIGNIN_LONG_CODE_ALPHABET = '23456789ABCDEFGHJKMNPQRSTVWXYZ';
54
+ export const EMAIL_SIGNIN_LONG_CODE_LENGTH = 10;
55
+ /**
56
+ * Normalise a typed sign-in code: 6 digits stay as they are; a long code is
57
+ * upper-cased with its dash and spaces removed. Anything else is returned
58
+ * trimmed (and will simply be wrong).
59
+ */
60
+ export function normalizeEmailSignInCode(code) {
61
+ const compact = code.trim().replace(/[\s-]/g, '').toUpperCase();
62
+ return compact;
63
+ }
64
+ const sixDigits = z
65
+ .string()
66
+ .trim()
67
+ .regex(new RegExp(`^\\d{${EMAIL_CODE_LENGTH}}$`), `code must be ${EMAIL_CODE_LENGTH} digits`);
68
+ /**
69
+ * 32 random bytes, base64url: a request secret, a link token, a challenge id.
70
+ * The same shape as an email ticket.
71
+ */
72
+ const opaqueTokenSchema = emailTicketSchema;
73
+ /** A username or an email. */
74
+ export const signInIdentifierSchema = z.string().trim().min(1).max(254);
75
+ /** A password as typed. Its policy applies only when one is set. */
76
+ export const passwordInputSchema = z.string().min(1).max(PASSWORD_MAX_LENGTH);
77
+ /** A new password. */
78
+ export const newPasswordSchema = z
79
+ .string()
80
+ .min(PASSWORD_MIN_LENGTH, `password must be at least ${PASSWORD_MIN_LENGTH} characters`)
81
+ .max(PASSWORD_MAX_LENGTH, `password must be at most ${PASSWORD_MAX_LENGTH} characters`);
82
+ /**
83
+ * An authenticator code (6 digits) or a backup code (10 characters, with or
84
+ * without its dash). Normalised server-side.
85
+ */
86
+ export const secondFactorCodeSchema = z.string().trim().min(TOTP_DIGITS).max(16);
87
+ /** The device-session fields every body that ends in a session may carry. */
88
+ const sessionEnvelope = {
89
+ deviceName: z.string().trim().min(1).max(120).optional(),
90
+ deviceFingerprint: z.string().trim().min(1).max(256).optional(),
91
+ /** Proof of the browser's shared device; invalid → the session gets its own. */
92
+ device: deviceProofSchema.optional(),
93
+ };
94
+ /** `POST /auth/signin/email/start` */
95
+ export const emailSignInStartRequestSchema = z
96
+ .object({
97
+ identifier: signInIdentifierSchema,
98
+ /** The requester's device: the email's link approves only in this browser. */
99
+ device: deviceProofSchema.optional(),
100
+ })
101
+ .strict();
102
+ export const emailSignInStartResponseSchema = z.object({
103
+ requestId: z.string().min(1).max(64),
104
+ requestSecret: opaqueTokenSchema,
105
+ expiresAt: z.number().int().positive(),
106
+ retryLater: z.literal(true).optional(),
107
+ });
108
+ /**
109
+ * The code from a sign-in email: 6 digits, or — for an account whose codes
110
+ * were guessed at too often today — the 10-character long code
111
+ * (`EMAIL_SIGNIN_LONG_CODE_ALPHABET`, with or without its dash, any case). The
112
+ * UI must accept BOTH in one field: the email says which was sent, and the
113
+ * start response is the same either way (it says nothing about the account).
114
+ */
115
+ export const emailSignInCodeSchema = z
116
+ .string()
117
+ .trim()
118
+ .refine((value) => {
119
+ const code = normalizeEmailSignInCode(value);
120
+ return (new RegExp(`^\\d{${EMAIL_CODE_LENGTH}}$`).test(code) ||
121
+ new RegExp(`^[${EMAIL_SIGNIN_LONG_CODE_ALPHABET}]{${EMAIL_SIGNIN_LONG_CODE_LENGTH}}$`).test(code));
122
+ }, { message: `code must be ${EMAIL_CODE_LENGTH} digits or the ${EMAIL_SIGNIN_LONG_CODE_LENGTH}-character code from the email` });
123
+ /** `POST /auth/signin/email/confirm` — the code from the email. */
124
+ export const emailSignInConfirmRequestSchema = z
125
+ .object({
126
+ requestId: z.string().trim().min(1).max(64),
127
+ requestSecret: opaqueTokenSchema,
128
+ code: emailSignInCodeSchema,
129
+ ...sessionEnvelope,
130
+ })
131
+ .strict();
132
+ /** `POST /auth/signin/email/collect` — the dialog asks whether its link was opened. */
133
+ export const emailSignInCollectRequestSchema = z
134
+ .object({
135
+ requestId: z.string().trim().min(1).max(64),
136
+ requestSecret: opaqueTokenSchema,
137
+ ...sessionEnvelope,
138
+ })
139
+ .strict();
140
+ /**
141
+ * `POST /auth/signin/email/link` — auth.oxy.so, where the email's link lands.
142
+ * It proves auth.oxy.so's own credential for the browser's device; the request
143
+ * is approved only when that is the device that asked.
144
+ */
145
+ export const emailSignInLinkRequestSchema = z
146
+ .object({
147
+ token: opaqueTokenSchema,
148
+ device: deviceProofSchema,
149
+ })
150
+ .strict();
151
+ export const emailSignInLinkResponseSchema = z.object({
152
+ approved: z.literal(true),
153
+ });
154
+ /** `POST /auth/signin/password` */
155
+ export const passwordSignInRequestSchema = z
156
+ .object({
157
+ identifier: signInIdentifierSchema,
158
+ password: passwordInputSchema,
159
+ ...sessionEnvelope,
160
+ })
161
+ .strict();
162
+ /** `POST /auth/signin/second-factor` */
163
+ export const secondFactorSignInRequestSchema = z
164
+ .object({
165
+ challengeId: opaqueTokenSchema,
166
+ code: secondFactorCodeSchema,
167
+ ...sessionEnvelope,
168
+ })
169
+ .strict();
170
+ /** `POST /auth/signup` — the account, from a username and a confirmed email. */
171
+ export const signUpRequestSchema = z
172
+ .object({
173
+ username: z.string().trim().min(1).max(60),
174
+ email: emailAddressSchema,
175
+ /** The ticket `POST /auth/email/verify/confirm` (purpose `signup`) returned. */
176
+ emailTicket: emailTicketSchema,
177
+ ...sessionEnvelope,
178
+ })
179
+ .strict();
180
+ export const secondFactorRequiredSchema = z.object({
181
+ secondFactorRequired: z.literal(true),
182
+ challengeId: opaqueTokenSchema,
183
+ expiresAt: z.number().int().positive(),
184
+ });
185
+ export const emailSignInPendingSchema = z.object({
186
+ status: z.literal('pending'),
187
+ expiresAt: z.number().int().positive(),
188
+ });
189
+ /** Narrow a {@link SignInStepResult}. */
190
+ export function isSecondFactorRequired(result) {
191
+ return result.secondFactorRequired === true;
192
+ }
193
+ /**
194
+ * What a re-verification email code confirms. The code is bound to it: a code
195
+ * asked for one change never authorises another, and the email names it.
196
+ */
197
+ export const REAUTH_ACTIONS = ['change_password', 'totp', 'link_commons', 'delete_account'];
198
+ /** `POST /users/me/reauth/email` */
199
+ export const reauthEmailStartRequestSchema = z.object({ action: z.enum(REAUTH_ACTIONS) }).strict();
200
+ /**
201
+ * A fresh proof that the person, not only their session, is asking: the
202
+ * current password, or a code just sent to the account's email
203
+ * (`POST /users/me/reauth/email`) — plus the authenticator code when the
204
+ * account has one. Carried inside the request it authorises.
205
+ */
206
+ export const reauthProofSchema = z
207
+ .object({
208
+ password: passwordInputSchema.optional(),
209
+ emailCode: z
210
+ .object({
211
+ verificationId: z.string().trim().min(1).max(64),
212
+ code: sixDigits,
213
+ })
214
+ .strict()
215
+ .optional(),
216
+ /** Required when the account has an authenticator: its code or a backup code. */
217
+ totpCode: secondFactorCodeSchema.optional(),
218
+ })
219
+ .strict()
220
+ .refine((proof) => proof.password !== undefined || proof.emailCode !== undefined, {
221
+ message: 'password or emailCode is required',
222
+ });
223
+ /** A proof by email code only (+ TOTP): deleting an account, linking Commons. */
224
+ export const emailReauthProofSchema = z
225
+ .object({
226
+ emailCode: z
227
+ .object({
228
+ verificationId: z.string().trim().min(1).max(64),
229
+ code: sixDigits,
230
+ })
231
+ .strict(),
232
+ totpCode: secondFactorCodeSchema.optional(),
233
+ })
234
+ .strict();
235
+ /** `PUT /users/me/password` — set or change the password. */
236
+ export const passwordSetRequestSchema = z
237
+ .object({
238
+ newPassword: newPasswordSchema,
239
+ reauth: reauthProofSchema,
240
+ /** Sign every other session of the account out. */
241
+ revokeOtherSessions: z.boolean().optional(),
242
+ })
243
+ .strict();
244
+ export const signInMethodsSchema = z.object({
245
+ hasEmail: z.boolean(),
246
+ hasPassword: z.boolean(),
247
+ totpEnabled: z.boolean(),
248
+ backupCodesRemaining: z.number().int().min(0),
249
+ });
250
+ export const totpEnrollResponseSchema = z.object({
251
+ secret: z.string().regex(/^[A-Z2-7]+$/),
252
+ otpauthUri: z.string().startsWith('otpauth://totp/'),
253
+ });
254
+ /** `POST /users/me/totp/confirm` — the first code from the authenticator turns it on. */
255
+ export const totpConfirmRequestSchema = z
256
+ .object({
257
+ code: z.string().trim().regex(new RegExp(`^\\d{${TOTP_DIGITS}}$`)),
258
+ reauth: reauthProofSchema,
259
+ })
260
+ .strict();
261
+ /** `POST /users/me/totp/disable` and `POST /users/me/totp/backup-codes` */
262
+ export const totpReauthRequestSchema = z.object({ reauth: reauthProofSchema }).strict();
263
+ export const totpBackupCodesResponseSchema = z.object({
264
+ backupCodes: z.array(z.string().regex(/^[a-z2-9]{5}-[a-z2-9]{5}$/)).length(TOTP_BACKUP_CODE_COUNT),
265
+ });
266
+ /**
267
+ * Stable error codes (`error.code` in the API error body). Clients map these
268
+ * through their localization, never the English message.
269
+ */
270
+ export const SIGN_IN_ERROR_CODES = {
271
+ /** Wrong identifier or password — never says which. */
272
+ invalidCredentials: 'SIGNIN_INVALID_CREDENTIALS',
273
+ /** The request is unknown, expired, spent, or its secret is wrong. */
274
+ requestInvalid: 'SIGNIN_REQUEST_INVALID',
275
+ /** The link is unknown, expired or already used. */
276
+ linkInvalid: 'SIGNIN_LINK_INVALID',
277
+ /** The link was opened in another browser: type the code in the app instead. */
278
+ linkOtherDevice: 'SIGNIN_LINK_OTHER_DEVICE',
279
+ /** The second-factor challenge or its code is wrong, expired or spent. */
280
+ secondFactorInvalid: 'SECOND_FACTOR_INVALID',
281
+ /** Too many failures for this account: wait and try again. */
282
+ locked: 'SIGNIN_LOCKED',
283
+ /** The step needs a fresh proof (`reauth`). */
284
+ reauthRequired: 'REAUTH_REQUIRED',
285
+ /** The fresh proof was wrong. */
286
+ reauthInvalid: 'REAUTH_INVALID',
287
+ /** The account has an authenticator: the proof must carry its code. */
288
+ totpRequired: 'TOTP_REQUIRED',
289
+ /** Enrolling or confirming an authenticator that is already on. */
290
+ totpAlreadyEnabled: 'TOTP_ALREADY_ENABLED',
291
+ /** Disabling, confirming or regenerating codes with no authenticator (or no pending one). */
292
+ totpNotEnabled: 'TOTP_NOT_ENABLED',
293
+ /** The code the authenticator showed at enrolment is wrong. */
294
+ totpCodeInvalid: 'TOTP_CODE_INVALID',
295
+ /** Only official Oxy apps and auth.oxy.so may sign people in here. */
296
+ originNotAllowed: 'SIGNIN_ORIGIN_NOT_ALLOWED',
297
+ /** The username is taken. */
298
+ usernameTaken: 'USERNAME_TAKEN',
299
+ };