@oxy.so/contracts 4.16.0 → 4.17.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 +12 -34
- package/dist/cjs/accountGraph.js +22 -320
- package/dist/cjs/agentKeys.js +11 -13
- package/dist/cjs/client/accountEmail.js +48 -0
- package/dist/cjs/client/accountGraph.js +323 -0
- package/dist/cjs/client/agentKeys.js +77 -0
- package/dist/cjs/client/check.js +466 -0
- package/dist/cjs/client/deviceBoot.js +22 -0
- package/dist/cjs/client/deviceDirectory.js +51 -0
- package/dist/cjs/client/deviceSession.js +63 -0
- package/dist/cjs/client/externalIdentity.js +19 -0
- package/dist/cjs/client/identity.js +45 -0
- package/dist/cjs/client/identityLink.js +55 -0
- package/dist/cjs/client/identityProof.js +109 -0
- package/dist/cjs/client/index.js +190 -0
- package/dist/cjs/client/organizations.js +114 -0
- package/dist/cjs/client/products.js +153 -0
- package/dist/cjs/client/recommendations.js +33 -0
- package/dist/cjs/client/reputation.js +41 -0
- package/dist/cjs/client/safety.js +15 -0
- package/dist/cjs/client/sessionStatus.js +23 -0
- package/dist/cjs/client/signIn.js +157 -0
- package/dist/cjs/client/userResponse.js +15 -0
- package/dist/cjs/client/username.js +173 -0
- package/dist/cjs/deviceSession.js +3 -16
- package/dist/cjs/identityLink.js +13 -34
- package/dist/cjs/identityProof.js +18 -106
- package/dist/cjs/organizations.js +21 -41
- package/dist/cjs/reputation.js +3 -34
- package/dist/cjs/signIn.js +33 -111
- package/dist/cjs/username.js +23 -158
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/accountEmail.js +4 -32
- package/dist/esm/accountGraph.js +4 -309
- package/dist/esm/agentKeys.js +4 -10
- package/dist/esm/client/accountEmail.js +45 -0
- package/dist/esm/client/accountGraph.js +314 -0
- package/dist/esm/client/agentKeys.js +74 -0
- package/dist/esm/client/check.js +428 -0
- package/dist/esm/client/deviceBoot.js +19 -0
- package/dist/esm/client/deviceDirectory.js +48 -0
- package/dist/esm/client/deviceSession.js +60 -0
- package/dist/esm/client/externalIdentity.js +16 -0
- package/dist/esm/client/identity.js +42 -0
- package/dist/esm/client/identityLink.js +50 -0
- package/dist/esm/client/identityProof.js +104 -0
- package/dist/esm/client/index.js +37 -0
- package/dist/esm/client/organizations.js +111 -0
- package/dist/esm/client/products.js +150 -0
- package/dist/esm/client/recommendations.js +30 -0
- package/dist/esm/client/reputation.js +37 -0
- package/dist/esm/client/safety.js +12 -0
- package/dist/esm/client/sessionStatus.js +20 -0
- package/dist/esm/client/signIn.js +151 -0
- package/dist/esm/client/userResponse.js +12 -0
- package/dist/esm/client/username.js +167 -0
- package/dist/esm/deviceSession.js +1 -15
- package/dist/esm/identityLink.js +4 -27
- package/dist/esm/identityProof.js +4 -99
- package/dist/esm/organizations.js +4 -31
- package/dist/esm/reputation.js +1 -32
- package/dist/esm/signIn.js +4 -96
- package/dist/esm/username.js +4 -148
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/accountEmail.d.ts +2 -34
- package/dist/types/accountGraph.d.ts +12 -208
- package/dist/types/agency.d.ts +24 -24
- package/dist/types/agentKeys.d.ts +8 -6
- package/dist/types/client/accountEmail.d.ts +44 -0
- package/dist/types/client/accountGraph.d.ts +203 -0
- package/dist/types/client/agentKeys.d.ts +113 -0
- package/dist/types/client/check.d.ts +184 -0
- package/dist/types/client/deviceBoot.d.ts +6 -0
- package/dist/types/client/deviceDirectory.d.ts +134 -0
- package/dist/types/client/deviceSession.d.ts +91 -0
- package/dist/types/client/externalIdentity.d.ts +14 -0
- package/dist/types/client/identity.d.ts +8 -0
- package/dist/types/client/identityLink.d.ts +21 -0
- package/dist/types/client/identityProof.d.ts +73 -0
- package/dist/types/client/index.d.ts +37 -0
- package/dist/types/client/organizations.d.ts +167 -0
- package/dist/types/client/products.d.ts +145 -0
- package/dist/types/client/recommendations.d.ts +27 -0
- package/dist/types/client/reputation.d.ts +28 -0
- package/dist/types/client/safety.d.ts +13 -0
- package/dist/types/client/sessionStatus.d.ts +19 -0
- package/dist/types/client/signIn.d.ts +107 -0
- package/dist/types/client/userResponse.d.ts +7 -0
- package/dist/types/client/username.d.ts +126 -0
- package/dist/types/deviceDirectory.d.ts +28 -28
- package/dist/types/deviceSession.d.ts +1 -12
- package/dist/types/externalIdentity.d.ts +11 -11
- package/dist/types/identityLink.d.ts +3 -11
- package/dist/types/identityProof.d.ts +2 -65
- package/dist/types/inference/decisions.d.ts +10 -10
- package/dist/types/inference/embeddings.d.ts +16 -16
- package/dist/types/inference/errors.d.ts +8 -8
- package/dist/types/inference/inbox.d.ts +8 -8
- package/dist/types/inference/privateAutoExecution.d.ts +6 -6
- package/dist/types/inference/realtime.d.ts +30 -30
- package/dist/types/inference/request.d.ts +28 -28
- package/dist/types/inference/streamEvents.d.ts +16 -16
- package/dist/types/inference/usage.d.ts +4 -4
- package/dist/types/oauth.d.ts +2 -2
- package/dist/types/organizations.d.ts +8 -10
- package/dist/types/products/billing.d.ts +3 -0
- package/dist/types/reputation.d.ts +1 -11
- package/dist/types/sessionStatus.d.ts +2 -2
- package/dist/types/signIn.d.ts +6 -89
- package/dist/types/userResponse.d.ts +20 -20
- package/dist/types/username.d.ts +1 -96
- package/package.json +20 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity-proof message format, without Zod. Schemas: `../identityProof.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
export const IDENTITY_PROOF_VERSION = 2;
|
|
7
|
+
export const IDENTITY_PROOF_DOMAIN = 'oxy-identity-proof';
|
|
8
|
+
/** The audience every API-verified identity proof names. */
|
|
9
|
+
export const IDENTITY_PROOF_AUDIENCE = 'oxy-api/identity';
|
|
10
|
+
/** How long a proof challenge lives: one interactive ceremony. */
|
|
11
|
+
export const IDENTITY_PROOF_CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
/**
|
|
13
|
+
* Everything a root proof may authorize. A challenge is minted for exactly one
|
|
14
|
+
* action and spent only by a proof for that action.
|
|
15
|
+
*/
|
|
16
|
+
export const IDENTITY_PROOF_ACTIONS = {
|
|
17
|
+
/**
|
|
18
|
+
* Link Commons' root to an account that has none (`POST /auth/link`).
|
|
19
|
+
* The account becomes self-custodied and its email is deleted
|
|
20
|
+
* (ADR 0029 D3).
|
|
21
|
+
*/
|
|
22
|
+
link: 'link_identity',
|
|
23
|
+
};
|
|
24
|
+
export const IDENTITY_PROOF_ACTION_VALUES = Object.values(IDENTITY_PROOF_ACTIONS);
|
|
25
|
+
export const HEX_DIGEST = /^[0-9a-f]{64}$/;
|
|
26
|
+
export const ROOT_KEY = /^04[0-9a-f]{128}$/;
|
|
27
|
+
export const CHALLENGE = /^[0-9a-f]{64}$/;
|
|
28
|
+
/**
|
|
29
|
+
* Canonical JSON: object keys sorted by UTF-16 code unit, no whitespace,
|
|
30
|
+
* `undefined` members omitted, arrays in order. Numbers must be finite. This is
|
|
31
|
+
* the ONLY serializer for anything digested into a proof.
|
|
32
|
+
*/
|
|
33
|
+
export function canonicalJson(value) {
|
|
34
|
+
if (value === null)
|
|
35
|
+
return 'null';
|
|
36
|
+
switch (typeof value) {
|
|
37
|
+
case 'string':
|
|
38
|
+
case 'boolean':
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
case 'number':
|
|
41
|
+
if (!Number.isFinite(value))
|
|
42
|
+
throw new Error('canonicalJson: non-finite number');
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
case 'object': {
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return `[${value.map((entry) => (entry === undefined ? 'null' : canonicalJson(entry))).join(',')}]`;
|
|
47
|
+
}
|
|
48
|
+
const record = value;
|
|
49
|
+
const keys = Object.keys(record)
|
|
50
|
+
.filter((key) => record[key] !== undefined)
|
|
51
|
+
.sort();
|
|
52
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`canonicalJson: unsupported ${typeof value}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The exact bytes a root signs. Throws on a malformed claim rather than signing
|
|
60
|
+
* (or verifying) something ambiguous.
|
|
61
|
+
*/
|
|
62
|
+
export function buildIdentityProofMessage(claims) {
|
|
63
|
+
if (!IDENTITY_PROOF_ACTION_VALUES.includes(claims.action))
|
|
64
|
+
throw new Error('identity proof: unknown action');
|
|
65
|
+
if (!claims.subject || !claims.actor)
|
|
66
|
+
throw new Error('identity proof: subject and actor are required');
|
|
67
|
+
if (!ROOT_KEY.test(claims.rootPublicKey))
|
|
68
|
+
throw new Error('identity proof: rootPublicKey must be canonical');
|
|
69
|
+
if (claims.payloadDigest !== null && !HEX_DIGEST.test(claims.payloadDigest)) {
|
|
70
|
+
throw new Error('identity proof: payloadDigest must be a lowercase SHA-256 hex digest');
|
|
71
|
+
}
|
|
72
|
+
if (claims.expectedRevision !== null &&
|
|
73
|
+
(!Number.isSafeInteger(claims.expectedRevision) || claims.expectedRevision < 0)) {
|
|
74
|
+
throw new Error('identity proof: expectedRevision must be a non-negative integer');
|
|
75
|
+
}
|
|
76
|
+
if (!CHALLENGE.test(claims.challenge))
|
|
77
|
+
throw new Error('identity proof: challenge must be 64 lowercase hex characters');
|
|
78
|
+
if (!Number.isSafeInteger(claims.expiresAt) || claims.expiresAt <= 0)
|
|
79
|
+
throw new Error('identity proof: expiresAt must be unix milliseconds');
|
|
80
|
+
return canonicalJson({
|
|
81
|
+
v: IDENTITY_PROOF_VERSION,
|
|
82
|
+
domain: IDENTITY_PROOF_DOMAIN,
|
|
83
|
+
action: claims.action,
|
|
84
|
+
subject: claims.subject,
|
|
85
|
+
actor: claims.actor,
|
|
86
|
+
rootPublicKey: claims.rootPublicKey,
|
|
87
|
+
payloadDigest: claims.payloadDigest,
|
|
88
|
+
expectedRevision: claims.expectedRevision,
|
|
89
|
+
audience: claims.audience,
|
|
90
|
+
challenge: claims.challenge,
|
|
91
|
+
expiresAt: claims.expiresAt,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stable error codes the root routes answer with (`error.code` in the API error
|
|
96
|
+
* body). Clients map these through their localization, never the English message.
|
|
97
|
+
*/
|
|
98
|
+
export const IDENTITY_ERROR_CODES = {
|
|
99
|
+
proofInvalid: 'IDENTITY_PROOF_INVALID',
|
|
100
|
+
rootAlreadyLinked: 'IDENTITY_ROOT_ALREADY_LINKED',
|
|
101
|
+
rootLinkedElsewhere: 'IDENTITY_ROOT_LINKED_ELSEWHERE',
|
|
102
|
+
freshFactorRequired: 'IDENTITY_FRESH_FACTOR_REQUIRED',
|
|
103
|
+
notPersonal: 'IDENTITY_NOT_PERSONAL_ACCOUNT',
|
|
104
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@oxy.so/contracts/client` — the contracts without Zod, for client bundles.
|
|
3
|
+
*
|
|
4
|
+
* The root entry is the contract: Zod schemas the API parses with, and the
|
|
5
|
+
* types inferred from them. Zod's runtime is ~120 KB of JavaScript, evaluated
|
|
6
|
+
* at startup, that a client only needed to answer "does this response have
|
|
7
|
+
* the shape its type promises". This entry answers that with small structural
|
|
8
|
+
* checks (`*Check`, used through {@link checkContract} / {@link parseContract}),
|
|
9
|
+
* and carries the constants and pure helpers clients use, so an app or SDK
|
|
10
|
+
* that imports only from here ships no Zod at all.
|
|
11
|
+
*
|
|
12
|
+
* Every check is the twin of one schema in the root entry: it is tied to the
|
|
13
|
+
* schema's type at compile time (`exact` in `./check.ts`) and to its runtime
|
|
14
|
+
* behaviour by `__tests__/clientChecks.parity.test.ts`. Types are not repeated
|
|
15
|
+
* here; import them from `@oxy.so/contracts` with `import type`.
|
|
16
|
+
*/
|
|
17
|
+
export { ContractCheckError, checkContract, formatCheckPath, parseContract, } from './check.js';
|
|
18
|
+
export { ACCOUNT_CATEGORY_IDS, ACCOUNT_CATEGORY_KINDS, ACCOUNT_KINDS, CHILD_ACCOUNT_KINDS, MAX_ACCOUNT_CATEGORIES, RETIRED_ACCOUNT_CATEGORY_IDS, SELECTABLE_ACCOUNT_CATEGORY_IDS, isAccountKind, isDelegatedActAsEligibleKind, isOperatorSwitchTargetKind, isSelectableAccountCategoryId, kindAcceptsAccountCategories, newlyAddedRetiredCategories, } from './accountGraph.js';
|
|
19
|
+
export { EMAIL_CODE_LENGTH, EMAIL_CODE_MAX_ATTEMPTS, EMAIL_CODE_TTL_MS, EMAIL_TICKET_TTL_MS, EMAIL_VERIFICATION_ERROR_CODES, EMAIL_VERIFICATION_PURPOSES, emailAddressCheck, emailTicketCheck, emailVerificationConfirmResponseCheck, emailVerificationStartResponseCheck, } from './accountEmail.js';
|
|
20
|
+
export { ACCOUNT_AUTH_METHODS, AGENT_PROOF_ACTIONS, AGENT_PROOF_AUDIENCE, AGENT_PROOF_TTL_MS, agentChallengeRequestCheck, agentKeyListResponseCheck, agentKeyOperationChallengeCheck, agentKeyOperationCheck, agentKeyOperationResultCheck, agentProofClaimsCheck, agentVerifyRequestCheck, executeAgentKeyOperationCheck, } from './agentKeys.js';
|
|
21
|
+
export { isLoopbackOrigin } from '../capabilityBindings.js';
|
|
22
|
+
export { loginResultCheck } from './deviceBoot.js';
|
|
23
|
+
export { externalIdentityReferenceCheck } from './externalIdentity.js';
|
|
24
|
+
export { signedRecordEnvelopeCheck } from './identity.js';
|
|
25
|
+
export { deviceActivateResponseCheck, deviceDirectoryCheck, deviceDirectorySyncCheck, } from './deviceDirectory.js';
|
|
26
|
+
export { SESSION_ACCOUNTS_CHANGED_EVENT, activeTokenCheck, deviceBackgroundCredentialResponseCheck, deviceJoinCodeResponseCheck, deviceJoinResponseCheck, deviceRegisterResponseCheck, deviceSessionStateCheck, deviceSessionSyncCheck, deviceTokenMintResponseCheck, sessionAccountsChangedEventCheck, } from './deviceSession.js';
|
|
27
|
+
export { IDENTITY_LINK_QR_PREFIX, IDENTITY_LINK_STATUSES, buildIdentityLinkQrPayload, identityLinkCreateResponseCheck, identityLinkStateCheck, parseIdentityLinkQrPayload, } from './identityLink.js';
|
|
28
|
+
export { IDENTITY_ERROR_CODES, IDENTITY_PROOF_ACTIONS, IDENTITY_PROOF_ACTION_VALUES, IDENTITY_PROOF_AUDIENCE, IDENTITY_PROOF_CHALLENGE_TTL_MS, IDENTITY_PROOF_DOMAIN, IDENTITY_PROOF_VERSION, buildIdentityProofMessage, canonicalJson, } from './identityProof.js';
|
|
29
|
+
export { ORGANIZATION_ALIAS_KINDS, ORGANIZATION_CLAIM_DECISIONS, ORGANIZATION_CLAIM_STATUSES, ORGANIZATION_ERROR_CODES, ORGANIZATION_NAME_MAX_LENGTH, ORGANIZATION_STATUSES, ORGANIZATION_TYPES, createOrganizationCheck, organizationBatchCheck, organizationCheck, organizationClaimCheck, organizationClaimDetailCheck, organizationClaimInputCheck, organizationClaimListCheck, organizationClaimReviewCheck, organizationCurationResultCheck, organizationExternalCheck, organizationExternalResolutionCheck, organizationImportBatchResultCheck, organizationListCheck, organizationNameCheck, updateOrganizationCheck, } from './organizations.js';
|
|
30
|
+
export { cancelProductSubscriptionCheck, personalPlanCatalogueCheck, personalPlanCheckoutRequestCheck, personalPlanCheckoutResultCheck, productSubscriptionCancellationResultCheck, productSubscriptionsResponseCheck, subjectProductAccessCheck, subjectProductAccessQueryCheck, subscriptionCreditGrantsResponseCheck, } from './products.js';
|
|
31
|
+
export { recommendationRequestCheck } from './recommendations.js';
|
|
32
|
+
export { isFullReputationBalance } from './reputation.js';
|
|
33
|
+
export { profileAccessDecisionCheck, profileAccessInputCheck } from './safety.js';
|
|
34
|
+
export { publicApplicationCheck } from './sessionStatus.js';
|
|
35
|
+
export { EMAIL_ACCOUNTS_MAX, EMAIL_SIGNIN_CHOICE_TTL_MS, EMAIL_SIGNIN_LINK_TTL_MS, EMAIL_SIGNIN_LONG_CODE_ALPHABET, EMAIL_SIGNIN_LONG_CODE_LENGTH, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, REAUTH_ACTIONS, SIGNIN_SECOND_FACTOR_MAX_ATTEMPTS, SIGNIN_SECOND_FACTOR_TTL_MS, SIGN_IN_ERROR_CODES, TOTP_BACKUP_CODE_COUNT, TOTP_DIGITS, TOTP_PERIOD_SECONDS, emailSignInChooseAccountCheck, emailSignInLinkResponseCheck, emailSignInPendingCheck, emailSignInStartResponseCheck, isEmailSignInChooseAccount, isSecondFactorRequired, normalizeEmailSignInCode, reauthProofCheck, secondFactorRequiredCheck, signInMethodsCheck, totpBackupCodesResponseCheck, totpEnrollResponseCheck, } from './signIn.js';
|
|
36
|
+
export { BOT_USERNAME_INVALID_MESSAGE, BOT_USERNAME_SUFFIX, NUMERIC_USERNAME_MESSAGE, RESERVED_USERNAMES, RESERVED_USERNAME_MESSAGE, USERNAME_INVALID_MESSAGE, USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH, USERNAME_PATTERN_SOURCE, applyBotUsernameSuffix, botUsernameCheck, stripDisallowedUsernameCharacters, usernameCheck, usernameCheckForAccountKind, } from './username.js';
|
|
37
|
+
export { userNameCheck } from './userResponse.js';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization vocabularies and checks, without Zod. Schemas: `../organizations.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { array, boolean, datetime, exact, maxItems, maxLength, minLength, nullable, oneOf, optional, pattern, refine, strictObject, string, trim, url, } from './check.js';
|
|
7
|
+
/** Real-world identity classification, independent of account topics and permissions. */
|
|
8
|
+
export const ORGANIZATION_TYPES = [
|
|
9
|
+
'business',
|
|
10
|
+
'nonprofit',
|
|
11
|
+
'public_body',
|
|
12
|
+
'community',
|
|
13
|
+
'other',
|
|
14
|
+
];
|
|
15
|
+
export const ORGANIZATION_STATUSES = ['active', 'inactive', 'merged', 'suppressed'];
|
|
16
|
+
export const ORGANIZATION_CLAIM_STATUSES = ['unclaimed', 'claimed'];
|
|
17
|
+
export const ORGANIZATION_ALIAS_KINDS = [
|
|
18
|
+
'name_variant',
|
|
19
|
+
'former_name',
|
|
20
|
+
'localized_name',
|
|
21
|
+
'marketing_name',
|
|
22
|
+
'misspelling',
|
|
23
|
+
];
|
|
24
|
+
export const ORGANIZATION_CLAIM_DECISIONS = ['pending', 'approved', 'rejected'];
|
|
25
|
+
export const ORGANIZATION_NAME_MAX_LENGTH = 300;
|
|
26
|
+
export const ORGANIZATION_ERROR_CODES = [
|
|
27
|
+
'bad_request',
|
|
28
|
+
'unauthorized',
|
|
29
|
+
'forbidden',
|
|
30
|
+
'not_found',
|
|
31
|
+
'unknown_route',
|
|
32
|
+
'conflict',
|
|
33
|
+
'validation_failed',
|
|
34
|
+
'rate_limited',
|
|
35
|
+
'internal_error',
|
|
36
|
+
'service_unavailable',
|
|
37
|
+
];
|
|
38
|
+
/* -------------------------------------------------------------------------- */
|
|
39
|
+
/* Checks */
|
|
40
|
+
/* -------------------------------------------------------------------------- */
|
|
41
|
+
const id = string(minLength(1), maxLength(128));
|
|
42
|
+
const optionalText = (max) => optional(nullable(string(trim, minLength(1), maxLength(max))));
|
|
43
|
+
const evidenceReference = string(trim, minLength(1), maxLength(1000));
|
|
44
|
+
const createdAt = string(datetime);
|
|
45
|
+
const organizationAlias = strictObject({
|
|
46
|
+
alias: string(trim, minLength(1), maxLength(300)),
|
|
47
|
+
kind: oneOf(ORGANIZATION_ALIAS_KINDS),
|
|
48
|
+
language: optional(string(minLength(2), maxLength(35))),
|
|
49
|
+
});
|
|
50
|
+
/** The twin of `organizationNameSchema`. */
|
|
51
|
+
export const organizationNameCheck = refine(string(trim, minLength(1), maxLength(ORGANIZATION_NAME_MAX_LENGTH)), (value) => !/[<>]/.test(value) &&
|
|
52
|
+
Array.from(value).every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127), 'Organization name must be plain text');
|
|
53
|
+
const organizationFields = {
|
|
54
|
+
name: organizationNameCheck,
|
|
55
|
+
organizationType: oneOf(ORGANIZATION_TYPES),
|
|
56
|
+
legalName: optionalText(300),
|
|
57
|
+
websiteUrl: optional(nullable(refine(string(url, maxLength(2048)), (value) => /^https?:\/\//.test(value), 'Use HTTP or HTTPS'))),
|
|
58
|
+
countryCode: optional(nullable(string(pattern(/^[A-Z]{2}$/)))),
|
|
59
|
+
aliases: optional(array(organizationAlias, maxItems(100))),
|
|
60
|
+
};
|
|
61
|
+
export const createOrganizationCheck = exact()(strictObject(organizationFields));
|
|
62
|
+
export const updateOrganizationCheck = exact()(strictObject({
|
|
63
|
+
...organizationFields,
|
|
64
|
+
name: optional(organizationNameCheck),
|
|
65
|
+
organizationType: optional(organizationFields.organizationType),
|
|
66
|
+
}));
|
|
67
|
+
export const organizationCheck = exact()(strictObject({
|
|
68
|
+
name: organizationNameCheck,
|
|
69
|
+
organizationType: oneOf(ORGANIZATION_TYPES),
|
|
70
|
+
legalName: nullable(string()),
|
|
71
|
+
websiteUrl: nullable(string()),
|
|
72
|
+
countryCode: nullable(string()),
|
|
73
|
+
aliases: array(organizationAlias),
|
|
74
|
+
accountId: id,
|
|
75
|
+
username: nullable(string()),
|
|
76
|
+
avatar: nullable(string()),
|
|
77
|
+
verifiedDomains: array(string()),
|
|
78
|
+
claimStatus: oneOf(ORGANIZATION_CLAIM_STATUSES),
|
|
79
|
+
status: oneOf(ORGANIZATION_STATUSES),
|
|
80
|
+
mergedIntoAccountId: nullable(id),
|
|
81
|
+
createdAt,
|
|
82
|
+
updatedAt: string(datetime),
|
|
83
|
+
}));
|
|
84
|
+
export const organizationListCheck = exact()(strictObject({ items: array(organizationCheck), nextCursor: nullable(string()) }));
|
|
85
|
+
export const organizationBatchCheck = exact()(strictObject({
|
|
86
|
+
items: array(strictObject({ requestedAccountId: id, organization: organizationCheck })),
|
|
87
|
+
missingIds: array(id),
|
|
88
|
+
}));
|
|
89
|
+
export const organizationClaimInputCheck = exact()(strictObject({ evidenceReference }));
|
|
90
|
+
const organizationClaimFields = {
|
|
91
|
+
id,
|
|
92
|
+
accountId: id,
|
|
93
|
+
claimantUserId: id,
|
|
94
|
+
status: oneOf(ORGANIZATION_CLAIM_DECISIONS),
|
|
95
|
+
createdAt,
|
|
96
|
+
};
|
|
97
|
+
export const organizationClaimCheck = exact()(strictObject(organizationClaimFields));
|
|
98
|
+
export const organizationClaimDetailCheck = exact()(strictObject({
|
|
99
|
+
...organizationClaimFields,
|
|
100
|
+
evidenceReference: string(),
|
|
101
|
+
reviewEvidenceReference: nullable(string()),
|
|
102
|
+
reviewedAt: nullable(string(datetime)),
|
|
103
|
+
}));
|
|
104
|
+
export const organizationClaimListCheck = exact()(strictObject({ items: array(organizationClaimDetailCheck), nextCursor: nullable(string()) }));
|
|
105
|
+
export const organizationClaimReviewCheck = exact()(strictObject({ decision: oneOf(['approved', 'rejected']), evidenceReference }));
|
|
106
|
+
export const organizationImportBatchResultCheck = exact()(strictObject({
|
|
107
|
+
items: array(strictObject({ externalId: string(), accountId: id, created: boolean })),
|
|
108
|
+
}));
|
|
109
|
+
export const organizationExternalCheck = exact()(strictObject({ accountId: id, created: boolean }));
|
|
110
|
+
export const organizationExternalResolutionCheck = exact()(strictObject({ accountId: id }));
|
|
111
|
+
export const organizationCurationResultCheck = exact()(strictObject({ accountId: id, status: oneOf(['active', 'inactive', 'suppressed']) }));
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product access, plans, checkout and subscription checks, without Zod.
|
|
3
|
+
* Schemas: `../products/*.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
6
|
+
*/
|
|
7
|
+
import { array, boolean, branded, datetime, discriminatedUnion, exact, int, literal, maxLength, minLength, nonnegative, nullable, number, oneOf, optional, pattern, positive, refine, safe, startsWith, strictObject, string, union, url, } from './check.js';
|
|
8
|
+
const identifier = string(minLength(1), maxLength(160));
|
|
9
|
+
const displayName = string(minLength(1), maxLength(100));
|
|
10
|
+
const count = number(int, nonnegative, safe);
|
|
11
|
+
const version = number(int, positive, safe);
|
|
12
|
+
const oxyAccountId = branded(string(minLength(1), maxLength(64)));
|
|
13
|
+
const actionKey = string(minLength(8), maxLength(160), pattern(/^[a-zA-Z0-9_-]+$/));
|
|
14
|
+
const combination = oneOf(['maximum', 'sum', 'exclusive']);
|
|
15
|
+
const origin = oneOf(['individual', 'bundle']);
|
|
16
|
+
const period = (message) => refine(strictObject({ start: string(datetime), end: string(datetime) }), (value) => Date.parse(value.end) > Date.parse(value.start), message);
|
|
17
|
+
const productBenefit = discriminatedUnion('kind', {
|
|
18
|
+
capability: strictObject({ kind: literal('capability'), productId: identifier, key: identifier }),
|
|
19
|
+
quota: strictObject({
|
|
20
|
+
kind: literal('quota'),
|
|
21
|
+
productId: identifier,
|
|
22
|
+
key: identifier,
|
|
23
|
+
unit: identifier,
|
|
24
|
+
included: count,
|
|
25
|
+
combination,
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
export const subjectProductAccessQueryCheck = exact()(strictObject({
|
|
29
|
+
schemaVersion: literal(1),
|
|
30
|
+
subjectAccountId: oxyAccountId,
|
|
31
|
+
productId: identifier,
|
|
32
|
+
}));
|
|
33
|
+
export const subjectProductAccessCheck = exact()(strictObject({
|
|
34
|
+
schemaVersion: literal(1),
|
|
35
|
+
subjectAccountId: oxyAccountId,
|
|
36
|
+
productId: identifier,
|
|
37
|
+
evaluatedAt: string(datetime),
|
|
38
|
+
capabilities: array(strictObject({ key: identifier, grantIds: array(identifier) })),
|
|
39
|
+
quotas: array(strictObject({
|
|
40
|
+
key: identifier,
|
|
41
|
+
unit: identifier,
|
|
42
|
+
included: count,
|
|
43
|
+
combination,
|
|
44
|
+
grantIds: array(identifier),
|
|
45
|
+
})),
|
|
46
|
+
conflicts: array(strictObject({
|
|
47
|
+
key: identifier,
|
|
48
|
+
reason: oneOf([
|
|
49
|
+
'combination_mismatch',
|
|
50
|
+
'unit_mismatch',
|
|
51
|
+
'exclusive_overlap',
|
|
52
|
+
'unsafe_total',
|
|
53
|
+
]),
|
|
54
|
+
grantIds: array(identifier),
|
|
55
|
+
})),
|
|
56
|
+
}));
|
|
57
|
+
export const personalPlanCatalogueCheck = exact()(refine(strictObject({
|
|
58
|
+
schemaVersion: literal(1),
|
|
59
|
+
state: oneOf(['unconfigured', 'configured']),
|
|
60
|
+
purchase: literal('unavailable'),
|
|
61
|
+
plans: array(strictObject({
|
|
62
|
+
offerId: identifier,
|
|
63
|
+
offerVersion: version,
|
|
64
|
+
displayName,
|
|
65
|
+
audience: literal('personal'),
|
|
66
|
+
kind: literal('oxy_one'),
|
|
67
|
+
price: optional(strictObject({
|
|
68
|
+
currency: string(pattern(/^[A-Z]{3}$/)),
|
|
69
|
+
amountMinorUnits: version,
|
|
70
|
+
interval: literal('month'),
|
|
71
|
+
trial: literal('none'),
|
|
72
|
+
taxTreatment: literal('inclusive'),
|
|
73
|
+
merchantTotal: literal('final'),
|
|
74
|
+
})),
|
|
75
|
+
benefits: array(strictObject({ displayName, benefit: productBenefit })),
|
|
76
|
+
})),
|
|
77
|
+
}), (value) => (value.state === 'unconfigured') === (value.plans.length === 0), 'Catalogue state must match published plans'));
|
|
78
|
+
export const personalPlanCheckoutRequestCheck = exact()(strictObject({
|
|
79
|
+
expectedSubjectAccountId: oxyAccountId,
|
|
80
|
+
offerId: identifier,
|
|
81
|
+
offerVersion: version,
|
|
82
|
+
idempotencyKey: actionKey,
|
|
83
|
+
}));
|
|
84
|
+
export const personalPlanCheckoutResultCheck = exact()(discriminatedUnion('state', {
|
|
85
|
+
unconfigured: strictObject({
|
|
86
|
+
state: literal('unconfigured'),
|
|
87
|
+
reason: oneOf(['provider_unconfigured', 'offer_unconfigured', 'price_unconfigured']),
|
|
88
|
+
}),
|
|
89
|
+
pending: strictObject({
|
|
90
|
+
state: literal('pending'),
|
|
91
|
+
intentId: identifier,
|
|
92
|
+
checkoutUrl: optional(string(url, startsWith('https://'))),
|
|
93
|
+
}),
|
|
94
|
+
closed: strictObject({ state: literal('closed'), intentId: identifier }),
|
|
95
|
+
fulfilled: strictObject({ state: literal('fulfilled'), intentId: identifier }),
|
|
96
|
+
}));
|
|
97
|
+
const billingPeriod = period('Period end must follow start');
|
|
98
|
+
export const productSubscriptionsResponseCheck = exact()(strictObject({
|
|
99
|
+
subscriptions: array(strictObject({
|
|
100
|
+
sourceId: identifier,
|
|
101
|
+
status: oneOf([
|
|
102
|
+
'active',
|
|
103
|
+
'trialing',
|
|
104
|
+
'past_due',
|
|
105
|
+
'unpaid',
|
|
106
|
+
'canceled',
|
|
107
|
+
'paused',
|
|
108
|
+
'incomplete',
|
|
109
|
+
'incomplete_expired',
|
|
110
|
+
]),
|
|
111
|
+
period: billingPeriod,
|
|
112
|
+
cancelAtPeriodEnd: boolean,
|
|
113
|
+
canCancel: boolean,
|
|
114
|
+
offers: array(strictObject({
|
|
115
|
+
segmentId: identifier,
|
|
116
|
+
offerId: identifier,
|
|
117
|
+
offerVersion: version,
|
|
118
|
+
displayName,
|
|
119
|
+
origin,
|
|
120
|
+
period: billingPeriod,
|
|
121
|
+
current: boolean,
|
|
122
|
+
products: array(strictObject({ id: identifier, displayName })),
|
|
123
|
+
})),
|
|
124
|
+
})),
|
|
125
|
+
}));
|
|
126
|
+
export const subscriptionCreditGrantsResponseCheck = exact()(strictObject({
|
|
127
|
+
grants: array(refine(strictObject({
|
|
128
|
+
id: identifier,
|
|
129
|
+
invoiceId: identifier,
|
|
130
|
+
origin: oneOf([
|
|
131
|
+
'subscription_payment',
|
|
132
|
+
'subscription_proration',
|
|
133
|
+
'subscription_promotional_grant',
|
|
134
|
+
]),
|
|
135
|
+
period: billingPeriod,
|
|
136
|
+
granted: count,
|
|
137
|
+
consumed: count,
|
|
138
|
+
clawed: count,
|
|
139
|
+
remaining: count,
|
|
140
|
+
promotionId: nullable(identifier),
|
|
141
|
+
createdAt: string(datetime),
|
|
142
|
+
}), (value) => BigInt(value.consumed) + BigInt(value.clawed) + BigInt(value.remaining) ===
|
|
143
|
+
BigInt(value.granted), 'Grant conservation differs')),
|
|
144
|
+
}));
|
|
145
|
+
export const cancelProductSubscriptionCheck = exact()(strictObject({
|
|
146
|
+
sourceId: identifier,
|
|
147
|
+
actionId: optional(actionKey),
|
|
148
|
+
expectedSubjectAccountId: optional(oxyAccountId),
|
|
149
|
+
}));
|
|
150
|
+
export const productSubscriptionCancellationResultCheck = exact()(union(strictObject({ sourceId: identifier, cancelAtPeriodEnd: literal(true) }), strictObject({ sourceId: identifier, reconciliationPending: literal(true) })));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recommendation-request check, without Zod. Schemas: `../recommendations.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { array, atLeast, atMost, exact, int, maxItems, maxLength, minItems, minLength, number, object, oneOf, optional, string, trim, } from './check.js';
|
|
7
|
+
const id = string(trim, minLength(1));
|
|
8
|
+
const weight = optional(number(atLeast(0), atMost(10)));
|
|
9
|
+
export const recommendationRequestCheck = exact()(object({
|
|
10
|
+
clientId: optional(string(trim, minLength(1))),
|
|
11
|
+
limit: optional(number(int, atLeast(1), atMost(100))),
|
|
12
|
+
offset: optional(number(int, atLeast(0))),
|
|
13
|
+
excludeTypes: optional(array(oneOf(['federated', 'agent', 'automated']))),
|
|
14
|
+
excludeIds: optional(array(id, maxItems(500))),
|
|
15
|
+
boosts: optional(array(object({
|
|
16
|
+
userIds: array(id, minItems(1), maxItems(200)),
|
|
17
|
+
weight: number(atLeast(-5), atMost(5)),
|
|
18
|
+
reason: optional(string(trim, maxLength(120))),
|
|
19
|
+
}), maxItems(50))),
|
|
20
|
+
signalWeights: optional(object({
|
|
21
|
+
graph: weight,
|
|
22
|
+
completeness: weight,
|
|
23
|
+
verified: weight,
|
|
24
|
+
curation: weight,
|
|
25
|
+
interest: weight,
|
|
26
|
+
appBoost: weight,
|
|
27
|
+
repCandidate: weight,
|
|
28
|
+
affinity: weight,
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reputation view guard, without Zod. Schemas: `../reputation.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The fields the full {@link ReputationBalance} carries beyond the public
|
|
8
|
+
* {@link ReputationBalanceSummary} that the API sends ALL-OR-NOTHING. The
|
|
9
|
+
* runtime discriminant between the two views.
|
|
10
|
+
*
|
|
11
|
+
* The V2 blocks (`conduct`, `contribution`, …) are deliberately NOT listed:
|
|
12
|
+
* they are optional on the wire, so requiring them here would make a balance
|
|
13
|
+
* from a server that predates them fail to narrow, hiding the whole private
|
|
14
|
+
* view. Read a V2 block by checking that block.
|
|
15
|
+
*/
|
|
16
|
+
export const FULL_BALANCE_FIELDS = [
|
|
17
|
+
'positive',
|
|
18
|
+
'negative',
|
|
19
|
+
'breakdown',
|
|
20
|
+
'influence',
|
|
21
|
+
'reliability',
|
|
22
|
+
'recalculatedAt',
|
|
23
|
+
'updatedAt',
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Whether a balance came back as the SUBJECT view, and so carries the
|
|
27
|
+
* breakdown / influence / reliability blocks.
|
|
28
|
+
*
|
|
29
|
+
* Checks every extra field rather than one representative: the point of the
|
|
30
|
+
* guard is that the caller then dereferences those blocks, so a partial payload
|
|
31
|
+
* must not narrow.
|
|
32
|
+
*
|
|
33
|
+
* @param balance - A balance from `getReputationBalance`.
|
|
34
|
+
*/
|
|
35
|
+
export function isFullReputationBalance(balance) {
|
|
36
|
+
return FULL_BALANCE_FIELDS.every((field) => field in balance);
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-access checks, without Zod. Schemas: `../safety.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { boolean, exact, literal, maxLength, minLength, oneOf, strictObject, string, } from './check.js';
|
|
7
|
+
export const profileAccessInputCheck = exact()(strictObject({ subjectAccountId: string(minLength(1), maxLength(128)) }));
|
|
8
|
+
export const profileAccessDecisionCheck = exact()(strictObject({
|
|
9
|
+
allowed: boolean,
|
|
10
|
+
reason: oneOf(['public', 'owner', 'private', 'blocked', 'unavailable']),
|
|
11
|
+
policyVersion: literal('1'),
|
|
12
|
+
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public-application check, without Zod. Schemas: `../sessionStatus.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { array, boolean, exact, object, oneOf, optional, string } from './check.js';
|
|
7
|
+
export const publicApplicationCheck = exact()(object({
|
|
8
|
+
id: string(),
|
|
9
|
+
name: string(),
|
|
10
|
+
description: optional(string()),
|
|
11
|
+
icon: optional(string()),
|
|
12
|
+
websiteUrl: optional(string()),
|
|
13
|
+
privacyPolicyUrl: optional(string()),
|
|
14
|
+
termsUrl: optional(string()),
|
|
15
|
+
type: oneOf(['first_party', 'third_party', 'internal', 'system']),
|
|
16
|
+
isOfficial: boolean,
|
|
17
|
+
isInternal: boolean,
|
|
18
|
+
scopes: array(string()),
|
|
19
|
+
developerName: optional(string()),
|
|
20
|
+
}));
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in constants, guards and response checks, without Zod. Schemas: `../signIn.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { EMAIL_CODE_LENGTH, emailTicketCheck } from './accountEmail.js';
|
|
7
|
+
import { array, boolean, exact, exactItems, int, literal, maxItems, maxLength, minItems, minLength, nonnegative, number, object, optional, pattern, positive, refine, startsWith, strictObject, string, trim, } from './check.js';
|
|
8
|
+
/** How long the link in a sign-in email can be opened. */
|
|
9
|
+
export const EMAIL_SIGNIN_LINK_TTL_MS = 15 * 60 * 1000;
|
|
10
|
+
/** How long a second-factor challenge can be answered. */
|
|
11
|
+
export const SIGNIN_SECOND_FACTOR_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
/** Wrong second-factor codes one challenge accepts. */
|
|
13
|
+
export const SIGNIN_SECOND_FACTOR_MAX_ATTEMPTS = 5;
|
|
14
|
+
/** Shortest password Oxy accepts when one is set. */
|
|
15
|
+
export const PASSWORD_MIN_LENGTH = 10;
|
|
16
|
+
/** Longest password Oxy accepts (characters). */
|
|
17
|
+
export const PASSWORD_MAX_LENGTH = 256;
|
|
18
|
+
/** Digits in an authenticator code. */
|
|
19
|
+
export const TOTP_DIGITS = 6;
|
|
20
|
+
/** Seconds each authenticator code lasts. */
|
|
21
|
+
export const TOTP_PERIOD_SECONDS = 30;
|
|
22
|
+
/** Backup codes issued when an authenticator is enabled or they are regenerated. */
|
|
23
|
+
export const TOTP_BACKUP_CODE_COUNT = 10;
|
|
24
|
+
/**
|
|
25
|
+
* The long sign-in code: ten characters of Crockford base32 without the
|
|
26
|
+
* look-alikes (no 0, O, 1, I, L, U), shown as `XXXXX-XXXXX`. An account whose
|
|
27
|
+
* sign-in codes were guessed at too often today (the per-account ceiling) is
|
|
28
|
+
* sent this instead of 6 digits for the rest of the day, so its owner can still
|
|
29
|
+
* type a code while guessing one is infeasible.
|
|
30
|
+
*/
|
|
31
|
+
export const EMAIL_SIGNIN_LONG_CODE_ALPHABET = '23456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
32
|
+
export const EMAIL_SIGNIN_LONG_CODE_LENGTH = 10;
|
|
33
|
+
/**
|
|
34
|
+
* Normalise a typed sign-in code: 6 digits stay as they are; a long code is
|
|
35
|
+
* upper-cased with its dash and spaces removed. Anything else is returned
|
|
36
|
+
* trimmed (and will simply be wrong).
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeEmailSignInCode(code) {
|
|
39
|
+
const compact = code.trim().replace(/[\s-]/g, '').toUpperCase();
|
|
40
|
+
return compact;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* How long, once the email is proven, the person has to choose which of its
|
|
44
|
+
* accounts signs in.
|
|
45
|
+
*/
|
|
46
|
+
export const EMAIL_SIGNIN_CHOICE_TTL_MS = 5 * 60 * 1000;
|
|
47
|
+
/** Most accounts one email can hold; sign-up refuses another past it. */
|
|
48
|
+
export const EMAIL_ACCOUNTS_MAX = 20;
|
|
49
|
+
/** Narrow a {@link SignInStepResult}. */
|
|
50
|
+
export function isSecondFactorRequired(result) {
|
|
51
|
+
return result.secondFactorRequired === true;
|
|
52
|
+
}
|
|
53
|
+
/** Narrow an {@link EmailSignInStepResult}. */
|
|
54
|
+
export function isEmailSignInChooseAccount(result) {
|
|
55
|
+
return result.chooseAccount === true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What a re-verification email code confirms. The code is bound to it: a code
|
|
59
|
+
* asked for one change never authorises another, and the email names it.
|
|
60
|
+
*/
|
|
61
|
+
export const REAUTH_ACTIONS = [
|
|
62
|
+
'change_password',
|
|
63
|
+
'totp',
|
|
64
|
+
'link_commons',
|
|
65
|
+
'delete_account',
|
|
66
|
+
'credentials_manage',
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Stable error codes (`error.code` in the API error body). Clients map these
|
|
70
|
+
* through their localization, never the English message.
|
|
71
|
+
*/
|
|
72
|
+
export const SIGN_IN_ERROR_CODES = {
|
|
73
|
+
/** Wrong identifier or password — never says which. */
|
|
74
|
+
invalidCredentials: 'SIGNIN_INVALID_CREDENTIALS',
|
|
75
|
+
/** The request is unknown, expired, spent, or its secret is wrong. */
|
|
76
|
+
requestInvalid: 'SIGNIN_REQUEST_INVALID',
|
|
77
|
+
/** The link is unknown, expired or already used. */
|
|
78
|
+
linkInvalid: 'SIGNIN_LINK_INVALID',
|
|
79
|
+
/** The link was opened in another browser: type the code in the app instead. */
|
|
80
|
+
linkOtherDevice: 'SIGNIN_LINK_OTHER_DEVICE',
|
|
81
|
+
/** The second-factor challenge or its code is wrong, expired or spent. */
|
|
82
|
+
secondFactorInvalid: 'SECOND_FACTOR_INVALID',
|
|
83
|
+
/** Too many failures for this account: wait and try again. */
|
|
84
|
+
locked: 'SIGNIN_LOCKED',
|
|
85
|
+
/** The step needs a fresh proof (`reauth`). */
|
|
86
|
+
reauthRequired: 'REAUTH_REQUIRED',
|
|
87
|
+
/** The fresh proof was wrong. */
|
|
88
|
+
reauthInvalid: 'REAUTH_INVALID',
|
|
89
|
+
/** The account has an authenticator: the proof must carry its code. */
|
|
90
|
+
totpRequired: 'TOTP_REQUIRED',
|
|
91
|
+
/** Enrolling or confirming an authenticator that is already on. */
|
|
92
|
+
totpAlreadyEnabled: 'TOTP_ALREADY_ENABLED',
|
|
93
|
+
/** Disabling, confirming or regenerating codes with no authenticator (or no pending one). */
|
|
94
|
+
totpNotEnabled: 'TOTP_NOT_ENABLED',
|
|
95
|
+
/** The code the authenticator showed at enrolment is wrong. */
|
|
96
|
+
totpCodeInvalid: 'TOTP_CODE_INVALID',
|
|
97
|
+
/** Only official Oxy apps and auth.oxy.so may sign people in here. */
|
|
98
|
+
originNotAllowed: 'SIGNIN_ORIGIN_NOT_ALLOWED',
|
|
99
|
+
/** The username is taken. */
|
|
100
|
+
usernameTaken: 'USERNAME_TAKEN',
|
|
101
|
+
/** The email already holds `EMAIL_ACCOUNTS_MAX` accounts: no sign-up adds another. */
|
|
102
|
+
emailAccountsFull: 'EMAIL_ACCOUNTS_FULL',
|
|
103
|
+
};
|
|
104
|
+
/* -------------------------------------------------------------------------- */
|
|
105
|
+
/* Checks */
|
|
106
|
+
/* -------------------------------------------------------------------------- */
|
|
107
|
+
const expiresAt = number(int, positive);
|
|
108
|
+
const sixDigits = string(trim, pattern(new RegExp(`^\\d{${EMAIL_CODE_LENGTH}}$`), `code must be ${EMAIL_CODE_LENGTH} digits`));
|
|
109
|
+
const passwordInput = string(minLength(1), maxLength(PASSWORD_MAX_LENGTH));
|
|
110
|
+
const secondFactorCode = string(trim, minLength(TOTP_DIGITS), maxLength(16));
|
|
111
|
+
export const emailSignInStartResponseCheck = exact()(object({
|
|
112
|
+
requestId: string(minLength(1), maxLength(64)),
|
|
113
|
+
requestSecret: emailTicketCheck,
|
|
114
|
+
expiresAt,
|
|
115
|
+
retryLater: optional(literal(true)),
|
|
116
|
+
}));
|
|
117
|
+
export const emailSignInLinkResponseCheck = exact()(object({ approved: literal(true) }));
|
|
118
|
+
export const secondFactorRequiredCheck = exact()(object({ secondFactorRequired: literal(true), challengeId: emailTicketCheck, expiresAt }));
|
|
119
|
+
export const emailSignInPendingCheck = exact()(object({ status: literal('pending'), expiresAt }));
|
|
120
|
+
const emailSignInAccountOption = exact()(object({
|
|
121
|
+
id: string(minLength(1), maxLength(64)),
|
|
122
|
+
username: optional(string(minLength(1))),
|
|
123
|
+
displayName: optional(string(minLength(1))),
|
|
124
|
+
avatar: optional(string(minLength(1))),
|
|
125
|
+
}));
|
|
126
|
+
export const emailSignInChooseAccountCheck = exact()(object({
|
|
127
|
+
chooseAccount: literal(true),
|
|
128
|
+
accounts: array(emailSignInAccountOption, minItems(2), maxItems(EMAIL_ACCOUNTS_MAX)),
|
|
129
|
+
expiresAt,
|
|
130
|
+
}));
|
|
131
|
+
export const signInMethodsCheck = exact()(object({
|
|
132
|
+
hasEmail: boolean,
|
|
133
|
+
hasPassword: boolean,
|
|
134
|
+
totpEnabled: boolean,
|
|
135
|
+
backupCodesRemaining: number(int, nonnegative),
|
|
136
|
+
}));
|
|
137
|
+
export const totpEnrollResponseCheck = exact()(object({
|
|
138
|
+
secret: string(pattern(/^[A-Z2-7]+$/)),
|
|
139
|
+
otpauthUri: string(startsWith('otpauth://totp/')),
|
|
140
|
+
}));
|
|
141
|
+
export const totpBackupCodesResponseCheck = exact()(object({
|
|
142
|
+
backupCodes: array(string(pattern(/^[a-z2-9]{5}-[a-z2-9]{5}$/)), exactItems(TOTP_BACKUP_CODE_COUNT)),
|
|
143
|
+
}));
|
|
144
|
+
export const reauthProofCheck = exact()(refine(strictObject({
|
|
145
|
+
password: optional(passwordInput),
|
|
146
|
+
emailCode: optional(strictObject({
|
|
147
|
+
verificationId: string(trim, minLength(1), maxLength(64)),
|
|
148
|
+
code: sixDigits,
|
|
149
|
+
})),
|
|
150
|
+
totpCode: optional(secondFactorCode),
|
|
151
|
+
}), (proof) => proof.password !== undefined || proof.emailCode !== undefined, 'password or emailCode is required'));
|