@oxy.so/contracts 1.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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/accountGraph.js +489 -0
- package/dist/cjs/agency.js +439 -0
- package/dist/cjs/browserHub.js +215 -0
- package/dist/cjs/civic.js +163 -0
- package/dist/cjs/commonsSignIn.js +59 -0
- package/dist/cjs/deviceBoot.js +50 -0
- package/dist/cjs/deviceDirectory.js +189 -0
- package/dist/cjs/devicePairing.js +138 -0
- package/dist/cjs/deviceSession.js +164 -0
- package/dist/cjs/emailAgentContext.js +32 -0
- package/dist/cjs/followGraph.js +28 -0
- package/dist/cjs/identity.js +258 -0
- package/dist/cjs/inboxPush.js +24 -0
- package/dist/cjs/index.js +618 -0
- package/dist/cjs/inference/accountBilling.js +334 -0
- package/dist/cjs/inference/aliaModelRelease.js +262 -0
- package/dist/cjs/inference/attribution.js +106 -0
- package/dist/cjs/inference/catalogue.js +487 -0
- package/dist/cjs/inference/entitlement.js +217 -0
- package/dist/cjs/inference/errors.js +309 -0
- package/dist/cjs/inference/identifiers.js +224 -0
- package/dist/cjs/inference/inbox.js +105 -0
- package/dist/cjs/inference/modelDocumentation.js +433 -0
- package/dist/cjs/inference/money.js +188 -0
- package/dist/cjs/inference/priceVersion.js +110 -0
- package/dist/cjs/inference/providerConnection.js +455 -0
- package/dist/cjs/inference/request.js +477 -0
- package/dist/cjs/inference/routingPolicy.js +318 -0
- package/dist/cjs/inference/streamEvents.js +258 -0
- package/dist/cjs/inference/usage.js +329 -0
- package/dist/cjs/inference/version.js +105 -0
- package/dist/cjs/keyRecovery.js +91 -0
- package/dist/cjs/keyRotation.js +75 -0
- package/dist/cjs/links.js +68 -0
- package/dist/cjs/moderationReputation.js +298 -0
- package/dist/cjs/oauth.js +66 -0
- package/dist/cjs/oxyRecordTypes.js +71 -0
- package/dist/cjs/protocol.js +53 -0
- package/dist/cjs/recommendations.js +168 -0
- package/dist/cjs/reputation.js +297 -0
- package/dist/cjs/sessionStatus.js +121 -0
- package/dist/cjs/transparency.js +89 -0
- package/dist/cjs/updates.js +252 -0
- package/dist/cjs/userInvalidation.js +89 -0
- package/dist/cjs/userResponse.js +245 -0
- package/dist/cjs/username.js +290 -0
- package/dist/cjs/webauthn.js +71 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/accountGraph.js +480 -0
- package/dist/esm/agency.js +436 -0
- package/dist/esm/browserHub.js +212 -0
- package/dist/esm/civic.js +160 -0
- package/dist/esm/commonsSignIn.js +56 -0
- package/dist/esm/deviceBoot.js +47 -0
- package/dist/esm/deviceDirectory.js +186 -0
- package/dist/esm/devicePairing.js +135 -0
- package/dist/esm/deviceSession.js +161 -0
- package/dist/esm/emailAgentContext.js +29 -0
- package/dist/esm/followGraph.js +27 -0
- package/dist/esm/identity.js +255 -0
- package/dist/esm/inboxPush.js +21 -0
- package/dist/esm/index.js +172 -0
- package/dist/esm/inference/accountBilling.js +331 -0
- package/dist/esm/inference/aliaModelRelease.js +259 -0
- package/dist/esm/inference/attribution.js +103 -0
- package/dist/esm/inference/catalogue.js +484 -0
- package/dist/esm/inference/entitlement.js +214 -0
- package/dist/esm/inference/errors.js +306 -0
- package/dist/esm/inference/identifiers.js +221 -0
- package/dist/esm/inference/inbox.js +102 -0
- package/dist/esm/inference/modelDocumentation.js +430 -0
- package/dist/esm/inference/money.js +185 -0
- package/dist/esm/inference/priceVersion.js +107 -0
- package/dist/esm/inference/providerConnection.js +452 -0
- package/dist/esm/inference/request.js +474 -0
- package/dist/esm/inference/routingPolicy.js +315 -0
- package/dist/esm/inference/streamEvents.js +255 -0
- package/dist/esm/inference/usage.js +326 -0
- package/dist/esm/inference/version.js +102 -0
- package/dist/esm/keyRecovery.js +88 -0
- package/dist/esm/keyRotation.js +72 -0
- package/dist/esm/links.js +65 -0
- package/dist/esm/moderationReputation.js +295 -0
- package/dist/esm/oauth.js +63 -0
- package/dist/esm/oxyRecordTypes.js +68 -0
- package/dist/esm/protocol.js +50 -0
- package/dist/esm/recommendations.js +165 -0
- package/dist/esm/reputation.js +293 -0
- package/dist/esm/sessionStatus.js +118 -0
- package/dist/esm/transparency.js +86 -0
- package/dist/esm/updates.js +249 -0
- package/dist/esm/userInvalidation.js +85 -0
- package/dist/esm/userResponse.js +240 -0
- package/dist/esm/username.js +283 -0
- package/dist/esm/webauthn.js +68 -0
- package/dist/types/.tsbuildinfo +1 -0
- package/dist/types/accountGraph.d.ts +378 -0
- package/dist/types/agency.d.ts +2162 -0
- package/dist/types/browserHub.d.ts +856 -0
- package/dist/types/civic.d.ts +338 -0
- package/dist/types/commonsSignIn.d.ts +58 -0
- package/dist/types/deviceBoot.d.ts +74 -0
- package/dist/types/deviceDirectory.d.ts +1317 -0
- package/dist/types/devicePairing.d.ts +130 -0
- package/dist/types/deviceSession.d.ts +411 -0
- package/dist/types/emailAgentContext.d.ts +248 -0
- package/dist/types/followGraph.d.ts +150 -0
- package/dist/types/identity.d.ts +402 -0
- package/dist/types/inboxPush.d.ts +30 -0
- package/dist/types/index.d.ts +100 -0
- package/dist/types/inference/accountBilling.d.ts +738 -0
- package/dist/types/inference/aliaModelRelease.d.ts +609 -0
- package/dist/types/inference/attribution.d.ts +176 -0
- package/dist/types/inference/catalogue.d.ts +1618 -0
- package/dist/types/inference/entitlement.d.ts +519 -0
- package/dist/types/inference/errors.d.ts +242 -0
- package/dist/types/inference/identifiers.d.ts +182 -0
- package/dist/types/inference/inbox.d.ts +374 -0
- package/dist/types/inference/modelDocumentation.d.ts +1603 -0
- package/dist/types/inference/money.d.ts +185 -0
- package/dist/types/inference/priceVersion.d.ts +182 -0
- package/dist/types/inference/providerConnection.d.ts +968 -0
- package/dist/types/inference/request.d.ts +2800 -0
- package/dist/types/inference/routingPolicy.d.ts +616 -0
- package/dist/types/inference/streamEvents.d.ts +950 -0
- package/dist/types/inference/usage.d.ts +1164 -0
- package/dist/types/inference/version.d.ts +102 -0
- package/dist/types/keyRecovery.d.ts +138 -0
- package/dist/types/keyRotation.d.ts +103 -0
- package/dist/types/links.d.ts +96 -0
- package/dist/types/moderationReputation.d.ts +487 -0
- package/dist/types/oauth.d.ts +86 -0
- package/dist/types/oxyRecordTypes.d.ts +62 -0
- package/dist/types/protocol.d.ts +86 -0
- package/dist/types/recommendations.d.ts +542 -0
- package/dist/types/reputation.d.ts +457 -0
- package/dist/types/sessionStatus.d.ts +231 -0
- package/dist/types/transparency.d.ts +392 -0
- package/dist/types/updates.d.ts +545 -0
- package/dist/types/userInvalidation.d.ts +94 -0
- package/dist/types/userResponse.d.ts +1706 -0
- package/dist/types/username.d.ts +265 -0
- package/dist/types/webauthn.d.ts +77 -0
- package/package.json +87 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Civic / Commons API contracts (Fase 1 — DNI + crypto-owned reputation).
|
|
4
|
+
*
|
|
5
|
+
* SINGLE SOURCE OF TRUTH for the wire shape of the public "DNI" card a Commons
|
|
6
|
+
* user shows (and others scan): the user's DID, display identity, trust tier,
|
|
7
|
+
* personhood status, verified domains, and credential badges — sealed with an
|
|
8
|
+
* Oxy custodial attestation so a scanner can verify it OFFLINE against the Oxy
|
|
9
|
+
* public key (the same `ES256K-DER-SHA256` scheme as the signed data export).
|
|
10
|
+
*
|
|
11
|
+
* The QR encodes ONLY the DID (`oxydni://card?did=…`) — never trust data — so a
|
|
12
|
+
* card cannot be spoofed by crafting a QR; the scanner resolves the signed card
|
|
13
|
+
* server-side and verifies the Oxy signature. The attestation is computed over
|
|
14
|
+
* the canonical-JSON of the `card` object, so a consumer re-canonicalizes the
|
|
15
|
+
* card it received and verifies `attestation.signature` against
|
|
16
|
+
* `attestation.publicKey` (which MUST be a current verification method of the
|
|
17
|
+
* Oxy DID).
|
|
18
|
+
*
|
|
19
|
+
* Explicit-`interface` exports (PublicCard, SignedPublicCard) follow the same
|
|
20
|
+
* node-resolution rationale as `UserNameResponse` / the identity contracts: a
|
|
21
|
+
* nested `z.infer<>` can degrade to `{}` under a consumer's
|
|
22
|
+
* `moduleResolution: "node"`, so the load-bearing shapes are declared as literal
|
|
23
|
+
* interfaces and the runtime schemas are annotated `z.ZodType<Interface>`.
|
|
24
|
+
*
|
|
25
|
+
* The `attestation` reuses the export-bundle `ExportAttestation` shape from
|
|
26
|
+
* `./identity` (mirrored, not duplicated): `{ issuer, publicKey, alg, signature,
|
|
27
|
+
* signedAt }`. It is `null` ONLY when the Oxy signing key is unconfigured (dev).
|
|
28
|
+
*
|
|
29
|
+
* Platform-agnostic — zod only, no react/react-native/expo, ESM-safe.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.credentialVerifyResultSchema = exports.credentialListResultSchema = exports.credentialIssueResultSchema = exports.verifiableCredentialResponseSchema = exports.credentialRecordSchema = exports.vouchResultSchema = exports.personhoodStatusResultSchema = exports.personhoodBreakdownSchema = exports.personhoodVouchRecordSchema = exports.validationVoteResultSchema = exports.validationRequestSummarySchema = exports.validationOpenResultSchema = exports.validationOpenRequestSchema = exports.validationVerdictRecordSchema = exports.realLifeAttestationResultSchema = exports.realLifeAttestationRecordSchema = exports.signedPublicCardSchema = exports.publicCardSchema = void 0;
|
|
33
|
+
const zod_1 = require("zod");
|
|
34
|
+
const identity_1 = require("./identity");
|
|
35
|
+
exports.publicCardSchema = zod_1.z.object({
|
|
36
|
+
did: zod_1.z.string(),
|
|
37
|
+
userId: zod_1.z.string(),
|
|
38
|
+
name: zod_1.z.string(),
|
|
39
|
+
username: zod_1.z.string().optional(),
|
|
40
|
+
avatarUrl: zod_1.z.string().optional(),
|
|
41
|
+
trustTier: zod_1.z.enum(['restricted', 'new', 'trusted', 'high_trust', 'verified']),
|
|
42
|
+
personhoodStatus: zod_1.z.enum(['unverified', 'pending', 'verified']),
|
|
43
|
+
verifiedDomains: zod_1.z.array(zod_1.z.string()),
|
|
44
|
+
credentialBadges: zod_1.z.array(zod_1.z.string()),
|
|
45
|
+
issuedAt: zod_1.z.number(),
|
|
46
|
+
});
|
|
47
|
+
exports.signedPublicCardSchema = zod_1.z.object({
|
|
48
|
+
card: exports.publicCardSchema,
|
|
49
|
+
attestation: identity_1.exportAttestationSchema.nullable(),
|
|
50
|
+
});
|
|
51
|
+
exports.realLifeAttestationRecordSchema = zod_1.z.object({
|
|
52
|
+
about: zod_1.z.string(),
|
|
53
|
+
context: zod_1.z.string(),
|
|
54
|
+
nonce: zod_1.z.string(),
|
|
55
|
+
exp: zod_1.z.number(),
|
|
56
|
+
geohash: zod_1.z.string().optional(),
|
|
57
|
+
biometricOk: zod_1.z.boolean().optional(),
|
|
58
|
+
});
|
|
59
|
+
exports.realLifeAttestationResultSchema = zod_1.z.object({
|
|
60
|
+
accepted: zod_1.z.literal(true),
|
|
61
|
+
recordId: zod_1.z.string(),
|
|
62
|
+
subjectUserId: zod_1.z.string(),
|
|
63
|
+
attestorUserId: zod_1.z.string(),
|
|
64
|
+
points: zod_1.z.number(),
|
|
65
|
+
});
|
|
66
|
+
exports.validationVerdictRecordSchema = zod_1.z.object({
|
|
67
|
+
requestId: zod_1.z.string(),
|
|
68
|
+
payloadHash: zod_1.z.string(),
|
|
69
|
+
verdict: zod_1.z.enum(['valid', 'invalid', 'abstain']),
|
|
70
|
+
});
|
|
71
|
+
/** Request body for opening a validation request (`POST /civic/validations`). */
|
|
72
|
+
exports.validationOpenRequestSchema = zod_1.z.object({
|
|
73
|
+
subjectUserId: zod_1.z.string(),
|
|
74
|
+
actionType: zod_1.z.string().min(1),
|
|
75
|
+
sourceActionId: zod_1.z.string().min(1),
|
|
76
|
+
payload: zod_1.z.record(zod_1.z.unknown()),
|
|
77
|
+
highValue: zod_1.z.boolean().optional(),
|
|
78
|
+
});
|
|
79
|
+
exports.validationOpenResultSchema = zod_1.z.object({
|
|
80
|
+
requestId: zod_1.z.string(),
|
|
81
|
+
selectedValidatorCount: zod_1.z.number(),
|
|
82
|
+
expiresAt: zod_1.z.string(),
|
|
83
|
+
});
|
|
84
|
+
exports.validationRequestSummarySchema = zod_1.z.object({
|
|
85
|
+
id: zod_1.z.string(),
|
|
86
|
+
subjectUserId: zod_1.z.string(),
|
|
87
|
+
actionType: zod_1.z.string(),
|
|
88
|
+
payload: zod_1.z.record(zod_1.z.unknown()),
|
|
89
|
+
payloadHash: zod_1.z.string(),
|
|
90
|
+
status: zod_1.z.enum(['pending', 'quorum_met', 'validated', 'rejected', 'expired']),
|
|
91
|
+
highValue: zod_1.z.boolean(),
|
|
92
|
+
expiresAt: zod_1.z.string(),
|
|
93
|
+
});
|
|
94
|
+
exports.validationVoteResultSchema = zod_1.z.object({
|
|
95
|
+
recorded: zod_1.z.literal(true),
|
|
96
|
+
requestId: zod_1.z.string(),
|
|
97
|
+
verdict: zod_1.z.enum(['valid', 'invalid', 'abstain']),
|
|
98
|
+
status: zod_1.z.enum(['pending', 'quorum_met', 'validated', 'rejected', 'expired']),
|
|
99
|
+
});
|
|
100
|
+
exports.personhoodVouchRecordSchema = zod_1.z.object({
|
|
101
|
+
about: zod_1.z.string(),
|
|
102
|
+
context: zod_1.z.string().optional(),
|
|
103
|
+
stake: zod_1.z.number().optional(),
|
|
104
|
+
});
|
|
105
|
+
exports.personhoodBreakdownSchema = zod_1.z.object({
|
|
106
|
+
vouchSignal: zod_1.z.number(),
|
|
107
|
+
realLifeSignal: zod_1.z.number(),
|
|
108
|
+
biometricSignal: zod_1.z.number(),
|
|
109
|
+
evidence: zod_1.z.number(),
|
|
110
|
+
sybilPenalty: zod_1.z.number(),
|
|
111
|
+
seed: zod_1.z.boolean(),
|
|
112
|
+
});
|
|
113
|
+
exports.personhoodStatusResultSchema = zod_1.z.object({
|
|
114
|
+
userId: zod_1.z.string(),
|
|
115
|
+
score: zod_1.z.number(),
|
|
116
|
+
isRealPerson: zod_1.z.boolean(),
|
|
117
|
+
vouchCount: zod_1.z.number(),
|
|
118
|
+
realLifeCount: zod_1.z.number(),
|
|
119
|
+
biometricBound: zod_1.z.boolean(),
|
|
120
|
+
sybilPenalty: zod_1.z.number(),
|
|
121
|
+
breakdown: exports.personhoodBreakdownSchema.nullable(),
|
|
122
|
+
updatedAt: zod_1.z.string().nullable(),
|
|
123
|
+
});
|
|
124
|
+
exports.vouchResultSchema = zod_1.z.object({
|
|
125
|
+
accepted: zod_1.z.literal(true),
|
|
126
|
+
recordId: zod_1.z.string(),
|
|
127
|
+
subjectUserId: zod_1.z.string(),
|
|
128
|
+
voucherUserId: zod_1.z.string(),
|
|
129
|
+
stakeAmount: zod_1.z.number(),
|
|
130
|
+
points: zod_1.z.number(),
|
|
131
|
+
});
|
|
132
|
+
exports.credentialRecordSchema = zod_1.z.object({
|
|
133
|
+
about: zod_1.z.string(),
|
|
134
|
+
types: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
135
|
+
claims: zod_1.z.record(zod_1.z.unknown()),
|
|
136
|
+
expiresAt: zod_1.z.number().optional(),
|
|
137
|
+
});
|
|
138
|
+
exports.verifiableCredentialResponseSchema = zod_1.z.object({
|
|
139
|
+
id: zod_1.z.string(),
|
|
140
|
+
recordId: zod_1.z.string(),
|
|
141
|
+
holderUserId: zod_1.z.string(),
|
|
142
|
+
holderDid: zod_1.z.string(),
|
|
143
|
+
issuerUserId: zod_1.z.string().optional(),
|
|
144
|
+
issuerDid: zod_1.z.string(),
|
|
145
|
+
types: zod_1.z.array(zod_1.z.string()),
|
|
146
|
+
claims: zod_1.z.record(zod_1.z.unknown()),
|
|
147
|
+
status: zod_1.z.enum(['active', 'revoked', 'expired']),
|
|
148
|
+
issuedAt: zod_1.z.number(),
|
|
149
|
+
expiresAt: zod_1.z.number().optional(),
|
|
150
|
+
revokedAt: zod_1.z.number().optional(),
|
|
151
|
+
});
|
|
152
|
+
exports.credentialIssueResultSchema = zod_1.z.object({
|
|
153
|
+
accepted: zod_1.z.literal(true),
|
|
154
|
+
credential: exports.verifiableCredentialResponseSchema,
|
|
155
|
+
});
|
|
156
|
+
exports.credentialListResultSchema = zod_1.z.object({
|
|
157
|
+
credentials: zod_1.z.array(exports.verifiableCredentialResponseSchema),
|
|
158
|
+
});
|
|
159
|
+
exports.credentialVerifyResultSchema = zod_1.z.object({
|
|
160
|
+
valid: zod_1.z.boolean(),
|
|
161
|
+
reason: zod_1.z.string().optional(),
|
|
162
|
+
credential: exports.verifiableCredentialResponseSchema.nullable(),
|
|
163
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical contract for the "Sign in with Oxy" approval handoff.
|
|
4
|
+
*
|
|
5
|
+
* SINGLE SOURCE OF TRUTH for the closed set of reasons an approver may attach
|
|
6
|
+
* when it DENIES a pending request via
|
|
7
|
+
* `POST /auth/session/deny/:authorizeCode`.
|
|
8
|
+
*
|
|
9
|
+
* That endpoint is UNAUTHENTICATED — the public `authorizeCode` is the only
|
|
10
|
+
* credential — so a free-form string from it is never stored: it would be an
|
|
11
|
+
* unauthenticated write of arbitrary text onto a record other surfaces read.
|
|
12
|
+
* The set is therefore deliberately tiny, and closed:
|
|
13
|
+
*
|
|
14
|
+
* - `'declined'` the approver rejected a request they recognised ("Not now").
|
|
15
|
+
* - `'not_me'` the approver did not start the request ("This wasn't me").
|
|
16
|
+
* The ONE value that records the denial as suspicious rather
|
|
17
|
+
* than an ordinary cancel, so a UI may only offer it where the
|
|
18
|
+
* user genuinely said so.
|
|
19
|
+
*
|
|
20
|
+
* Why this lives in `@oxy.so/contracts` rather than in either consumer: the same
|
|
21
|
+
* closed set is enforced in three places — the request schema of the API route,
|
|
22
|
+
* the `enum` of the persisted `AuthSession.deniedReason` field, and the client
|
|
23
|
+
* SDK's `denyCommonsSignIn` parameter. Two hand-maintained copies of a wire
|
|
24
|
+
* contract drift the moment a value is added on one side only, and the failure
|
|
25
|
+
* lands at runtime, in an auth path, as a generic validation error. One
|
|
26
|
+
* declaration makes that impossible.
|
|
27
|
+
*
|
|
28
|
+
* Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
|
|
29
|
+
* `require()`).
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.IDENTITY_APPROVAL_PUSH_CHANNEL = exports.commonsDenyReasonSchema = exports.COMMONS_DENY_REASONS = void 0;
|
|
33
|
+
const zod_1 = require("zod");
|
|
34
|
+
/**
|
|
35
|
+
* The closed set, as a value — consumed directly where a runtime list is
|
|
36
|
+
* required (e.g. the Mongoose `enum` of `AuthSession.deniedReason`, which is
|
|
37
|
+
* the storage-level guarantee that an unauthenticated caller can never write
|
|
38
|
+
* free-form text into the field).
|
|
39
|
+
*/
|
|
40
|
+
exports.COMMONS_DENY_REASONS = ['declined', 'not_me'];
|
|
41
|
+
/**
|
|
42
|
+
* The same set as a zod enum — the edge validator. Anything outside it
|
|
43
|
+
* (including free-form text) is rejected with 400 before any handler runs.
|
|
44
|
+
*/
|
|
45
|
+
exports.commonsDenyReasonSchema = zod_1.z.enum(exports.COMMONS_DENY_REASONS);
|
|
46
|
+
/**
|
|
47
|
+
* Android notification channel id the identity-approval push is sent on.
|
|
48
|
+
*
|
|
49
|
+
* A wire contract for the same reason the deny set is: Android 8+ DROPS a
|
|
50
|
+
* notification whose channel id the app has not created, silently and with no
|
|
51
|
+
* client-side error. The API attaches this id when it sends, and the vault
|
|
52
|
+
* creates the channel with it before registering a push token — two hand-typed
|
|
53
|
+
* copies of that string would fail as "the notification never arrived", which
|
|
54
|
+
* is the single hardest push symptom to diagnose.
|
|
55
|
+
*
|
|
56
|
+
* The channel's user-visible NAME and description are deliberately NOT here:
|
|
57
|
+
* those are localized app copy, and the vault owns them.
|
|
58
|
+
*/
|
|
59
|
+
exports.IDENTITY_APPROVAL_PUSH_CHANNEL = 'auth-approval';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* First-party login result contract.
|
|
4
|
+
*
|
|
5
|
+
* SINGLE SOURCE OF TRUTH for the first-party login result (the session arm). The
|
|
6
|
+
* API validates its OUTPUT against this schema; every consumer (`@oxy.so/core`'s
|
|
7
|
+
* auth mixin) validates its INPUT against the same definition, so producer and
|
|
8
|
+
* consumers cannot drift. Sign-in is passkey (WebAuthn) or Commons handoff —
|
|
9
|
+
* password and 2FA were removed, so the only outcome is a completed session.
|
|
10
|
+
*
|
|
11
|
+
* The device transport is `deviceId` + `deviceSecret` + `POST /session/device/token`
|
|
12
|
+
* (see `deviceSession.ts`). The legacy cookie/bootstrap/refresh-family lanes were
|
|
13
|
+
* removed in the zero-cookie cutover — nothing here carries a refresh token or a
|
|
14
|
+
* boot fragment.
|
|
15
|
+
*
|
|
16
|
+
* Nested-object response shapes are declared as explicit `interface`s with the
|
|
17
|
+
* runtime schema annotated `z.ZodType<Interface>` — the same rationale as
|
|
18
|
+
* `identity.ts` / `userResponse.ts`: a `z.infer<>` of a nested object schema can
|
|
19
|
+
* degrade to `{}` under a consumer's `moduleResolution: "node"` (node10), so the
|
|
20
|
+
* load-bearing shapes are pinned by literal interfaces. Flat request/response
|
|
21
|
+
* shapes (no nested-object hazard) are inferred via `z.infer<>`.
|
|
22
|
+
*
|
|
23
|
+
* Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
|
|
24
|
+
* `require()`).
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.loginResultSchema = void 0;
|
|
28
|
+
const zod_1 = require("zod");
|
|
29
|
+
const securityAlertSchema = zod_1.z.object({
|
|
30
|
+
message: zod_1.z.string(),
|
|
31
|
+
anomalies: zod_1.z.array(zod_1.z.object({
|
|
32
|
+
type: zod_1.z.string(),
|
|
33
|
+
reason: zod_1.z.string(),
|
|
34
|
+
details: zod_1.z.string().optional(),
|
|
35
|
+
})),
|
|
36
|
+
});
|
|
37
|
+
const loginSessionResultSchema = zod_1.z.object({
|
|
38
|
+
sessionId: zod_1.z.string(),
|
|
39
|
+
deviceId: zod_1.z.string(),
|
|
40
|
+
expiresAt: zod_1.z.string(),
|
|
41
|
+
accessToken: zod_1.z.string().optional(),
|
|
42
|
+
deviceSecret: zod_1.z.string().optional(),
|
|
43
|
+
securityAlert: securityAlertSchema.optional(),
|
|
44
|
+
user: zod_1.z.object({
|
|
45
|
+
id: zod_1.z.string(),
|
|
46
|
+
username: zod_1.z.string().optional(),
|
|
47
|
+
avatar: zod_1.z.string().optional(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
exports.loginResultSchema = loginSessionResultSchema;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deviceDirectorySyncSchema = exports.deviceActivateResponseSchema = exports.deviceActivateRequestSchema = exports.deviceDirectorySchema = exports.devicePrincipalSchema = exports.deviceAccountContextSchema = exports.deviceDirectoryProfileSchema = exports.deviceContextRelationshipSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const accountGraph_1 = require("./accountGraph");
|
|
6
|
+
const deviceSession_1 = require("./deviceSession");
|
|
7
|
+
const userResponse_1 = require("./userResponse");
|
|
8
|
+
/**
|
|
9
|
+
* The canonical device directory — the ONE server-authoritative read model an
|
|
10
|
+
* account switcher renders (issue #937, ADR 0002).
|
|
11
|
+
*
|
|
12
|
+
* It replaces the client-side union of `DeviceSessionState.accounts[]` with a
|
|
13
|
+
* separately fetched `AccountNode[]` graph. That union cannot be correct on a
|
|
14
|
+
* device holding more than one person: the client only ever holds ONE caller's
|
|
15
|
+
* account graph, so it cannot enumerate what the OTHER principals may act as.
|
|
16
|
+
* Switchability is an authorization question, so the server answers it.
|
|
17
|
+
*
|
|
18
|
+
* The directory is deterministic and revision-bound: two reads at the same
|
|
19
|
+
* `revision` describe the same device state, and `revision` only advances on a
|
|
20
|
+
* real mutation (an idempotent activation advances nothing — ADR 0002).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* How a principal reaches an account.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors `AccountRelationship` in `@oxy.so/core`'s account graph rather than
|
|
26
|
+
* inventing a second vocabulary for the same fact:
|
|
27
|
+
* - `self` — the principal's own personal account (`principal.userId === accountId`)
|
|
28
|
+
* - `owner` — the principal owns this account
|
|
29
|
+
* - `member` — the principal holds a membership granting `account:act_as`
|
|
30
|
+
*/
|
|
31
|
+
exports.deviceContextRelationshipSchema = zod_1.z.enum(['self', 'owner', 'member']);
|
|
32
|
+
/**
|
|
33
|
+
* Sanitized display metadata for a principal or an account context.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT `userResponseSchema`: a switcher needs a handle, a name, an
|
|
36
|
+
* avatar and the accent the row is drawn in, and the directory is read by every
|
|
37
|
+
* app on the device — so it carries the minimum that renders a row and nothing
|
|
38
|
+
* that would make it a general profile feed. `name.displayName` stays OPTIONAL;
|
|
39
|
+
* consumers fall back to the handle (`getNormalizedUserHandle`), never to a
|
|
40
|
+
* synthesized name.
|
|
41
|
+
*
|
|
42
|
+
* `color` is here and `email` is not, and the line between them is what the
|
|
43
|
+
* field is FOR rather than how sensitive it looks. An accent is a property of
|
|
44
|
+
* drawing the row — without it every non-active row falls back to the ambient
|
|
45
|
+
* theme accent and a device holding two people renders them identically
|
|
46
|
+
* (issue #961). An address is somebody's contact detail, and the `@handle` the
|
|
47
|
+
* secondary line already shows fills the same slot, so putting one in a payload
|
|
48
|
+
* every installed app reads would widen the profile for no rendering gain.
|
|
49
|
+
*/
|
|
50
|
+
exports.deviceDirectoryProfileSchema = zod_1.z.object({
|
|
51
|
+
id: zod_1.z.string().min(1),
|
|
52
|
+
username: zod_1.z.string(),
|
|
53
|
+
name: userResponse_1.userNameSchema.optional(),
|
|
54
|
+
avatar: zod_1.z.string().nullable().optional(),
|
|
55
|
+
/**
|
|
56
|
+
* Named Bloom color preset (e.g. `"blue"`), or null when the account has
|
|
57
|
+
* none. Same shape as `userResponseSchema.color` — one spelling of one fact,
|
|
58
|
+
* so a consumer that themes a row from either source reads the same values.
|
|
59
|
+
*/
|
|
60
|
+
color: zod_1.z.string().nullable().optional(),
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* One `principal acting as account` pair — the globally switchable unit.
|
|
64
|
+
*
|
|
65
|
+
* `id` is the identifier `POST /session/device/activate` takes. It names the
|
|
66
|
+
* PAIR, not the account: the same `accountId` legitimately appears under two
|
|
67
|
+
* principals on a shared device, and those are different sessions, permissions,
|
|
68
|
+
* audit actors and revocation paths.
|
|
69
|
+
*
|
|
70
|
+
* `onDevice` is `false` for a context the principal may act as but has never
|
|
71
|
+
* activated here — it exists as a row so it has a stable id to activate, and
|
|
72
|
+
* its delegated session is minted on first activation rather than eagerly for
|
|
73
|
+
* every organization the principal belongs to.
|
|
74
|
+
*
|
|
75
|
+
* `available` is `principalLive && (personal || live account:act_as)`, and the
|
|
76
|
+
* principal clause is not decoration: a principal whose own personal session
|
|
77
|
+
* has died makes EVERY context of theirs unavailable, including delegated ones
|
|
78
|
+
* whose sessions are perfectly alive. Activation verifies the principal's
|
|
79
|
+
* personal session (ADR 0002 step 3), so a client that models availability as
|
|
80
|
+
* "delegated context has a live session ⇒ activatable" will be refused by the
|
|
81
|
+
* server and the context healed away.
|
|
82
|
+
*
|
|
83
|
+
* A managed account whose membership was revoked is returned with
|
|
84
|
+
* `available: false` rather than silently omitted, so the UI can explain a row
|
|
85
|
+
* disappearing instead of just dropping it.
|
|
86
|
+
*
|
|
87
|
+
* (An earlier version of this comment described `available` as the
|
|
88
|
+
* `account:act_as` verdict alone. It was written before the server existed and
|
|
89
|
+
* was weaker than the code that shipped — the kind of wrong statement nothing
|
|
90
|
+
* recomputes, so it is spelled out here in full.)
|
|
91
|
+
*/
|
|
92
|
+
exports.deviceAccountContextSchema = zod_1.z.object({
|
|
93
|
+
id: zod_1.z.string().min(1),
|
|
94
|
+
accountId: zod_1.z.string().min(1),
|
|
95
|
+
kind: accountGraph_1.accountKindSchema,
|
|
96
|
+
relationship: exports.deviceContextRelationshipSchema,
|
|
97
|
+
account: exports.deviceDirectoryProfileSchema,
|
|
98
|
+
onDevice: zod_1.z.boolean(),
|
|
99
|
+
available: zod_1.z.boolean(),
|
|
100
|
+
active: zod_1.z.boolean(),
|
|
101
|
+
lastUsedAt: zod_1.z.number().nullable(),
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* A human who authenticated onto this device.
|
|
105
|
+
*
|
|
106
|
+
* Never an organization, project, channel or bot — those are subjects a
|
|
107
|
+
* principal acts as, and they appear only in `contexts`. `authuser` is the
|
|
108
|
+
* Google-style signed-in-human slot and belongs HERE, not to an account: adding
|
|
109
|
+
* an organization must never consume one.
|
|
110
|
+
*/
|
|
111
|
+
exports.devicePrincipalSchema = zod_1.z.object({
|
|
112
|
+
id: zod_1.z.string().min(1),
|
|
113
|
+
userId: zod_1.z.string().min(1),
|
|
114
|
+
authuser: zod_1.z.number().int().nonnegative(),
|
|
115
|
+
user: exports.deviceDirectoryProfileSchema,
|
|
116
|
+
contexts: zod_1.z.array(exports.deviceAccountContextSchema),
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* Response of `GET /session/device/directory`.
|
|
120
|
+
*
|
|
121
|
+
* `activeContextId` is the authority for what every official app in
|
|
122
|
+
* `sessionMode: 'account'` renders. It is null when the device has no active
|
|
123
|
+
* context — a real state (every context removed, or an active context healed
|
|
124
|
+
* away), not an error.
|
|
125
|
+
*/
|
|
126
|
+
exports.deviceDirectorySchema = zod_1.z.object({
|
|
127
|
+
deviceId: zod_1.z.string().min(1),
|
|
128
|
+
revision: zod_1.z.number().int().nonnegative(),
|
|
129
|
+
activeContextId: zod_1.z.string().nullable(),
|
|
130
|
+
principals: zod_1.z.array(exports.devicePrincipalSchema),
|
|
131
|
+
updatedAt: zod_1.z.number(),
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* Request body of `POST /session/device/activate`.
|
|
135
|
+
*
|
|
136
|
+
* `contextId`, never `accountId`: an account id cannot name a context on a
|
|
137
|
+
* device where two people can both reach the same organization, and resolving
|
|
138
|
+
* that ambiguity server-side would mean guessing inside an authorization path.
|
|
139
|
+
*/
|
|
140
|
+
exports.deviceActivateRequestSchema = zod_1.z.object({
|
|
141
|
+
contextId: zod_1.z.string().min(1),
|
|
142
|
+
});
|
|
143
|
+
/**
|
|
144
|
+
* Response of `POST /session/device/activate`.
|
|
145
|
+
*
|
|
146
|
+
* Carries the post-transition directory AND the bearer for the newly active
|
|
147
|
+
* context, because the client's ordering invariant is
|
|
148
|
+
* `commit token → reset caches → publish snapshot → notify` (ADR 0002). Handing
|
|
149
|
+
* the directory back without the token would force a second round trip in the
|
|
150
|
+
* middle of that sequence, which is exactly where a component would render the
|
|
151
|
+
* new subject while still holding the previous subject's bearer.
|
|
152
|
+
*
|
|
153
|
+
* `activeToken` is null when the activation legitimately produced no bearer for
|
|
154
|
+
* THIS caller — an identity-pinned client, or a caller whose application is not
|
|
155
|
+
* entitled to a token for the new context. It is never an error signal.
|
|
156
|
+
*/
|
|
157
|
+
exports.deviceActivateResponseSchema = zod_1.z.object({
|
|
158
|
+
directory: exports.deviceDirectorySchema,
|
|
159
|
+
activeToken: deviceSession_1.activeTokenSchema.nullable(),
|
|
160
|
+
});
|
|
161
|
+
/**
|
|
162
|
+
* Response of the CONTEXT-aware removals — `POST /session/device/signout` with
|
|
163
|
+
* `{ contextId }` (one `principal → account` pair) or `{ principalId }` (one
|
|
164
|
+
* person and every context they reach, and nobody else's).
|
|
165
|
+
*
|
|
166
|
+
* It carries BOTH halves because a removal elects a replacement active context,
|
|
167
|
+
* so both the directory and the flat compatibility projection move in the same
|
|
168
|
+
* transition — and a client that learned only one of them would render one
|
|
169
|
+
* half of a device that no longer exists.
|
|
170
|
+
*
|
|
171
|
+
* This is deliberately NOT {@link deviceSessionSyncSchema} and must never be
|
|
172
|
+
* merged into it. A zod object strips unknown keys, so `{directory, state,
|
|
173
|
+
* activeToken}` parses cleanly as `{state, activeToken}` — silently dropping the
|
|
174
|
+
* directory. One schema covering both shapes would therefore make "the server
|
|
175
|
+
* stopped sending the directory" and "this endpoint never sends one"
|
|
176
|
+
* indistinguishable at the parse, on the exact path that decides which identity
|
|
177
|
+
* the app is running as. Two schemas fail closed instead: `directory` is
|
|
178
|
+
* required here, so a directory-less payload is refused outright.
|
|
179
|
+
*
|
|
180
|
+
* `activeToken` is null on the same terms as everywhere else — an identity-
|
|
181
|
+
* pinned caller, or one not entitled to a bearer for the newly-elected context —
|
|
182
|
+
* and null is also the honest answer when the removal left the device with no
|
|
183
|
+
* active context at all.
|
|
184
|
+
*/
|
|
185
|
+
exports.deviceDirectorySyncSchema = zod_1.z.object({
|
|
186
|
+
directory: exports.deviceDirectorySchema,
|
|
187
|
+
state: deviceSession_1.deviceSessionStateSchema,
|
|
188
|
+
activeToken: deviceSession_1.activeTokenSchema.nullable(),
|
|
189
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Device-to-device identity transfer contracts (b3 Feature 2 — "add a device").
|
|
4
|
+
*
|
|
5
|
+
* SINGLE SOURCE OF TRUTH for the wire shape of the short-lived, unauthenticated
|
|
6
|
+
* relay that carries E2E-encrypted key material from an existing (old) device to
|
|
7
|
+
* a fresh (new) device so both end up holding the SAME secp256k1 private key
|
|
8
|
+
* (key cloning). The relay is E2E-encrypted via an ephemeral secp256k1 ECDH
|
|
9
|
+
* handshake: the server stores only the two ephemeral public keys plus an opaque
|
|
10
|
+
* AEAD ciphertext + nonce and NEVER holds a decryption key.
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* 1. New device (no identity) generates an ephemeral pair and calls
|
|
14
|
+
* `POST /identity/device-transfer/init { newEphPub, newDeviceLabel? }` →
|
|
15
|
+
* `{ pairingId, expiresAt }`. The QR carries ONLY `pairingId` (not
|
|
16
|
+
* self-contained — mirrors the QR sign-in `approve-info` resolution).
|
|
17
|
+
* 2. Old device (has identity) scans, resolves the request via
|
|
18
|
+
* `GET /identity/device-transfer/:pairingId` (returns `newEphPub` + label),
|
|
19
|
+
* derives `transferKey = HKDF(ECDH(oldEphPriv, newEphPub), pairingId,
|
|
20
|
+
* 'oxy-device-transfer-v1')`, AEAD-encrypts `{ privateKey, publicKey }`, and
|
|
21
|
+
* calls `POST /identity/device-transfer/:pairingId/approve` with the
|
|
22
|
+
* ciphertext PLUS a fresh signature over
|
|
23
|
+
* `{ action:'approve_device_transfer', pairingId, timestamp }` made with the
|
|
24
|
+
* CURRENT identity key (dual-proof: a bearer alone cannot exfiltrate the key).
|
|
25
|
+
* 3. New device (socket push or poll fallback) re-derives the same
|
|
26
|
+
* `transferKey` from `ECDH(newEphPriv, oldEphPub)`, decrypts, and imports the
|
|
27
|
+
* private key, then completes a NORMAL challenge/verify sign-in.
|
|
28
|
+
*
|
|
29
|
+
* The load-bearing response shapes are declared as explicit `interface`s (same
|
|
30
|
+
* `moduleResolution: node` rationale as `UserNameResponse` / the identity/civic
|
|
31
|
+
* contracts: a nested `z.infer<>` can degrade to `{}` under a consumer's
|
|
32
|
+
* `moduleResolution: "node"`), with the runtime schemas annotated
|
|
33
|
+
* `z.ZodType<Interface>`.
|
|
34
|
+
*
|
|
35
|
+
* Platform-agnostic — zod only, no react/react-native/expo, ESM-safe.
|
|
36
|
+
*/
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.deviceTransferDenyResponseSchema = exports.deviceTransferApproveResponseSchema = exports.deviceTransferApproveRequestSchema = exports.deviceTransferInfoResponseSchema = exports.deviceTransferInitResponseSchema = exports.deviceTransferInitRequestSchema = exports.devicePairingStatusSchema = void 0;
|
|
39
|
+
const zod_1 = require("zod");
|
|
40
|
+
/* -------------------------------------------------------------------------- */
|
|
41
|
+
/* Shared field validators */
|
|
42
|
+
/* -------------------------------------------------------------------------- */
|
|
43
|
+
/** Lowercase/uppercase hex string (no `0x` prefix). */
|
|
44
|
+
const hexString = zod_1.z
|
|
45
|
+
.string()
|
|
46
|
+
.trim()
|
|
47
|
+
.regex(/^[0-9a-fA-F]+$/, 'must be a hex string');
|
|
48
|
+
/**
|
|
49
|
+
* A secp256k1 public key, hex-encoded — compressed (`02`/`03` + 32 bytes = 66
|
|
50
|
+
* chars) or uncompressed (`04` + 64 bytes = 130 chars). The exact curve-point
|
|
51
|
+
* validity is re-checked server-side; this only bounds the shape/length.
|
|
52
|
+
*/
|
|
53
|
+
const publicKeyHex = hexString.min(66).max(130);
|
|
54
|
+
/** DER-encoded ECDSA signature, hex (variable length, ~140–144 chars). */
|
|
55
|
+
const signatureHex = hexString.min(2).max(256);
|
|
56
|
+
/**
|
|
57
|
+
* The 24-byte XChaCha20-Poly1305 nonce, hex (exactly 48 chars). Matches
|
|
58
|
+
* `@oxy.so/core` `AEAD_NONCE_LENGTH` (24 bytes).
|
|
59
|
+
*/
|
|
60
|
+
const nonceHex = hexString.length(48);
|
|
61
|
+
/**
|
|
62
|
+
* The AEAD ciphertext (Poly1305 tag appended), hex. The plaintext is the small
|
|
63
|
+
* JSON `{ privateKey, publicKey }` (~200 bytes), so the ciphertext stays well
|
|
64
|
+
* under the cap; the bound blunts relay-abuse via oversized blobs.
|
|
65
|
+
*/
|
|
66
|
+
const ciphertextHex = hexString.min(2).max(8192);
|
|
67
|
+
/* -------------------------------------------------------------------------- */
|
|
68
|
+
/* Status */
|
|
69
|
+
/* -------------------------------------------------------------------------- */
|
|
70
|
+
/**
|
|
71
|
+
* Pairing lifecycle:
|
|
72
|
+
* - `pending` — created by the new device, awaiting the old device's approval.
|
|
73
|
+
* - `approved` — the old device sealed and posted the encrypted key material.
|
|
74
|
+
* - `denied` — the old device explicitly cancelled the transfer.
|
|
75
|
+
* - `expired` — the 3-minute TTL elapsed before approval.
|
|
76
|
+
*/
|
|
77
|
+
exports.devicePairingStatusSchema = zod_1.z.enum([
|
|
78
|
+
'pending',
|
|
79
|
+
'approved',
|
|
80
|
+
'denied',
|
|
81
|
+
'expired',
|
|
82
|
+
]);
|
|
83
|
+
/* -------------------------------------------------------------------------- */
|
|
84
|
+
/* POST /identity/device-transfer/init */
|
|
85
|
+
/* -------------------------------------------------------------------------- */
|
|
86
|
+
/** Request body for `POST /identity/device-transfer/init` (public). */
|
|
87
|
+
exports.deviceTransferInitRequestSchema = zod_1.z.object({
|
|
88
|
+
/** The new device's ephemeral secp256k1 public key (single-use). */
|
|
89
|
+
newEphPub: publicKeyHex,
|
|
90
|
+
/** Optional human-readable label for the new device (e.g. "iPhone 15"). */
|
|
91
|
+
newDeviceLabel: zod_1.z.string().trim().min(1).max(120).optional(),
|
|
92
|
+
});
|
|
93
|
+
exports.deviceTransferInitResponseSchema = zod_1.z.object({
|
|
94
|
+
pairingId: zod_1.z.string(),
|
|
95
|
+
expiresAt: zod_1.z.string(),
|
|
96
|
+
});
|
|
97
|
+
exports.deviceTransferInfoResponseSchema = zod_1.z.object({
|
|
98
|
+
pairingId: zod_1.z.string(),
|
|
99
|
+
newDeviceEphemeralPublicKey: zod_1.z.string(),
|
|
100
|
+
newDeviceLabel: zod_1.z.string().nullable(),
|
|
101
|
+
status: exports.devicePairingStatusSchema,
|
|
102
|
+
expiresAt: zod_1.z.string(),
|
|
103
|
+
oldDeviceEphemeralPublicKey: zod_1.z.string().nullable(),
|
|
104
|
+
ciphertext: zod_1.z.string().nullable(),
|
|
105
|
+
nonce: zod_1.z.string().nullable(),
|
|
106
|
+
});
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
/* POST /identity/device-transfer/:pairingId/approve */
|
|
109
|
+
/* -------------------------------------------------------------------------- */
|
|
110
|
+
/**
|
|
111
|
+
* Request body for `POST /identity/device-transfer/:pairingId/approve`
|
|
112
|
+
* (bearer-authenticated AND signature-proven). The `signature` covers
|
|
113
|
+
* `JSON.stringify({ action:'approve_device_transfer', pairingId, timestamp })`
|
|
114
|
+
* made with the caller's CURRENT identity key — dual-proof so a bearer token
|
|
115
|
+
* alone can never exfiltrate the private key.
|
|
116
|
+
*/
|
|
117
|
+
exports.deviceTransferApproveRequestSchema = zod_1.z.object({
|
|
118
|
+
/** The old device's ephemeral secp256k1 public key (single-use). */
|
|
119
|
+
oldEphPub: publicKeyHex,
|
|
120
|
+
/** AEAD ciphertext of `{ privateKey, publicKey }`, hex. */
|
|
121
|
+
ciphertext: ciphertextHex,
|
|
122
|
+
/** AEAD nonce, hex (24 bytes). */
|
|
123
|
+
nonce: nonceHex,
|
|
124
|
+
/** ECDSA (DER, hex) signature proving control of the CURRENT identity key. */
|
|
125
|
+
signature: signatureHex,
|
|
126
|
+
/** Signing timestamp (ms since epoch) — freshness-checked server-side. */
|
|
127
|
+
timestamp: zod_1.z.number().int().positive(),
|
|
128
|
+
});
|
|
129
|
+
exports.deviceTransferApproveResponseSchema = zod_1.z.object({
|
|
130
|
+
success: zod_1.z.boolean(),
|
|
131
|
+
pairingId: zod_1.z.string(),
|
|
132
|
+
status: exports.devicePairingStatusSchema,
|
|
133
|
+
});
|
|
134
|
+
exports.deviceTransferDenyResponseSchema = zod_1.z.object({
|
|
135
|
+
success: zod_1.z.boolean(),
|
|
136
|
+
pairingId: zod_1.z.string(),
|
|
137
|
+
status: exports.devicePairingStatusSchema,
|
|
138
|
+
});
|