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