@oxy.so/contracts 2.1.0 → 3.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 +79 -0
- package/dist/cjs/deviceBoot.js +2 -2
- package/dist/cjs/deviceSession.js +73 -16
- package/dist/cjs/identity.js +1 -3
- package/dist/cjs/identityLink.js +92 -0
- package/dist/cjs/identityProof.js +6 -29
- package/dist/cjs/index.js +82 -65
- 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 +76 -0
- package/dist/esm/deviceBoot.js +2 -2
- package/dist/esm/deviceSession.js +72 -15
- package/dist/esm/identity.js +1 -3
- package/dist/esm/identityLink.js +87 -0
- package/dist/esm/identityProof.js +6 -29
- package/dist/esm/index.js +13 -16
- 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 +90 -0
- package/dist/types/agency.d.ts +2 -2
- package/dist/types/deviceBoot.d.ts +2 -2
- package/dist/types/deviceSession.d.ts +131 -19
- package/dist/types/externalIdentity.d.ts +4 -4
- package/dist/types/identity.d.ts +4 -8
- package/dist/types/identityLink.d.ts +146 -0
- package/dist/types/identityProof.d.ts +15 -42
- package/dist/types/index.d.ts +10 -12
- package/dist/types/inference/accountBilling.d.ts +2 -2
- package/dist/types/inference/embeddings.d.ts +16 -16
- package/dist/types/inference/entitlement.d.ts +2 -2
- package/dist/types/inference/errors.d.ts +8 -8
- package/dist/types/inference/inbox.d.ts +8 -8
- package/dist/types/inference/providerConnection.d.ts +20 -20
- package/dist/types/inference/request.d.ts +16 -16
- package/dist/types/inference/streamEvents.d.ts +20 -20
- package/dist/types/keyRecovery.d.ts +4 -4
- package/dist/types/oauth.d.ts +16 -16
- package/dist/types/reputation.d.ts +32 -127
- package/dist/types/sessionStatus.d.ts +2 -2
- package/dist/types/signIn.d.ts +717 -0
- package/dist/types/userResponse.d.ts +2 -2
- package/package.json +1 -1
- package/dist/cjs/identityMove.js +0 -156
- package/dist/cjs/identityRecovery.js +0 -51
- package/dist/cjs/webIdentityCarrier.js +0 -181
- package/dist/cjs/webauthn.js +0 -85
- package/dist/esm/identityMove.js +0 -148
- package/dist/esm/identityRecovery.js +0 -48
- package/dist/esm/webIdentityCarrier.js +0 -178
- package/dist/esm/webauthn.js +0 -82
- package/dist/types/identityMove.d.ts +0 -185
- package/dist/types/identityRecovery.d.ts +0 -246
- package/dist/types/webIdentityCarrier.d.ts +0 -1130
- package/dist/types/webauthn.d.ts +0 -285
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_VERIFICATION_ERROR_CODES = exports.emailVerificationConfirmResponseSchema = exports.emailVerificationConfirmRequestSchema = exports.emailVerificationStartResponseSchema = exports.emailVerificationStartRequestSchema = exports.emailTicketSchema = exports.emailAddressSchema = exports.EMAIL_TICKET_TTL_MS = exports.EMAIL_CODE_MAX_ATTEMPTS = exports.EMAIL_CODE_TTL_MS = exports.EMAIL_CODE_LENGTH = exports.EMAIL_VERIFICATION_PURPOSES = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Email codes and tickets (ADR 0030).
|
|
6
|
+
*
|
|
7
|
+
* A web account is a username and an email; a password and an authenticator
|
|
8
|
+
* app are optional and added later. The email is proven by a 6-digit code sent
|
|
9
|
+
* to it, and the proof is a short-lived one-use ticket the next step spends:
|
|
10
|
+
*
|
|
11
|
+
* - `signup`: the ticket lets `POST /auth/signup` create the account with that
|
|
12
|
+
* email. `start` answers the same whether or not the address already has an
|
|
13
|
+
* account, so it never tells anyone which emails have an Oxy account.
|
|
14
|
+
*
|
|
15
|
+
* Signing in by email (`signin`) and re-verifying before a sensitive change
|
|
16
|
+
* (`reauth`) use the same code, through their own routes (`signIn.ts`).
|
|
17
|
+
*/
|
|
18
|
+
const zod_1 = require("zod");
|
|
19
|
+
/**
|
|
20
|
+
* - `signup`: above.
|
|
21
|
+
* - `signin`: the code (and link) of an email sign-in (`POST /auth/signin/email/start`).
|
|
22
|
+
* - `reauth`: a signed-in person proving it is them before a sensitive step
|
|
23
|
+
* (`POST /users/me/reauth/email`): a password, an authenticator, deleting the
|
|
24
|
+
* account, linking Commons.
|
|
25
|
+
*/
|
|
26
|
+
exports.EMAIL_VERIFICATION_PURPOSES = ['signup', 'signin', 'reauth'];
|
|
27
|
+
/** Digits in a code. */
|
|
28
|
+
exports.EMAIL_CODE_LENGTH = 6;
|
|
29
|
+
/** How long a code can be confirmed. */
|
|
30
|
+
exports.EMAIL_CODE_TTL_MS = 10 * 60 * 1000;
|
|
31
|
+
/** Wrong codes before a verification is spent and a new code is needed. */
|
|
32
|
+
exports.EMAIL_CODE_MAX_ATTEMPTS = 5;
|
|
33
|
+
/** How long a confirmed code's ticket can be spent. */
|
|
34
|
+
exports.EMAIL_TICKET_TTL_MS = 15 * 60 * 1000;
|
|
35
|
+
/** An email address as the API stores it: trimmed and lowercase. */
|
|
36
|
+
exports.emailAddressSchema = zod_1.z.string().trim().toLowerCase().min(3).max(254).email();
|
|
37
|
+
/** An opaque one-use ticket (32 random bytes, base64url). */
|
|
38
|
+
exports.emailTicketSchema = zod_1.z
|
|
39
|
+
.string()
|
|
40
|
+
.trim()
|
|
41
|
+
.regex(/^[A-Za-z0-9_-]{43}$/, 'ticket must be 32 bytes of base64url');
|
|
42
|
+
/** `POST /auth/email/verify/start` */
|
|
43
|
+
exports.emailVerificationStartRequestSchema = zod_1.z
|
|
44
|
+
.object({ purpose: zod_1.z.literal('signup'), email: exports.emailAddressSchema })
|
|
45
|
+
.strict();
|
|
46
|
+
exports.emailVerificationStartResponseSchema = zod_1.z.object({
|
|
47
|
+
verificationId: zod_1.z.string().min(1).max(64),
|
|
48
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
49
|
+
});
|
|
50
|
+
/** `POST /auth/email/verify/confirm` */
|
|
51
|
+
exports.emailVerificationConfirmRequestSchema = zod_1.z
|
|
52
|
+
.object({
|
|
53
|
+
verificationId: zod_1.z.string().trim().min(1).max(64),
|
|
54
|
+
code: zod_1.z
|
|
55
|
+
.string()
|
|
56
|
+
.trim()
|
|
57
|
+
.regex(new RegExp(`^\\d{${exports.EMAIL_CODE_LENGTH}}$`), `code must be ${exports.EMAIL_CODE_LENGTH} digits`),
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
exports.emailVerificationConfirmResponseSchema = zod_1.z.object({
|
|
61
|
+
ticket: exports.emailTicketSchema,
|
|
62
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Stable error codes (`error.code` in the API error body). Clients map these
|
|
66
|
+
* through their localization, never the English message.
|
|
67
|
+
*/
|
|
68
|
+
exports.EMAIL_VERIFICATION_ERROR_CODES = {
|
|
69
|
+
/** The code is wrong, or its verification expired or was spent. */
|
|
70
|
+
codeInvalid: 'EMAIL_CODE_INVALID',
|
|
71
|
+
/** Too many wrong codes: request a new one. */
|
|
72
|
+
tooManyAttempts: 'EMAIL_CODE_TOO_MANY_ATTEMPTS',
|
|
73
|
+
/** The ticket is unknown, expired, spent, or for another email or purpose. */
|
|
74
|
+
ticketInvalid: 'EMAIL_TICKET_INVALID',
|
|
75
|
+
/** A sign-up without a confirmed email. */
|
|
76
|
+
ticketRequired: 'EMAIL_TICKET_REQUIRED',
|
|
77
|
+
/** This server cannot send mail. */
|
|
78
|
+
unavailable: 'EMAIL_UNAVAILABLE',
|
|
79
|
+
};
|
package/dist/cjs/deviceBoot.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* SINGLE SOURCE OF TRUTH for the first-party login result (the session arm). The
|
|
6
6
|
* API validates its OUTPUT against this schema; every consumer (`@oxy.so/core`'s
|
|
7
7
|
* auth mixin) validates its INPUT against the same definition, so producer and
|
|
8
|
-
* consumers cannot drift.
|
|
9
|
-
*
|
|
8
|
+
* consumers cannot drift. This is the session arm every sign-in
|
|
9
|
+
* ends in (email code or link, password, authenticator, Commons handoff).
|
|
10
10
|
*
|
|
11
11
|
* The device transport is `deviceId` + `deviceSecret` + `POST /session/device/token`
|
|
12
12
|
* (see `deviceSession.ts`). The legacy cookie/bootstrap/refresh-family lanes were
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.deviceBackgroundTokenResponseSchema = exports.deviceBackgroundTokenRequestSchema = exports.deviceBackgroundCredentialResponseSchema = exports.sessionAccountsChangedEventSchema = exports.sessionAccountsChangedReasonSchema = exports.SESSION_ACCOUNTS_CHANGED_EVENT = exports.deviceTokenMintResponseSchema = exports.deviceTokenMintRequestSchema = exports.deviceSessionSyncSchema = exports.activeTokenSchema = exports.deviceSessionStateSchema = exports.sessionAccountSchema = void 0;
|
|
3
|
+
exports.deviceJoinResponseSchema = exports.deviceJoinRequestSchema = exports.deviceJoinCodeResponseSchema = exports.deviceJoinCodeRequestSchema = exports.deviceRegisterResponseSchema = exports.deviceProofSchema = exports.deviceBackgroundTokenResponseSchema = exports.deviceBackgroundTokenRequestSchema = exports.deviceBackgroundCredentialResponseSchema = exports.sessionAccountsChangedEventSchema = exports.sessionAccountsChangedReasonSchema = exports.SESSION_ACCOUNTS_CHANGED_EVENT = exports.deviceTokenMintResponseSchema = exports.deviceTokenMintRequestSchema = exports.deviceSessionSyncSchema = exports.activeTokenSchema = exports.deviceSessionStateSchema = exports.sessionAccountSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
exports.sessionAccountSchema = zod_1.z.object({
|
|
6
6
|
accountId: zod_1.z.string(),
|
|
@@ -29,9 +29,10 @@ exports.deviceSessionSyncSchema = zod_1.z.object({
|
|
|
29
29
|
/**
|
|
30
30
|
* Request body for `POST /session/device/token` — the client presents the
|
|
31
31
|
* `deviceId` it stored first-party plus the opaque `deviceSecret`. NO bearer:
|
|
32
|
-
* possession of the secret IS the proof of device ownership. The server
|
|
33
|
-
* `sha256(deviceSecret)`
|
|
34
|
-
*
|
|
32
|
+
* possession of the secret IS the proof of device ownership. The server looks
|
|
33
|
+
* `sha256(deviceSecret)` up among the device's holder credentials (one per app
|
|
34
|
+
* or origin that joined the shared DeviceSession) and mints a short access
|
|
35
|
+
* token for the device's active account.
|
|
35
36
|
*
|
|
36
37
|
* `accountId` pins the mint to ONE account of that device instead of whichever
|
|
37
38
|
* account is currently active. It exists for identity-bound clients (Commons),
|
|
@@ -51,8 +52,8 @@ exports.deviceTokenMintRequestSchema = zod_1.z.object({
|
|
|
51
52
|
* short access token for the active account, its expiry, the device secret the
|
|
52
53
|
* client must persist (`nextDeviceSecret` — on mint this echoes the presented
|
|
53
54
|
* secret unchanged so concurrent refreshes from multiple origins do not race),
|
|
54
|
-
* and the projected device-session state.
|
|
55
|
-
*
|
|
55
|
+
* and the projected device-session state. Nothing rotates: each sign-in issues
|
|
56
|
+
* a NEW holder credential and leaves the others valid.
|
|
56
57
|
*/
|
|
57
58
|
exports.deviceTokenMintResponseSchema = zod_1.z.object({
|
|
58
59
|
accessToken: zod_1.z.string(),
|
|
@@ -110,12 +111,11 @@ exports.sessionAccountsChangedEventSchema = zod_1.z.object({
|
|
|
110
111
|
* derived server-side from it) and consumed afterwards only by native
|
|
111
112
|
* background code, which has no JS runtime to mint a token for itself.
|
|
112
113
|
*
|
|
113
|
-
* Deliberately a SEPARATE credential from the
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* anything JS reads.
|
|
114
|
+
* Deliberately a SEPARATE credential from the holder `deviceSecret`: that one
|
|
115
|
+
* is device-wide and mints for whichever account is active, while this one is
|
|
116
|
+
* bound to ONE account and expires, so a widget worker never holds a
|
|
117
|
+
* credential that reaches every account on the device. Background code is its
|
|
118
|
+
* sole writer and never touches anything JS reads.
|
|
119
119
|
*
|
|
120
120
|
* The raw `secret` is returned exactly once, at provision time — never stored
|
|
121
121
|
* retrievably, never logged, never re-read. A caller that loses it provisions
|
|
@@ -138,10 +138,10 @@ exports.deviceBackgroundCredentialResponseSchema = zod_1.z.object({
|
|
|
138
138
|
* native background code with NO bearer and NO cookies: possession of the
|
|
139
139
|
* background `secret` IS the proof, as it is for the device-secret mint.
|
|
140
140
|
*
|
|
141
|
-
*
|
|
142
|
-
* `next…` field
|
|
143
|
-
*
|
|
144
|
-
*
|
|
141
|
+
* Like that mint this one NEVER rotates the presented secret, and it carries
|
|
142
|
+
* no `next…` field at all, so background code interrupted anywhere between
|
|
143
|
+
* request and response leaves the credential intact and usable on its next
|
|
144
|
+
* run.
|
|
145
145
|
*/
|
|
146
146
|
exports.deviceBackgroundTokenRequestSchema = zod_1.z.object({
|
|
147
147
|
deviceId: zod_1.z.string().min(1),
|
|
@@ -162,3 +162,60 @@ exports.deviceBackgroundTokenResponseSchema = zod_1.z.object({
|
|
|
162
162
|
expiresAt: zod_1.z.string(),
|
|
163
163
|
accountId: zod_1.z.string().min(1),
|
|
164
164
|
});
|
|
165
|
+
/* -------------------------------------------------------------------------- */
|
|
166
|
+
/* The browser bridge — joining the browser's DeviceSession (ADR 0029 D2) */
|
|
167
|
+
/* -------------------------------------------------------------------------- */
|
|
168
|
+
/**
|
|
169
|
+
* Proof that the caller holds a device: the `deviceId` it stored first-party and
|
|
170
|
+
* one of that device's holder secrets. Sent with `POST /session/device/join-code`
|
|
171
|
+
* and, optionally, with a sign-in (`POST /auth/session/claim`, the email,
|
|
172
|
+
* password and second-factor sign-ins, `POST /auth/signup`), where
|
|
173
|
+
* a valid proof puts the new session on THAT device so every app holding it sees
|
|
174
|
+
* the account. An invalid proof on a sign-in is ignored, never an error.
|
|
175
|
+
*/
|
|
176
|
+
exports.deviceProofSchema = zod_1.z.object({
|
|
177
|
+
deviceId: zod_1.z.string().min(1).max(128),
|
|
178
|
+
deviceSecret: zod_1.z.string().min(1).max(256),
|
|
179
|
+
});
|
|
180
|
+
/**
|
|
181
|
+
* `POST /session/device/register` — auth.oxy.so only. No body. A new, empty
|
|
182
|
+
* DeviceSession with a server-chosen `deviceId` and ONE holder credential for
|
|
183
|
+
* auth.oxy.so. The raw secret is returned exactly once.
|
|
184
|
+
*/
|
|
185
|
+
exports.deviceRegisterResponseSchema = zod_1.z.object({
|
|
186
|
+
deviceId: zod_1.z.string().min(1),
|
|
187
|
+
deviceSecret: zod_1.z.string().min(1),
|
|
188
|
+
});
|
|
189
|
+
/** PKCE S256 challenge: base64url of a SHA-256 digest (43 characters). */
|
|
190
|
+
const pkceS256ChallengeSchema = zod_1.z.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
191
|
+
/**
|
|
192
|
+
* `POST /session/device/join-code` — auth.oxy.so only (the bridge window). Proves
|
|
193
|
+
* auth.oxy.so's device and asks for a one-use code an official app redeems to
|
|
194
|
+
* join it. The code is bound to the application (`clientId`), its exact
|
|
195
|
+
* registered `redirectUri` and the app's PKCE challenge, and lives about a
|
|
196
|
+
* minute.
|
|
197
|
+
*/
|
|
198
|
+
exports.deviceJoinCodeRequestSchema = exports.deviceProofSchema.extend({
|
|
199
|
+
clientId: zod_1.z.string().min(1).max(256),
|
|
200
|
+
redirectUri: zod_1.z.string().url().max(2048),
|
|
201
|
+
codeChallenge: pkceS256ChallengeSchema,
|
|
202
|
+
codeChallengeMethod: zod_1.z.literal('S256'),
|
|
203
|
+
});
|
|
204
|
+
exports.deviceJoinCodeResponseSchema = zod_1.z.object({
|
|
205
|
+
code: zod_1.z.string().min(1),
|
|
206
|
+
/** Seconds until the code expires. */
|
|
207
|
+
expiresIn: zod_1.z.number().int().positive(),
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* `POST /session/device/join` — called by the app's own origin with the code the
|
|
211
|
+
* bridge window posted to it and the PKCE verifier only the app holds. Returns a
|
|
212
|
+
* NEW holder credential for the browser's device; the app then mints through the
|
|
213
|
+
* ordinary `POST /session/device/token`.
|
|
214
|
+
*/
|
|
215
|
+
exports.deviceJoinRequestSchema = zod_1.z.object({
|
|
216
|
+
code: zod_1.z.string().min(1).max(256),
|
|
217
|
+
codeVerifier: zod_1.z.string().min(43).max(128).regex(/^[A-Za-z0-9._~-]+$/),
|
|
218
|
+
clientId: zod_1.z.string().min(1).max(256),
|
|
219
|
+
redirectUri: zod_1.z.string().url().max(2048),
|
|
220
|
+
});
|
|
221
|
+
exports.deviceJoinResponseSchema = exports.deviceRegisterResponseSchema;
|
package/dist/cjs/identity.js
CHANGED
|
@@ -210,11 +210,9 @@ exports.domainVerificationInstructionsSchema = zod_1.z.object({
|
|
|
210
210
|
}),
|
|
211
211
|
});
|
|
212
212
|
exports.authMethodEntrySchema = zod_1.z.object({
|
|
213
|
-
type: zod_1.z.
|
|
213
|
+
type: zod_1.z.literal('identity'),
|
|
214
214
|
linkedAt: zod_1.z.union([zod_1.z.string(), zod_1.z.date()]),
|
|
215
215
|
verificationMethodId: zod_1.z.string().optional(),
|
|
216
|
-
credentialId: zod_1.z.string().optional(),
|
|
217
|
-
name: zod_1.z.string().optional(),
|
|
218
216
|
});
|
|
219
217
|
exports.authMethodsResponseSchema = zod_1.z.object({
|
|
220
218
|
did: zod_1.z.string(),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.identityLinkCompleteRequestSchema = exports.identityLinkProofRequestSchema = exports.identityLinkStateSchema = exports.identityLinkCreateResponseSchema = exports.identityLinkIdSchema = exports.IDENTITY_LINK_QR_PREFIX = exports.IDENTITY_LINK_STATUSES = void 0;
|
|
4
|
+
exports.buildIdentityLinkQrPayload = buildIdentityLinkQrPayload;
|
|
5
|
+
exports.parseIdentityLinkQrPayload = parseIdentityLinkQrPayload;
|
|
6
|
+
/**
|
|
7
|
+
* Linking Commons to an account without a key (ADR 0029 D3) — the two-device
|
|
8
|
+
* relay.
|
|
9
|
+
*
|
|
10
|
+
* An account without a key links a Commons root once, and becomes
|
|
11
|
+
* self-custodied: its email is deleted and its phrase in Commons is how it gets
|
|
12
|
+
* back in. The authority is the same as `POST /auth/link` (ADR 0024 D8): a root
|
|
13
|
+
* proof (`link_identity`) by the key Commons holds over a one-use challenge,
|
|
14
|
+
* and the account's own confirmation — a code just sent to its email (plus its
|
|
15
|
+
* authenticator code when it has one). Only the transport is new, because the
|
|
16
|
+
* two factors live on two devices:
|
|
17
|
+
*
|
|
18
|
+
* 1. The signed-in account (the "Link Commons" panel of `@oxy.so/services`)
|
|
19
|
+
* opens a link request → `{ linkId, challenge }`, shown as a QR
|
|
20
|
+
* (`oxycommons://link?id=…&c=…`).
|
|
21
|
+
* 2. Commons scans it, reads the request (the account's id and username),
|
|
22
|
+
* signs the root proof over the challenge and posts it with its key.
|
|
23
|
+
* 3. Both screens show the same 6-digit code, derived from the link id and
|
|
24
|
+
* that key (`deriveIdentityLinkCode` in `@oxy.so/core`); the person checks
|
|
25
|
+
* they match, so a photographed QR cannot slip another key in.
|
|
26
|
+
* 4. The panel completes with the email code: the account gains the root,
|
|
27
|
+
* loses the email, and Commons signs in with it.
|
|
28
|
+
*
|
|
29
|
+
* The server stores only the challenge's hash; the challenge travels in the QR.
|
|
30
|
+
*/
|
|
31
|
+
const zod_1 = require("zod");
|
|
32
|
+
const identityProof_1 = require("./identityProof");
|
|
33
|
+
const signIn_1 = require("./signIn");
|
|
34
|
+
exports.IDENTITY_LINK_STATUSES = ['pending', 'signed', 'completed', 'cancelled'];
|
|
35
|
+
/** The scheme and host Commons routes a link QR to. */
|
|
36
|
+
exports.IDENTITY_LINK_QR_PREFIX = 'oxycommons://link';
|
|
37
|
+
const LINK_ID = /^[0-9a-f]{32}$/;
|
|
38
|
+
const CHALLENGE = /^[0-9a-f]{64}$/;
|
|
39
|
+
exports.identityLinkIdSchema = zod_1.z.string().trim().regex(LINK_ID, 'linkId must be 32 lowercase hex characters');
|
|
40
|
+
/** The QR auth.oxy.so shows: the request's id and the challenge Commons signs. */
|
|
41
|
+
function buildIdentityLinkQrPayload(linkId, challenge) {
|
|
42
|
+
return `${exports.IDENTITY_LINK_QR_PREFIX}?id=${linkId}&c=${challenge}`;
|
|
43
|
+
}
|
|
44
|
+
/** The request a scanned code names, or `null` for anything that is not a link QR. */
|
|
45
|
+
function parseIdentityLinkQrPayload(raw) {
|
|
46
|
+
const value = raw.trim();
|
|
47
|
+
if (!value.startsWith(`${exports.IDENTITY_LINK_QR_PREFIX}?`))
|
|
48
|
+
return null;
|
|
49
|
+
// Parsed by hand: React Native's `URLSearchParams` does not implement `get`.
|
|
50
|
+
const params = new Map();
|
|
51
|
+
for (const pair of value.slice(exports.IDENTITY_LINK_QR_PREFIX.length + 1).split('&')) {
|
|
52
|
+
const separator = pair.indexOf('=');
|
|
53
|
+
if (separator > 0)
|
|
54
|
+
params.set(pair.slice(0, separator), pair.slice(separator + 1));
|
|
55
|
+
}
|
|
56
|
+
const linkId = params.get('id') ?? '';
|
|
57
|
+
const challenge = params.get('c') ?? '';
|
|
58
|
+
if (!LINK_ID.test(linkId) || !CHALLENGE.test(challenge))
|
|
59
|
+
return null;
|
|
60
|
+
return { linkId, challenge };
|
|
61
|
+
}
|
|
62
|
+
exports.identityLinkCreateResponseSchema = zod_1.z.object({
|
|
63
|
+
linkId: exports.identityLinkIdSchema,
|
|
64
|
+
challenge: zod_1.z.string().regex(CHALLENGE),
|
|
65
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
66
|
+
qrPayload: zod_1.z.string().startsWith(exports.IDENTITY_LINK_QR_PREFIX),
|
|
67
|
+
});
|
|
68
|
+
exports.identityLinkStateSchema = zod_1.z.object({
|
|
69
|
+
status: zod_1.z.enum(exports.IDENTITY_LINK_STATUSES),
|
|
70
|
+
userId: zod_1.z.string().min(1),
|
|
71
|
+
username: zod_1.z.string().nullable(),
|
|
72
|
+
publicKey: zod_1.z.string().nullable(),
|
|
73
|
+
audience: zod_1.z.string().min(1),
|
|
74
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
75
|
+
});
|
|
76
|
+
/** `POST /identity/link/:linkId/proof` — from Commons, no bearer. */
|
|
77
|
+
exports.identityLinkProofRequestSchema = zod_1.z
|
|
78
|
+
.object({
|
|
79
|
+
publicKey: zod_1.z
|
|
80
|
+
.string()
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.regex(/^04[0-9a-f]{128}$/, 'publicKey must be an uncompressed secp256k1 key'),
|
|
84
|
+
proof: identityProof_1.identityProofSchema,
|
|
85
|
+
})
|
|
86
|
+
.strict();
|
|
87
|
+
/**
|
|
88
|
+
* `POST /identity/link/:linkId/complete` — the account's own confirmation: a
|
|
89
|
+
* code just sent to its email for this link (`reauth`, plus its authenticator
|
|
90
|
+
* code when it has one).
|
|
91
|
+
*/
|
|
92
|
+
exports.identityLinkCompleteRequestSchema = zod_1.z.object({ reauth: signIn_1.emailReauthProofSchema }).strict();
|
|
@@ -32,26 +32,12 @@ exports.IDENTITY_PROOF_CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
|
32
32
|
* action and spent only by a proof for that action.
|
|
33
33
|
*/
|
|
34
34
|
exports.IDENTITY_PROOF_ACTIONS = {
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
phraseConfirmed: 'web_envelope_phrase_confirmed',
|
|
41
|
-
/** Record that the recovery material re-derived the root. */
|
|
42
|
-
recoveryVerified: 'web_envelope_recovery_verified',
|
|
43
|
-
/** Remove the web holder. */
|
|
44
|
-
delete: 'web_envelope_delete',
|
|
45
|
-
/** Link a keyless account's first root without a web envelope (`POST /auth/link`). */
|
|
35
|
+
/**
|
|
36
|
+
* Link Commons' root to an account that has none (`POST /auth/link`).
|
|
37
|
+
* The account becomes self-custodied and its email is deleted
|
|
38
|
+
* (ADR 0029 D3).
|
|
39
|
+
*/
|
|
46
40
|
link: 'link_identity',
|
|
47
|
-
/** Create a personal account together with its root (passkey sign-up). */
|
|
48
|
-
enroll: 'enroll_identity',
|
|
49
|
-
/** Prove the root to start signed-out recovery. */
|
|
50
|
-
recoverStart: 'recover_account_start',
|
|
51
|
-
/** Bind the new passkey and envelope when completing signed-out recovery. */
|
|
52
|
-
recoverComplete: 'recover_account_complete',
|
|
53
|
-
/** Seal the root for the Commons device that joined a move (payload: move id + sealed bytes). */
|
|
54
|
-
moveSeal: 'identity_move_seal',
|
|
55
41
|
};
|
|
56
42
|
exports.IDENTITY_PROOF_ACTION_VALUES = Object.values(exports.IDENTITY_PROOF_ACTIONS);
|
|
57
43
|
const HEX_DIGEST = /^[0-9a-f]{64}$/;
|
|
@@ -144,21 +130,12 @@ exports.identityProofChallengeResponseSchema = zod_1.z.object({
|
|
|
144
130
|
*/
|
|
145
131
|
exports.IDENTITY_ERROR_CODES = {
|
|
146
132
|
proofInvalid: 'IDENTITY_PROOF_INVALID',
|
|
147
|
-
revisionConflict: 'IDENTITY_ENVELOPE_REVISION_CONFLICT',
|
|
148
133
|
rootAlreadyLinked: 'IDENTITY_ROOT_ALREADY_LINKED',
|
|
149
134
|
rootLinkedElsewhere: 'IDENTITY_ROOT_LINKED_ELSEWHERE',
|
|
150
|
-
noRoot: 'IDENTITY_NO_ROOT',
|
|
151
135
|
freshFactorRequired: 'IDENTITY_FRESH_FACTOR_REQUIRED',
|
|
152
|
-
lastWebHolder: 'IDENTITY_LAST_WEB_HOLDER',
|
|
153
|
-
enrollmentRequired: 'IDENTITY_ENROLLMENT_REQUIRED',
|
|
154
|
-
enrollmentInvalid: 'IDENTITY_ENROLLMENT_INVALID',
|
|
155
136
|
notPersonal: 'IDENTITY_NOT_PERSONAL_ACCOUNT',
|
|
156
|
-
recoveryFailed: 'IDENTITY_RECOVERY_FAILED',
|
|
157
137
|
};
|
|
158
138
|
exports.identityRootStatusSchema = zod_1.z.object({
|
|
159
139
|
rootLinked: zod_1.z.boolean(),
|
|
160
|
-
|
|
161
|
-
hasPhrase: zod_1.z.boolean().nullable(),
|
|
162
|
-
phraseConfirmedAt: zod_1.z.string().datetime().nullable(),
|
|
163
|
-
recoveryVerifiedAt: zod_1.z.string().datetime().nullable(),
|
|
140
|
+
recoveryEmail: zod_1.z.string().nullable(),
|
|
164
141
|
});
|