@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,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural checks for contract values, without Zod.
|
|
3
|
+
*
|
|
4
|
+
* The Zod schemas in this package are the contract, and the API parses with
|
|
5
|
+
* them. A client only needs to know that a response has the shape its type
|
|
6
|
+
* promises, and Zod's object schemas pull Zod's whole runtime into every app
|
|
7
|
+
* that bundles the SDK (~120 KB before compression, evaluated at startup). The
|
|
8
|
+
* checks here give the client the same answers in plain TypeScript.
|
|
9
|
+
*
|
|
10
|
+
* Every check mirrors one Zod 3 schema in this package, and
|
|
11
|
+
* `__tests__/clientChecks.parity.test.ts` runs both over the same inputs and
|
|
12
|
+
* requires the same verdict, the same output and the same failing path. The
|
|
13
|
+
* rules follow Zod 3 exactly:
|
|
14
|
+
*
|
|
15
|
+
* string(...rules) a string; `trim`/`toLowerCase` rewrite it, the other
|
|
16
|
+
* rules test it, in the order given
|
|
17
|
+
* number(...rules) a number that is not NaN (Infinity passes, as in Zod)
|
|
18
|
+
* literal(v) / oneOf `===` one value / one of a fixed set of strings
|
|
19
|
+
* optional(c) `undefined` (or an absent key) passes; `null` does not
|
|
20
|
+
* nullable(c) `null` passes and is kept
|
|
21
|
+
* withDefault(c, v) `undefined` becomes `v`, which is then checked
|
|
22
|
+
* array(c, ...rules) the size rules first, then every element in order
|
|
23
|
+
* object(shape) unknown keys are DROPPED (Zod's default "strip")
|
|
24
|
+
* strictObject(shape) unknown keys FAIL (`.strict()`)
|
|
25
|
+
* looseObject(shape) unknown keys are KEPT (`.passthrough()`)
|
|
26
|
+
* refine(c, test) a further condition, run once `c` passes
|
|
27
|
+
* union / discriminatedUnion as Zod 3 reports them
|
|
28
|
+
*
|
|
29
|
+
* A failure carries the first issue Zod would report and whether Zod would have
|
|
30
|
+
* stopped there (`fatal`: the type was wrong) or kept checking (`fatal: false`:
|
|
31
|
+
* only a rule failed). Only `union` reads the difference, because Zod reports a
|
|
32
|
+
* union through its first "rule failed" option.
|
|
33
|
+
*
|
|
34
|
+
* This module, and everything under `src/client/`, must never import `zod`
|
|
35
|
+
* (`__tests__/clientEntryGraph.test.ts` walks the graph).
|
|
36
|
+
*/
|
|
37
|
+
function ok(value) {
|
|
38
|
+
return { ok: true, value };
|
|
39
|
+
}
|
|
40
|
+
function fail(path, problem, fatal) {
|
|
41
|
+
return { ok: false, issue: { path, ...problem }, fatal };
|
|
42
|
+
}
|
|
43
|
+
function describe(input) {
|
|
44
|
+
if (input === null)
|
|
45
|
+
return 'null';
|
|
46
|
+
if (Array.isArray(input))
|
|
47
|
+
return 'array';
|
|
48
|
+
if (typeof input === 'number' && Number.isNaN(input))
|
|
49
|
+
return 'nan';
|
|
50
|
+
return typeof input;
|
|
51
|
+
}
|
|
52
|
+
/** Zod's wording for a value of the wrong type. */
|
|
53
|
+
function wrongType(path, expected, input) {
|
|
54
|
+
const received = describe(input);
|
|
55
|
+
const message = input === undefined ? 'Required' : `Expected ${expected}, received ${received}`;
|
|
56
|
+
return fail(path, { code: 'invalid_type', message, expected, received }, true);
|
|
57
|
+
}
|
|
58
|
+
/** `'a' | 'b'` — how Zod lists allowed values. */
|
|
59
|
+
function listValues(values, separator = ' | ') {
|
|
60
|
+
return values
|
|
61
|
+
.map((value) => (typeof value === 'string' ? `'${value}'` : String(value)))
|
|
62
|
+
.join(separator);
|
|
63
|
+
}
|
|
64
|
+
function isRecord(input) {
|
|
65
|
+
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
|
66
|
+
}
|
|
67
|
+
export const trim = { rewrite: (value) => value.trim() };
|
|
68
|
+
export const toLowerCase = { rewrite: (value) => value.toLowerCase() };
|
|
69
|
+
export function minLength(min, message) {
|
|
70
|
+
return {
|
|
71
|
+
test: (value) => value.length >= min,
|
|
72
|
+
code: 'too_small',
|
|
73
|
+
message: message ?? `String must contain at least ${min} character(s)`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function maxLength(max, message) {
|
|
77
|
+
return {
|
|
78
|
+
test: (value) => value.length <= max,
|
|
79
|
+
code: 'too_big',
|
|
80
|
+
message: message ?? `String must contain at most ${max} character(s)`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function pattern(regex, message) {
|
|
84
|
+
return {
|
|
85
|
+
test: (value) => {
|
|
86
|
+
regex.lastIndex = 0;
|
|
87
|
+
return regex.test(value);
|
|
88
|
+
},
|
|
89
|
+
code: 'invalid_string',
|
|
90
|
+
message: message ?? 'Invalid',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function startsWith(prefix) {
|
|
94
|
+
return {
|
|
95
|
+
test: (value) => value.startsWith(prefix),
|
|
96
|
+
code: 'invalid_string',
|
|
97
|
+
message: `Invalid input: must start with "${prefix}"`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Zod 3's `.email()`. */
|
|
101
|
+
const EMAIL = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i;
|
|
102
|
+
export const email = pattern(EMAIL, 'Invalid email');
|
|
103
|
+
/** Zod 3's `.url()`: anything `new URL()` accepts. */
|
|
104
|
+
export const url = {
|
|
105
|
+
test: (value) => {
|
|
106
|
+
try {
|
|
107
|
+
new URL(value);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
code: 'invalid_string',
|
|
115
|
+
message: 'Invalid url',
|
|
116
|
+
};
|
|
117
|
+
/** Zod 3's `.datetime()`: ISO 8601 in UTC (`Z`), any sub-second precision, leap days checked. */
|
|
118
|
+
const DATETIME = new RegExp('^((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))' +
|
|
119
|
+
'T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?(Z)$');
|
|
120
|
+
export const datetime = pattern(DATETIME, 'Invalid datetime');
|
|
121
|
+
export function string(...rules) {
|
|
122
|
+
return (input, path) => {
|
|
123
|
+
if (typeof input !== 'string')
|
|
124
|
+
return wrongType(path, 'string', input);
|
|
125
|
+
let value = input;
|
|
126
|
+
let firstFailure = null;
|
|
127
|
+
for (const rule of rules) {
|
|
128
|
+
if ('rewrite' in rule)
|
|
129
|
+
value = rule.rewrite(value);
|
|
130
|
+
else if (firstFailure === null && !rule.test(value)) {
|
|
131
|
+
firstFailure = { code: rule.code, message: rule.message };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return firstFailure === null ? ok(value) : fail(path, firstFailure, false);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Zod's `.int()` — reported, as Zod does, as an `invalid_type` that does not stop the parse. */
|
|
138
|
+
export const int = (value) => Number.isInteger(value)
|
|
139
|
+
? undefined
|
|
140
|
+
: {
|
|
141
|
+
code: 'invalid_type',
|
|
142
|
+
message: 'Expected integer, received float',
|
|
143
|
+
expected: 'integer',
|
|
144
|
+
received: 'float',
|
|
145
|
+
};
|
|
146
|
+
export function atLeast(min) {
|
|
147
|
+
return (value) => value >= min
|
|
148
|
+
? undefined
|
|
149
|
+
: { code: 'too_small', message: `Number must be greater than or equal to ${min}` };
|
|
150
|
+
}
|
|
151
|
+
export function atMost(max) {
|
|
152
|
+
return (value) => value <= max
|
|
153
|
+
? undefined
|
|
154
|
+
: { code: 'too_big', message: `Number must be less than or equal to ${max}` };
|
|
155
|
+
}
|
|
156
|
+
/** Zod's `.positive()`. */
|
|
157
|
+
export const positive = (value) => value > 0 ? undefined : { code: 'too_small', message: 'Number must be greater than 0' };
|
|
158
|
+
/** Zod's `.nonnegative()` (and `.min(0)`). */
|
|
159
|
+
export const nonnegative = atLeast(0);
|
|
160
|
+
/** Zod's `.safe()`: both bounds, low first. */
|
|
161
|
+
export const safe = (value) => atLeast(Number.MIN_SAFE_INTEGER)(value) ?? atMost(Number.MAX_SAFE_INTEGER)(value);
|
|
162
|
+
export function number(...rules) {
|
|
163
|
+
return (input, path) => {
|
|
164
|
+
if (typeof input !== 'number' || Number.isNaN(input))
|
|
165
|
+
return wrongType(path, 'number', input);
|
|
166
|
+
for (const rule of rules) {
|
|
167
|
+
const problem = rule(input);
|
|
168
|
+
if (problem)
|
|
169
|
+
return fail(path, problem, false);
|
|
170
|
+
}
|
|
171
|
+
return ok(input);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
export const boolean = (input, path) => typeof input === 'boolean' ? ok(input) : wrongType(path, 'boolean', input);
|
|
175
|
+
export function literal(expected) {
|
|
176
|
+
return (input, path) => input === expected
|
|
177
|
+
? ok(expected)
|
|
178
|
+
: fail(path, {
|
|
179
|
+
code: 'invalid_literal',
|
|
180
|
+
message: `Invalid literal value, expected ${JSON.stringify(expected)}`,
|
|
181
|
+
}, true);
|
|
182
|
+
}
|
|
183
|
+
/** One of a fixed set of strings — Zod's `z.enum`. */
|
|
184
|
+
export function oneOf(values) {
|
|
185
|
+
return (input, path) => {
|
|
186
|
+
if (typeof input !== 'string')
|
|
187
|
+
return wrongType(path, listValues(values), input);
|
|
188
|
+
return values.includes(input)
|
|
189
|
+
? ok(input)
|
|
190
|
+
: fail(path, {
|
|
191
|
+
code: 'invalid_enum_value',
|
|
192
|
+
message: `Invalid enum value. Expected ${listValues(values)}, received '${input}'`,
|
|
193
|
+
}, true);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/* -------------------------------------------------------------------------- */
|
|
197
|
+
/* Wrappers */
|
|
198
|
+
/* -------------------------------------------------------------------------- */
|
|
199
|
+
export function optional(inner) {
|
|
200
|
+
return (input, path) => (input === undefined ? ok(undefined) : inner(input, path));
|
|
201
|
+
}
|
|
202
|
+
export function nullable(inner) {
|
|
203
|
+
return (input, path) => (input === null ? ok(null) : inner(input, path));
|
|
204
|
+
}
|
|
205
|
+
export function withDefault(inner, fallback) {
|
|
206
|
+
return (input, path) => inner(input === undefined ? fallback : input, path);
|
|
207
|
+
}
|
|
208
|
+
/** A further condition on a value `inner` accepted. */
|
|
209
|
+
export function refine(inner, test, message) {
|
|
210
|
+
return (input, path) => {
|
|
211
|
+
const checked = inner(input, path);
|
|
212
|
+
if (!checked.ok)
|
|
213
|
+
return checked;
|
|
214
|
+
return test(checked.value) ? checked : fail(path, { code: 'custom', message }, false);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Zod's `.superRefine()` for the one-issue case: `test` returns the first
|
|
219
|
+
* problem (its path relative to the value) or `undefined`.
|
|
220
|
+
*/
|
|
221
|
+
export function refineAt(inner, test) {
|
|
222
|
+
return (input, path) => {
|
|
223
|
+
const checked = inner(input, path);
|
|
224
|
+
if (!checked.ok)
|
|
225
|
+
return checked;
|
|
226
|
+
const problem = test(checked.value);
|
|
227
|
+
return problem
|
|
228
|
+
? fail([...path, ...problem.path], { code: 'custom', message: problem.message }, false)
|
|
229
|
+
: checked;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Narrows a checked value to a branded contract type (Zod's `.brand()`). */
|
|
233
|
+
export function branded(inner) {
|
|
234
|
+
return inner;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The issues of a container, as Zod 3 orders them: the first one is reported,
|
|
238
|
+
* and the container is fatal when any part of it was.
|
|
239
|
+
*/
|
|
240
|
+
class Failures {
|
|
241
|
+
constructor(first) {
|
|
242
|
+
this.fatal = false;
|
|
243
|
+
this.first = first;
|
|
244
|
+
}
|
|
245
|
+
add(failure) {
|
|
246
|
+
this.first ?? (this.first = failure.issue);
|
|
247
|
+
this.fatal || (this.fatal = failure.fatal);
|
|
248
|
+
}
|
|
249
|
+
result() {
|
|
250
|
+
return this.first ? { ok: false, issue: this.first, fatal: this.fatal } : null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export function minItems(min) {
|
|
254
|
+
return {
|
|
255
|
+
test: (length) => length >= min,
|
|
256
|
+
code: 'too_small',
|
|
257
|
+
message: `Array must contain at least ${min} element(s)`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export function maxItems(max) {
|
|
261
|
+
return {
|
|
262
|
+
test: (length) => length <= max,
|
|
263
|
+
code: 'too_big',
|
|
264
|
+
message: `Array must contain at most ${max} element(s)`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/** Zod's `.length(n)`: `too_big` or `too_small`, by which side it missed. */
|
|
268
|
+
export function exactItems(count) {
|
|
269
|
+
const message = `Array must contain exactly ${count} element(s)`;
|
|
270
|
+
return [
|
|
271
|
+
{ test: (length) => length <= count, code: 'too_big', message },
|
|
272
|
+
{ test: (length) => length >= count, code: 'too_small', message },
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
export function array(inner, ...rules) {
|
|
276
|
+
const sizeRules = rules.flat();
|
|
277
|
+
return (input, path) => {
|
|
278
|
+
if (!Array.isArray(input))
|
|
279
|
+
return wrongType(path, 'array', input);
|
|
280
|
+
// Zod reports the size before any element, then every element in order.
|
|
281
|
+
const size = sizeRules.find((rule) => !rule.test(input.length));
|
|
282
|
+
const failures = new Failures(size ? { path, code: size.code, message: size.message } : null);
|
|
283
|
+
const out = [];
|
|
284
|
+
for (let index = 0; index < input.length; index++) {
|
|
285
|
+
const checked = inner(input[index], [...path, index]);
|
|
286
|
+
if (checked.ok)
|
|
287
|
+
out.push(checked.value);
|
|
288
|
+
else
|
|
289
|
+
failures.add(checked);
|
|
290
|
+
}
|
|
291
|
+
return failures.result() ?? ok(out);
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function objectWith(shape, unknownKeys) {
|
|
295
|
+
const keys = Object.keys(shape);
|
|
296
|
+
return (input, path) => {
|
|
297
|
+
if (!isRecord(input))
|
|
298
|
+
return wrongType(path, 'object', input);
|
|
299
|
+
const failures = new Failures(null);
|
|
300
|
+
const out = {};
|
|
301
|
+
for (const key of keys) {
|
|
302
|
+
const checked = shape[key](input[key], [...path, key]);
|
|
303
|
+
if (!checked.ok)
|
|
304
|
+
failures.add(checked);
|
|
305
|
+
// An optional key the input did not send stays absent rather than
|
|
306
|
+
// appearing as `key: undefined` — what Zod does, and what `in` sees.
|
|
307
|
+
else if (key !== '__proto__' && (checked.value !== undefined || key in input)) {
|
|
308
|
+
out[key] = checked.value;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (unknownKeys !== 'strip') {
|
|
312
|
+
const unknown = [];
|
|
313
|
+
for (const key in input) {
|
|
314
|
+
if (keys.includes(key))
|
|
315
|
+
continue;
|
|
316
|
+
unknown.push(key);
|
|
317
|
+
if (unknownKeys === 'passthrough' && key !== '__proto__' && input[key] !== undefined) {
|
|
318
|
+
out[key] = input[key];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (unknownKeys === 'strict' && unknown.length > 0) {
|
|
322
|
+
const message = `Unrecognized key(s) in object: ${listValues(unknown, ', ')}`;
|
|
323
|
+
failures.add({ issue: { path, code: 'unrecognized_keys', message }, fatal: false });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return failures.result() ?? ok(out);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** Unknown keys are dropped — Zod's `z.object(shape)`. */
|
|
330
|
+
export function object(shape) {
|
|
331
|
+
return objectWith(shape, 'strip');
|
|
332
|
+
}
|
|
333
|
+
/** Unknown keys fail — Zod's `z.object(shape).strict()`. */
|
|
334
|
+
export function strictObject(shape) {
|
|
335
|
+
return objectWith(shape, 'strict');
|
|
336
|
+
}
|
|
337
|
+
/** Unknown keys are kept as sent — Zod's `z.object(shape).passthrough()`. */
|
|
338
|
+
export function looseObject(shape) {
|
|
339
|
+
return objectWith(shape, 'passthrough');
|
|
340
|
+
}
|
|
341
|
+
/** Any object, every value kept — Zod's `z.record(z.unknown())`. */
|
|
342
|
+
export const unknownRecord = (input, path) => {
|
|
343
|
+
if (!isRecord(input))
|
|
344
|
+
return wrongType(path, 'object', input);
|
|
345
|
+
const out = {};
|
|
346
|
+
for (const key in input)
|
|
347
|
+
if (key !== '__proto__')
|
|
348
|
+
out[key] = input[key];
|
|
349
|
+
return ok(out);
|
|
350
|
+
};
|
|
351
|
+
/* -------------------------------------------------------------------------- */
|
|
352
|
+
/* Unions */
|
|
353
|
+
/* -------------------------------------------------------------------------- */
|
|
354
|
+
/** The first option that passes; else Zod 3's report (the first option that only failed a rule). */
|
|
355
|
+
export function union(...options) {
|
|
356
|
+
return (input, path) => {
|
|
357
|
+
let ruleFailure = null;
|
|
358
|
+
for (const option of options) {
|
|
359
|
+
const checked = option(input, path);
|
|
360
|
+
if (checked.ok)
|
|
361
|
+
return checked;
|
|
362
|
+
if (!checked.fatal)
|
|
363
|
+
ruleFailure ?? (ruleFailure = checked);
|
|
364
|
+
}
|
|
365
|
+
return ruleFailure ?? fail(path, { code: 'invalid_union', message: 'Invalid input' }, true);
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** Zod 3's `z.discriminatedUnion(key, options)`, with the options keyed by their tag. */
|
|
369
|
+
export function discriminatedUnion(key, options) {
|
|
370
|
+
return (input, path) => {
|
|
371
|
+
if (!isRecord(input))
|
|
372
|
+
return wrongType(path, 'object', input);
|
|
373
|
+
const tag = input[key];
|
|
374
|
+
const option = typeof tag === 'string' && Object.prototype.hasOwnProperty.call(options, tag)
|
|
375
|
+
? options[tag]
|
|
376
|
+
: undefined;
|
|
377
|
+
if (!option) {
|
|
378
|
+
const message = `Invalid discriminator value. Expected ${listValues(Object.keys(options))}`;
|
|
379
|
+
return fail([...path, key], { code: 'invalid_union_discriminator', message }, true);
|
|
380
|
+
}
|
|
381
|
+
return option(input, path);
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/* -------------------------------------------------------------------------- */
|
|
385
|
+
/* Using a check */
|
|
386
|
+
/* -------------------------------------------------------------------------- */
|
|
387
|
+
/** `a.b[0].c` — for messages. */
|
|
388
|
+
export function formatCheckPath(path) {
|
|
389
|
+
let out = '';
|
|
390
|
+
for (const part of path) {
|
|
391
|
+
out += typeof part === 'number' ? `[${part}]` : out ? `.${part}` : part;
|
|
392
|
+
}
|
|
393
|
+
return out || '(root)';
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* A value did not have the shape its contract requires. Thrown where a Zod
|
|
397
|
+
* schema's `.parse()` used to throw a `ZodError`; `issues` keeps that error's
|
|
398
|
+
* issue shape (`code`, `path`, `message`), holding the first issue Zod reports.
|
|
399
|
+
*/
|
|
400
|
+
export class ContractCheckError extends Error {
|
|
401
|
+
constructor(issue, context) {
|
|
402
|
+
const where = formatCheckPath(issue.path);
|
|
403
|
+
super(`${context ? `${context}: ` : ''}${where}: ${issue.message}`);
|
|
404
|
+
this.name = 'ContractCheckError';
|
|
405
|
+
this.issues = [issue];
|
|
406
|
+
Object.setPrototypeOf(this, ContractCheckError.prototype);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** The checked value, or `null` — the twin of `safeParseContract`. */
|
|
410
|
+
export function checkContract(check, input) {
|
|
411
|
+
const checked = check(input, []);
|
|
412
|
+
return checked.ok ? checked.value : null;
|
|
413
|
+
}
|
|
414
|
+
/** The checked value; throws {@link ContractCheckError} — the twin of a schema's `.parse()`. */
|
|
415
|
+
export function parseContract(check, input, context) {
|
|
416
|
+
const checked = check(input, []);
|
|
417
|
+
if (!checked.ok)
|
|
418
|
+
throw new ContractCheckError(checked.issue, context);
|
|
419
|
+
return checked.value;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* `exact<DeviceSessionState>()(check)` compiles only when `check` produces
|
|
423
|
+
* exactly `DeviceSessionState` — every key, every optional mark, every literal
|
|
424
|
+
* — so a contract type that changes without its check is a type error here.
|
|
425
|
+
*/
|
|
426
|
+
export function exact() {
|
|
427
|
+
return (check) => check;
|
|
428
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-party login result check, without Zod. Schemas: `../deviceBoot.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { array, exact, object, optional, string } from './check.js';
|
|
7
|
+
const securityAlert = object({
|
|
8
|
+
message: string(),
|
|
9
|
+
anomalies: array(object({ type: string(), reason: string(), details: optional(string()) })),
|
|
10
|
+
});
|
|
11
|
+
export const loginResultCheck = exact()(object({
|
|
12
|
+
sessionId: string(),
|
|
13
|
+
deviceId: string(),
|
|
14
|
+
expiresAt: string(),
|
|
15
|
+
accessToken: optional(string()),
|
|
16
|
+
deviceSecret: optional(string()),
|
|
17
|
+
securityAlert: optional(securityAlert),
|
|
18
|
+
user: object({ id: string(), username: optional(string()), avatar: optional(string()) }),
|
|
19
|
+
}));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-directory checks, without Zod. Schemas: `../deviceDirectory.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { ACCOUNT_KINDS } from './accountGraph.js';
|
|
7
|
+
import { array, boolean, exact, int, minLength, nonnegative, nullable, number, object, oneOf, optional, string, } from './check.js';
|
|
8
|
+
import { activeTokenCheck, deviceSessionStateCheck } from './deviceSession.js';
|
|
9
|
+
import { userNameCheck } from './userResponse.js';
|
|
10
|
+
const DEVICE_CONTEXT_RELATIONSHIPS = ['self', 'owner', 'member'];
|
|
11
|
+
const profile = object({
|
|
12
|
+
id: string(minLength(1)),
|
|
13
|
+
username: string(),
|
|
14
|
+
name: optional(userNameCheck),
|
|
15
|
+
avatar: optional(nullable(string())),
|
|
16
|
+
color: optional(nullable(string())),
|
|
17
|
+
});
|
|
18
|
+
const accountContext = object({
|
|
19
|
+
id: string(minLength(1)),
|
|
20
|
+
accountId: string(minLength(1)),
|
|
21
|
+
kind: oneOf(ACCOUNT_KINDS),
|
|
22
|
+
relationship: oneOf(DEVICE_CONTEXT_RELATIONSHIPS),
|
|
23
|
+
account: profile,
|
|
24
|
+
onDevice: boolean,
|
|
25
|
+
available: boolean,
|
|
26
|
+
active: boolean,
|
|
27
|
+
lastUsedAt: nullable(number()),
|
|
28
|
+
});
|
|
29
|
+
const principal = object({
|
|
30
|
+
id: string(minLength(1)),
|
|
31
|
+
userId: string(minLength(1)),
|
|
32
|
+
authuser: number(int, nonnegative),
|
|
33
|
+
user: profile,
|
|
34
|
+
contexts: array(accountContext),
|
|
35
|
+
});
|
|
36
|
+
export const deviceDirectoryCheck = exact()(object({
|
|
37
|
+
deviceId: string(minLength(1)),
|
|
38
|
+
revision: number(int, nonnegative),
|
|
39
|
+
activeContextId: nullable(string()),
|
|
40
|
+
principals: array(principal),
|
|
41
|
+
updatedAt: number(),
|
|
42
|
+
}));
|
|
43
|
+
export const deviceActivateResponseCheck = exact()(object({ directory: deviceDirectoryCheck, activeToken: nullable(activeTokenCheck) }));
|
|
44
|
+
export const deviceDirectorySyncCheck = exact()(object({
|
|
45
|
+
directory: deviceDirectoryCheck,
|
|
46
|
+
state: deviceSessionStateCheck,
|
|
47
|
+
activeToken: nullable(activeTokenCheck),
|
|
48
|
+
}));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-session checks, without Zod. Schemas: `../deviceSession.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { array, exact, int, minLength, nonnegative, nullable, number, object, oneOf, optional, positive, string, } from './check.js';
|
|
7
|
+
/* -------------------------------------------------------------------------- */
|
|
8
|
+
/* Instant cross-app session sync (token-free socket signal) */
|
|
9
|
+
/* -------------------------------------------------------------------------- */
|
|
10
|
+
/**
|
|
11
|
+
* Name of the token-free Socket.IO event emitted to room `user:<userId>` on
|
|
12
|
+
* every DeviceSession mutation that changes what is signed in for that user.
|
|
13
|
+
*
|
|
14
|
+
* This is a pure SIGNAL — it carries NO access token, NO deviceSecret and NO
|
|
15
|
+
* account bodies. A client that receives it re-fetches its authenticated
|
|
16
|
+
* session/account state (`GET /session/device/state`, `GET /accounts`). Unlike
|
|
17
|
+
* `session_state` (scoped to `device:<deviceId>`, i.e. a single origin), this
|
|
18
|
+
* reaches ALL of a user's connected sockets across their devices/origins so
|
|
19
|
+
* every Oxy app reflects an add / switch / signout instantly.
|
|
20
|
+
*/
|
|
21
|
+
export const SESSION_ACCOUNTS_CHANGED_EVENT = 'session_accounts_changed';
|
|
22
|
+
/* -------------------------------------------------------------------------- */
|
|
23
|
+
/* Response checks */
|
|
24
|
+
/* -------------------------------------------------------------------------- */
|
|
25
|
+
const SESSION_ACCOUNTS_CHANGED_REASONS = ['login', 'add', 'switch', 'signout', 'revoke'];
|
|
26
|
+
const sessionAccount = object({
|
|
27
|
+
accountId: string(),
|
|
28
|
+
sessionId: string(),
|
|
29
|
+
authuser: number(int, nonnegative),
|
|
30
|
+
operatedByUserId: optional(string()),
|
|
31
|
+
});
|
|
32
|
+
export const deviceSessionStateCheck = exact()(object({
|
|
33
|
+
deviceId: string(),
|
|
34
|
+
accounts: array(sessionAccount),
|
|
35
|
+
activeAccountId: nullable(string()),
|
|
36
|
+
revision: number(int, nonnegative),
|
|
37
|
+
updatedAt: number(),
|
|
38
|
+
}));
|
|
39
|
+
export const activeTokenCheck = exact()(object({ accessToken: string(), expiresAt: string() }));
|
|
40
|
+
export const deviceSessionSyncCheck = exact()(object({ state: deviceSessionStateCheck, activeToken: nullable(activeTokenCheck) }));
|
|
41
|
+
export const deviceTokenMintResponseCheck = exact()(object({
|
|
42
|
+
accessToken: string(),
|
|
43
|
+
expiresAt: string(),
|
|
44
|
+
nextDeviceSecret: string(),
|
|
45
|
+
state: deviceSessionStateCheck,
|
|
46
|
+
}));
|
|
47
|
+
export const sessionAccountsChangedEventCheck = exact()(object({
|
|
48
|
+
userId: string(),
|
|
49
|
+
revision: number(int, nonnegative),
|
|
50
|
+
reason: oneOf(SESSION_ACCOUNTS_CHANGED_REASONS),
|
|
51
|
+
}));
|
|
52
|
+
export const deviceBackgroundCredentialResponseCheck = exact()(object({
|
|
53
|
+
deviceId: string(minLength(1)),
|
|
54
|
+
secret: string(minLength(1)),
|
|
55
|
+
accountId: string(minLength(1)),
|
|
56
|
+
expiresAt: string(),
|
|
57
|
+
}));
|
|
58
|
+
export const deviceRegisterResponseCheck = exact()(object({ deviceId: string(minLength(1)), deviceSecret: string(minLength(1)) }));
|
|
59
|
+
export const deviceJoinResponseCheck = exact()(deviceRegisterResponseCheck);
|
|
60
|
+
export const deviceJoinCodeResponseCheck = exact()(object({ code: string(minLength(1)), expiresIn: number(int, positive) }));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The external-identity reference check, without Zod. Schemas: `../externalIdentity.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { exact, minLength, object, oneOf, string } from './check.js';
|
|
7
|
+
const required = string(minLength(1));
|
|
8
|
+
/** One Oxy-proven network alias of a profile — the twin of `externalIdentityReferenceSchema`. */
|
|
9
|
+
export const externalIdentityReferenceCheck = exact()(object({
|
|
10
|
+
canonicalAcct: required,
|
|
11
|
+
network: required,
|
|
12
|
+
protocol: oneOf(['activitypub', 'atproto', 'instagram-graph']),
|
|
13
|
+
actorUri: required,
|
|
14
|
+
transportAcct: required,
|
|
15
|
+
sourceUserId: required,
|
|
16
|
+
}));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-record envelope check, without Zod. Schemas: `../identity.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { exact, int, literal, minLength, nonnegative, nullable, number, object, optional, refineAt, string, union, unknownRecord, } from './check.js';
|
|
7
|
+
const CHAIN_FIELDS = ['seq', 'prev', 'collection', 'rkey'];
|
|
8
|
+
/** The twin of `signedRecordEnvelopeSchema`: v2 requires the chain fields, v1 forbids them. */
|
|
9
|
+
export const signedRecordEnvelopeCheck = exact()(refineAt(object({
|
|
10
|
+
version: union(literal(1), literal(2)),
|
|
11
|
+
type: string(minLength(1)),
|
|
12
|
+
subject: string(),
|
|
13
|
+
issuer: string(),
|
|
14
|
+
record: unknownRecord,
|
|
15
|
+
issuedAt: number(),
|
|
16
|
+
seq: optional(number(int, nonnegative)),
|
|
17
|
+
prev: optional(nullable(string())),
|
|
18
|
+
collection: optional(string(minLength(1))),
|
|
19
|
+
rkey: optional(string(minLength(1))),
|
|
20
|
+
publicKey: string(),
|
|
21
|
+
alg: literal('ES256K-DER-SHA256'),
|
|
22
|
+
signature: string(),
|
|
23
|
+
}), (env) => {
|
|
24
|
+
if (env.version === 2) {
|
|
25
|
+
if (typeof env.seq !== 'number')
|
|
26
|
+
return { path: ['seq'], message: 'v2 envelope requires `seq`' };
|
|
27
|
+
if (env.prev === undefined) {
|
|
28
|
+
return { path: ['prev'], message: 'v2 envelope requires `prev` (use `null` at genesis)' };
|
|
29
|
+
}
|
|
30
|
+
if (typeof env.collection !== 'string') {
|
|
31
|
+
return { path: ['collection'], message: 'v2 envelope requires `collection`' };
|
|
32
|
+
}
|
|
33
|
+
if (typeof env.rkey !== 'string') {
|
|
34
|
+
return { path: ['rkey'], message: 'v2 envelope requires `rkey`' };
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const carried = CHAIN_FIELDS.find((field) => env[field] !== undefined);
|
|
39
|
+
return carried
|
|
40
|
+
? { path: [carried], message: `v1 envelope must not carry \`${carried}\`` }
|
|
41
|
+
: undefined;
|
|
42
|
+
}));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-link constants, QR payload and response checks, without Zod. Schemas: `../identityLink.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Part of `@oxy.so/contracts/client`: never import `zod` here (see `./check.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { exact, int, minLength, nullable, number, object, oneOf, pattern, positive, startsWith, string, trim, } from './check.js';
|
|
7
|
+
export const IDENTITY_LINK_STATUSES = ['pending', 'signed', 'completed', 'cancelled'];
|
|
8
|
+
/** The scheme and host Commons routes a link QR to. */
|
|
9
|
+
export const IDENTITY_LINK_QR_PREFIX = 'oxycommons://link';
|
|
10
|
+
export const LINK_ID = /^[0-9a-f]{32}$/;
|
|
11
|
+
export const CHALLENGE = /^[0-9a-f]{64}$/;
|
|
12
|
+
/** The QR auth.oxy.so shows: the request's id and the challenge Commons signs. */
|
|
13
|
+
export function buildIdentityLinkQrPayload(linkId, challenge) {
|
|
14
|
+
return `${IDENTITY_LINK_QR_PREFIX}?id=${linkId}&c=${challenge}`;
|
|
15
|
+
}
|
|
16
|
+
/** The request a scanned code names, or `null` for anything that is not a link QR. */
|
|
17
|
+
export function parseIdentityLinkQrPayload(raw) {
|
|
18
|
+
const value = raw.trim();
|
|
19
|
+
if (!value.startsWith(`${IDENTITY_LINK_QR_PREFIX}?`))
|
|
20
|
+
return null;
|
|
21
|
+
// Parsed by hand: React Native's `URLSearchParams` does not implement `get`.
|
|
22
|
+
const params = new Map();
|
|
23
|
+
for (const pair of value.slice(IDENTITY_LINK_QR_PREFIX.length + 1).split('&')) {
|
|
24
|
+
const separator = pair.indexOf('=');
|
|
25
|
+
if (separator > 0)
|
|
26
|
+
params.set(pair.slice(0, separator), pair.slice(separator + 1));
|
|
27
|
+
}
|
|
28
|
+
const linkId = params.get('id') ?? '';
|
|
29
|
+
const challenge = params.get('c') ?? '';
|
|
30
|
+
if (!LINK_ID.test(linkId) || !CHALLENGE.test(challenge))
|
|
31
|
+
return null;
|
|
32
|
+
return { linkId, challenge };
|
|
33
|
+
}
|
|
34
|
+
/* -------------------------------------------------------------------------- */
|
|
35
|
+
/* Checks */
|
|
36
|
+
/* -------------------------------------------------------------------------- */
|
|
37
|
+
export const identityLinkCreateResponseCheck = exact()(object({
|
|
38
|
+
linkId: string(trim, pattern(LINK_ID, 'linkId must be 32 lowercase hex characters')),
|
|
39
|
+
challenge: string(pattern(CHALLENGE)),
|
|
40
|
+
expiresAt: number(int, positive),
|
|
41
|
+
qrPayload: string(startsWith(IDENTITY_LINK_QR_PREFIX)),
|
|
42
|
+
}));
|
|
43
|
+
export const identityLinkStateCheck = exact()(object({
|
|
44
|
+
status: oneOf(IDENTITY_LINK_STATUSES),
|
|
45
|
+
userId: string(minLength(1)),
|
|
46
|
+
username: nullable(string()),
|
|
47
|
+
publicKey: nullable(string()),
|
|
48
|
+
audience: string(minLength(1)),
|
|
49
|
+
expiresAt: number(int, positive),
|
|
50
|
+
}));
|