@tula-tech/contract 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,386 @@
1
+ /**
2
+ * The error codes of the public contract, as plain data.
3
+ *
4
+ * This module has no dependencies (no Zod), so an SDK can import it from
5
+ * `@tula-tech/contract/error-codes` without adding a schema library to an application's bundle. The
6
+ * Zod schemas built on these codes live in `./errors`.
7
+ */
8
+ /** Definition of one contract error code: its HTTP status and default (English) message. */
9
+ interface ErrorDefinition {
10
+ /** HTTP status the API responds with for this code. */
11
+ readonly status: number;
12
+ /** Default human-readable message; SDKs may localize by `code`. */
13
+ readonly message: string;
14
+ }
15
+ /**
16
+ * Every machine-readable error code the API can return, keyed `area.reason`.
17
+ *
18
+ * Codes are part of the public contract: SDKs switch on them and translate them, so they are
19
+ * append-only. Removing or renaming one is a breaking change.
20
+ *
21
+ * Sign-up deliberately has no "email already taken" code: an existing address still receives
22
+ * `needs_email_verification` (and a notice email) so sign-up can't be used to enumerate accounts.
23
+ */
24
+ declare const ERROR_DEFINITIONS: {
25
+ readonly "auth.invalid_credentials": {
26
+ readonly status: 401;
27
+ readonly message: "The email or password is incorrect.";
28
+ };
29
+ readonly "auth.unauthenticated": {
30
+ readonly status: 401;
31
+ readonly message: "You need to sign in to do that.";
32
+ };
33
+ readonly "auth.invalid_key": {
34
+ readonly status: 401;
35
+ readonly message: "The API key is missing, invalid or revoked.";
36
+ };
37
+ readonly "auth.forbidden": {
38
+ readonly status: 403;
39
+ readonly message: "You do not have permission to do that.";
40
+ };
41
+ readonly "auth.user_banned": {
42
+ readonly status: 403;
43
+ readonly message: "This account has been disabled.";
44
+ };
45
+ readonly "auth.method_disabled": {
46
+ readonly status: 403;
47
+ readonly message: "This sign-in method is not available.";
48
+ };
49
+ readonly "auth.step_up_required": {
50
+ readonly status: 403;
51
+ readonly message: "Confirm it is you to continue.";
52
+ };
53
+ readonly "flow.not_found": {
54
+ readonly status: 404;
55
+ readonly message: "This attempt does not exist or has expired.";
56
+ };
57
+ readonly "flow.invalid_step": {
58
+ readonly status: 409;
59
+ readonly message: "That action is not valid at this step. Please start again.";
60
+ };
61
+ readonly "email.invalid": {
62
+ readonly status: 422;
63
+ readonly message: "Enter a valid email address.";
64
+ };
65
+ readonly "email.already_set": {
66
+ readonly status: 409;
67
+ readonly message: "This account already has an email address.";
68
+ };
69
+ readonly "password.too_short": {
70
+ readonly status: 422;
71
+ readonly message: "Password is too short.";
72
+ };
73
+ readonly "password.too_long": {
74
+ readonly status: 422;
75
+ readonly message: "Password is too long.";
76
+ };
77
+ readonly "password.missing_lowercase": {
78
+ readonly status: 422;
79
+ readonly message: "Add a lowercase letter.";
80
+ };
81
+ readonly "password.missing_uppercase": {
82
+ readonly status: 422;
83
+ readonly message: "Add an uppercase letter.";
84
+ };
85
+ readonly "password.missing_number": {
86
+ readonly status: 422;
87
+ readonly message: "Add a number.";
88
+ };
89
+ readonly "password.missing_special": {
90
+ readonly status: 422;
91
+ readonly message: "Add a special character.";
92
+ };
93
+ readonly "password.too_few_character_classes": {
94
+ readonly status: 422;
95
+ readonly message: "Use a wider mix of letters, numbers and symbols.";
96
+ };
97
+ readonly "password.contains_user_info": {
98
+ readonly status: 422;
99
+ readonly message: "Password must not contain your name or email.";
100
+ };
101
+ readonly "password.common": {
102
+ readonly status: 422;
103
+ readonly message: "This password is too common.";
104
+ };
105
+ readonly "password.breached": {
106
+ readonly status: 422;
107
+ readonly message: "This password appeared in a data breach. Choose a different one.";
108
+ };
109
+ readonly "password.repeated_characters": {
110
+ readonly status: 422;
111
+ readonly message: "Avoid repeating the same character.";
112
+ };
113
+ readonly "password.sequence": {
114
+ readonly status: 422;
115
+ readonly message: "Avoid sequences like \"abcd\" or \"1234\".";
116
+ };
117
+ readonly "password.reused": {
118
+ readonly status: 422;
119
+ readonly message: "You have used this password recently. Choose a different one.";
120
+ };
121
+ readonly "password.not_set": {
122
+ readonly status: 409;
123
+ readonly message: "This account has no password yet. Use \"forgot password\" to set one.";
124
+ };
125
+ readonly "verification.invalid_code": {
126
+ readonly status: 422;
127
+ readonly message: "That code is incorrect.";
128
+ };
129
+ readonly "verification.expired": {
130
+ readonly status: 410;
131
+ readonly message: "That code has expired. Request a new one.";
132
+ };
133
+ readonly "verification.too_many_attempts": {
134
+ readonly status: 429;
135
+ readonly message: "Too many incorrect codes. Request a new one.";
136
+ };
137
+ readonly "verification.different_browser": {
138
+ readonly status: 409;
139
+ readonly message: "Open this link in the browser where you started signing in, or enter the code from the email there.";
140
+ };
141
+ readonly "mfa.invalid_code": {
142
+ readonly status: 422;
143
+ readonly message: "That code is incorrect.";
144
+ };
145
+ readonly "mfa.already_enabled": {
146
+ readonly status: 409;
147
+ readonly message: "Two-step verification is already on for this account.";
148
+ };
149
+ readonly "mfa.not_enabled": {
150
+ readonly status: 409;
151
+ readonly message: "Two-step verification is not on for this account.";
152
+ };
153
+ readonly "mfa.enrolment_expired": {
154
+ readonly status: 410;
155
+ readonly message: "This setup has expired. Start again.";
156
+ };
157
+ readonly "mfa.not_available": {
158
+ readonly status: 403;
159
+ readonly message: "Two-step verification is not available for this app.";
160
+ };
161
+ readonly "mfa.enrolment_needs_other_sign_in": {
162
+ readonly status: 403;
163
+ readonly message: "Sign in another way to set up two-step verification.";
164
+ };
165
+ readonly "mfa.needs_other_sign_in": {
166
+ readonly status: 403;
167
+ readonly message: "Sign in another way. A texted code cannot be both steps.";
168
+ };
169
+ readonly "mfa.sms_not_allowed": {
170
+ readonly status: 409;
171
+ readonly message: "This account already has a stronger second step than a texted code.";
172
+ };
173
+ readonly "mfa.phone_number_required": {
174
+ readonly status: 409;
175
+ readonly message: "Add a phone number to your account first.";
176
+ };
177
+ readonly "mfa.required_by_policy": {
178
+ readonly status: 403;
179
+ readonly message: "This app requires two-step verification. It cannot be turned off.";
180
+ };
181
+ readonly "oauth.access_denied": {
182
+ readonly status: 403;
183
+ readonly message: "Sign-in was cancelled. Try again or choose another way to sign in.";
184
+ };
185
+ readonly "oauth.provider_error": {
186
+ readonly status: 502;
187
+ readonly message: "The sign-in provider could not complete the request. Try again.";
188
+ };
189
+ readonly "oauth.state_invalid": {
190
+ readonly status: 400;
191
+ readonly message: "This sign-in could not be completed. Start again.";
192
+ };
193
+ readonly "oauth.ticket_invalid": {
194
+ readonly status: 410;
195
+ readonly message: "This sign-in has expired. Start again.";
196
+ };
197
+ readonly "oauth.different_browser": {
198
+ readonly status: 409;
199
+ readonly message: "Finish signing in in the browser where you started, or start again here.";
200
+ };
201
+ readonly "oauth.email_missing": {
202
+ readonly status: 403;
203
+ readonly message: "This provider did not share an email address. Allow access to your email and try again.";
204
+ };
205
+ readonly "oauth.email_unverified": {
206
+ readonly status: 403;
207
+ readonly message: "Your email address is not verified with this provider. Verify it there and try again.";
208
+ };
209
+ readonly "oauth.account_exists": {
210
+ readonly status: 409;
211
+ readonly message: "An account with this email already exists. Sign in the way you usually do, then connect this provider from your account.";
212
+ };
213
+ readonly "oauth.identity_in_use": {
214
+ readonly status: 409;
215
+ readonly message: "This provider account is already connected to another user.";
216
+ };
217
+ readonly "oauth.already_linked": {
218
+ readonly status: 409;
219
+ readonly message: "An account of this provider is already connected. Disconnect it first.";
220
+ };
221
+ readonly "identity.last_sign_in_method": {
222
+ readonly status: 409;
223
+ readonly message: "This is your only way to sign in. Add a password or connect another account first.";
224
+ };
225
+ readonly "webhook.url_not_allowed": {
226
+ readonly status: 422;
227
+ readonly message: "The server cannot deliver to that address.";
228
+ };
229
+ readonly "webhook.cannot_redeliver": {
230
+ readonly status: 409;
231
+ readonly message: "This delivery cannot be sent again.";
232
+ };
233
+ readonly "webhook.rotation_refused": {
234
+ readonly status: 409;
235
+ readonly message: "The signing secret cannot be changed now.";
236
+ };
237
+ readonly "hook.denied": {
238
+ readonly status: 403;
239
+ readonly message: "This was not allowed.";
240
+ };
241
+ readonly "hook.unavailable": {
242
+ readonly status: 503;
243
+ readonly message: "This is unavailable right now. Try again later.";
244
+ };
245
+ readonly "hook.url_not_allowed": {
246
+ readonly status: 422;
247
+ readonly message: "The server cannot call that address.";
248
+ };
249
+ readonly "passkey.registration_failed": {
250
+ readonly status: 422;
251
+ readonly message: "That passkey could not be saved. Try again.";
252
+ };
253
+ readonly "passkey.already_registered": {
254
+ readonly status: 409;
255
+ readonly message: "This passkey is already saved to an account.";
256
+ };
257
+ readonly "passkey.limit_reached": {
258
+ readonly status: 409;
259
+ readonly message: "You have reached the number of passkeys an account can have. Remove one first.";
260
+ };
261
+ readonly "passkey.last_sign_in_method": {
262
+ readonly status: 409;
263
+ readonly message: "This is your only way to sign in. Add a password or connect an account first.";
264
+ };
265
+ readonly "phone.invalid": {
266
+ readonly status: 422;
267
+ readonly message: "Enter a phone number with its country code, such as +14155550100.";
268
+ };
269
+ readonly "sms.disabled": {
270
+ readonly status: 403;
271
+ readonly message: "Text messages are not available.";
272
+ };
273
+ readonly "sms.country_not_allowed": {
274
+ readonly status: 422;
275
+ readonly message: "Text messages cannot be sent to that country.";
276
+ };
277
+ readonly "sms.unavailable": {
278
+ readonly status: 503;
279
+ readonly message: "The text message could not be sent. Try again later.";
280
+ };
281
+ readonly "session.invalid_token": {
282
+ readonly status: 401;
283
+ readonly message: "Your session is invalid. Sign in again.";
284
+ };
285
+ readonly "session.expired": {
286
+ readonly status: 401;
287
+ readonly message: "Your session has expired. Sign in again.";
288
+ };
289
+ readonly "session.revoked": {
290
+ readonly status: 401;
291
+ readonly message: "Your session was signed out. Sign in again.";
292
+ };
293
+ readonly "session.reuse_detected": {
294
+ readonly status: 401;
295
+ readonly message: "For your security this session was signed out. Sign in again.";
296
+ };
297
+ readonly "session.limit_reached": {
298
+ readonly status: 403;
299
+ readonly message: "You are signed in on too many devices. Sign out on another device, or reset your password to sign out everywhere, then try again.";
300
+ };
301
+ readonly "device.proof_invalid": {
302
+ readonly status: 401;
303
+ readonly message: "This session is bound to a device key, and the request did not prove that key.";
304
+ };
305
+ readonly "device.nonce_required": {
306
+ readonly status: 400;
307
+ readonly message: "The proof needs a fresh nonce. Send the request again with the nonce provided.";
308
+ };
309
+ readonly "device.binding_not_supported": {
310
+ readonly status: 400;
311
+ readonly message: "A session of this kind of client cannot be bound to a device key.";
312
+ };
313
+ readonly "device.binding_required": {
314
+ readonly status: 400;
315
+ readonly message: "Signing in here needs a device key.";
316
+ };
317
+ readonly rate_limited: {
318
+ readonly status: 429;
319
+ readonly message: "Too many requests. Try again shortly.";
320
+ };
321
+ readonly "request.malformed": {
322
+ readonly status: 400;
323
+ readonly message: "The request could not be read.";
324
+ };
325
+ readonly "request.too_large": {
326
+ readonly status: 413;
327
+ readonly message: "The request body is too large.";
328
+ };
329
+ readonly "request.origin_not_allowed": {
330
+ readonly status: 403;
331
+ readonly message: "This origin is not allowed to sign in to this app.";
332
+ };
333
+ readonly "request.redirect_not_allowed": {
334
+ readonly status: 400;
335
+ readonly message: "This redirect URL is not allowed for this app.";
336
+ };
337
+ readonly "validation.failed": {
338
+ readonly status: 422;
339
+ readonly message: "Some fields are invalid.";
340
+ };
341
+ readonly "resource.not_found": {
342
+ readonly status: 404;
343
+ readonly message: "The requested resource does not exist.";
344
+ };
345
+ readonly "resource.conflict": {
346
+ readonly status: 409;
347
+ readonly message: "The resource conflicts with existing data.";
348
+ };
349
+ readonly "precondition.required": {
350
+ readonly status: 428;
351
+ readonly message: "Send the revision you are changing in an If-Match header.";
352
+ };
353
+ readonly "precondition.failed": {
354
+ readonly status: 412;
355
+ readonly message: "The resource changed since you read it. Read it again and retry.";
356
+ };
357
+ readonly not_implemented: {
358
+ readonly status: 501;
359
+ readonly message: "This capability is not available yet.";
360
+ };
361
+ readonly "service.unavailable": {
362
+ readonly status: 503;
363
+ readonly message: "The service is temporarily unavailable. Try again shortly.";
364
+ };
365
+ readonly internal: {
366
+ readonly status: 500;
367
+ readonly message: "Something went wrong on our side.";
368
+ };
369
+ };
370
+ /** A stable, machine-readable error code (e.g. `password.too_short`). */
371
+ type ErrorCode = keyof typeof ERROR_DEFINITIONS;
372
+ /** All error codes, for building enums in generated clients. */
373
+ declare const ERROR_CODES: [ErrorCode, ...ErrorCode[]];
374
+ /**
375
+ * Look up the HTTP status and default message for an error code.
376
+ *
377
+ * @param code - The contract error code.
378
+ * @returns Its definition.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * errorDefinition('password.too_short').status // 422
383
+ * ```
384
+ */
385
+ declare function errorDefinition(code: ErrorCode): ErrorDefinition;
386
+ export { ERROR_CODES, ERROR_DEFINITIONS, ErrorCode, ErrorDefinition, errorDefinition };
@@ -0,0 +1,10 @@
1
+ import {
2
+ ERROR_DEFINITIONS2,
3
+ ERROR_CODES2,
4
+ errorDefinition2
5
+ } from "./shared/chunk-cce3bhet.js";
6
+ export {
7
+ ERROR_CODES2 as ERROR_CODES,
8
+ ERROR_DEFINITIONS2 as ERROR_DEFINITIONS,
9
+ errorDefinition2 as errorDefinition
10
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Everything the API records. Each entry is written to the event outbox (for webhooks) and to
3
+ * the audit log, in the same database transaction as the change it describes.
4
+ *
5
+ * A session ends with exactly one of `session.revoked` (its `reason` says why) or
6
+ * `session.reuse_detected` (a rotated refresh token was replayed, so the session was revoked as
7
+ * possibly stolen).
8
+ *
9
+ * A new type needs three things, and a test fails for each one that is missing: its name here
10
+ * (and in {@link EVENT_TARGET_TYPES}), a `data` schema in `EVENT_DATA_SCHEMAS` and an example
11
+ * in `EVENT_FIXTURES`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { ACTIVITY_TYPES, type ActivityType } from '@tula-tech/contract/event-types'
16
+ *
17
+ * function isKnown(type: string): type is ActivityType {
18
+ * return (ACTIVITY_TYPES as readonly string[]).includes(type)
19
+ * }
20
+ * ```
21
+ */
22
+ declare const ACTIVITY_TYPES: readonly ["user.created", "user.email_verified", "user.banned", "user.unbanned", "user.deleted", "user.password_changed", "user.mfa_enabled", "user.mfa_disabled", "user.backup_codes_regenerated", "user.backup_code_used", "user.identity_linked", "user.identity_unlinked", "user.email_added", "user.passkey_added", "user.passkey_renamed", "user.passkey_removed", "user.passkey_counter_regressed", "user.phone_number_added", "user.phone_number_removed", "user.sms_factor_enabled", "user.sms_factor_removed", "session.created", "session.revoked", "session.reuse_detected", "session.stepped_up", "session.refresh_proof_refused", "api_key.created", "api_key.revoked", "signing_key.rotated", "environment.settings_updated", "oauth_provider.updated", "oauth_provider.deleted", "webhook_endpoint.created", "webhook_endpoint.updated", "webhook_endpoint.deleted", "webhook_endpoint.disabled", "webhook_endpoint.secret_rotated", "webhook_endpoint.previous_secret_revoked", "hook.created", "hook.updated", "hook.deleted", "native_app.created", "native_app.updated", "native_app.deleted", "proposal.created", "proposal.approved", "proposal.executed", "proposal.failed", "sign_in_test.started", "sign_in_test.finished"];
23
+ /** A recorded action type: one of {@link ACTIVITY_TYPES}. */
24
+ type ActivityType = (typeof ACTIVITY_TYPES)[number];
25
+ /**
26
+ * The version of the event payloads: the `schemaVersion` of every event.
27
+ *
28
+ * Within a version a payload only grows: a later server may add an event type, a field or an
29
+ * enum value, so a receiver ignores what it does not know. Removing or renaming a field, or
30
+ * changing what one means, is a new version.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * if (event.schemaVersion !== EVENT_SCHEMA_VERSION) {
35
+ * // Written by a server with a newer payload format than this code was built for.
36
+ * }
37
+ * ```
38
+ */
39
+ declare const EVENT_SCHEMA_VERSION = 1;
40
+ /**
41
+ * What each event is about: the `target.type` of its payload. `target.id` is that thing's id.
42
+ *
43
+ * An event about a user's credentials targets the `user` (the passkey or session concerned
44
+ * is named in `data`); an OAuth provider's credentials belong to the `environment`. A webhook
45
+ * endpoint has an id of its own, as an API key does, so it is its own kind of target, and so
46
+ * are a hook, a native app and a proposal (what a proposal would change is in its `data`).
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * EVENT_TARGET_TYPES['session.revoked'] // 'session'
51
+ * ```
52
+ */
53
+ declare const EVENT_TARGET_TYPES: {
54
+ readonly "user.created": "user";
55
+ readonly "user.email_verified": "user";
56
+ readonly "user.banned": "user";
57
+ readonly "user.unbanned": "user";
58
+ readonly "user.deleted": "user";
59
+ readonly "user.password_changed": "user";
60
+ readonly "user.mfa_enabled": "user";
61
+ readonly "user.mfa_disabled": "user";
62
+ readonly "user.backup_codes_regenerated": "user";
63
+ readonly "user.backup_code_used": "user";
64
+ readonly "user.identity_linked": "user";
65
+ readonly "user.email_added": "user";
66
+ readonly "user.identity_unlinked": "user";
67
+ readonly "user.passkey_added": "user";
68
+ readonly "user.passkey_renamed": "user";
69
+ readonly "user.passkey_removed": "user";
70
+ readonly "user.passkey_counter_regressed": "user";
71
+ readonly "user.phone_number_added": "user";
72
+ readonly "user.phone_number_removed": "user";
73
+ readonly "user.sms_factor_enabled": "user";
74
+ readonly "user.sms_factor_removed": "user";
75
+ readonly "session.created": "session";
76
+ readonly "session.revoked": "session";
77
+ readonly "session.reuse_detected": "session";
78
+ readonly "session.stepped_up": "session";
79
+ readonly "session.refresh_proof_refused": "session";
80
+ readonly "api_key.created": "api_key";
81
+ readonly "api_key.revoked": "api_key";
82
+ readonly "signing_key.rotated": "signing_key";
83
+ readonly "environment.settings_updated": "environment";
84
+ readonly "oauth_provider.updated": "environment";
85
+ readonly "oauth_provider.deleted": "environment";
86
+ readonly "webhook_endpoint.created": "webhook_endpoint";
87
+ readonly "webhook_endpoint.updated": "webhook_endpoint";
88
+ readonly "webhook_endpoint.deleted": "webhook_endpoint";
89
+ readonly "webhook_endpoint.disabled": "webhook_endpoint";
90
+ readonly "webhook_endpoint.secret_rotated": "webhook_endpoint";
91
+ readonly "webhook_endpoint.previous_secret_revoked": "webhook_endpoint";
92
+ readonly "hook.created": "hook";
93
+ readonly "hook.updated": "hook";
94
+ readonly "hook.deleted": "hook";
95
+ readonly "native_app.created": "native_app";
96
+ readonly "native_app.updated": "native_app";
97
+ readonly "native_app.deleted": "native_app";
98
+ readonly "proposal.created": "proposal";
99
+ readonly "proposal.approved": "proposal";
100
+ readonly "proposal.executed": "proposal";
101
+ readonly "proposal.failed": "proposal";
102
+ readonly "sign_in_test.started": "environment";
103
+ readonly "sign_in_test.finished": "environment";
104
+ };
105
+ /** What an event of type `T` is about. */
106
+ type EventTargetType<T extends ActivityType = ActivityType> = (typeof EVENT_TARGET_TYPES)[T];
107
+ export { ACTIVITY_TYPES, ActivityType, EVENT_SCHEMA_VERSION, EVENT_TARGET_TYPES, EventTargetType };
@@ -0,0 +1,10 @@
1
+ import {
2
+ ACTIVITY_TYPES2,
3
+ EVENT_SCHEMA_VERSION2,
4
+ EVENT_TARGET_TYPES2
5
+ } from "./shared/chunk-7yqzghgm.js";
6
+ export {
7
+ ACTIVITY_TYPES2 as ACTIVITY_TYPES,
8
+ EVENT_SCHEMA_VERSION2 as EVENT_SCHEMA_VERSION,
9
+ EVENT_TARGET_TYPES2 as EVENT_TARGET_TYPES
10
+ };