@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.
- package/LICENSE +202 -0
- package/README.md +37 -2
- package/dist/custom-claims.d.ts +161 -0
- package/dist/custom-claims.js +28 -0
- package/dist/device-binding.d.ts +217 -0
- package/dist/device-binding.js +24 -0
- package/dist/error-codes.d.ts +386 -0
- package/dist/error-codes.js +10 -0
- package/dist/event-types.d.ts +107 -0
- package/dist/event-types.js +10 -0
- package/dist/headers.d.ts +223 -0
- package/dist/headers.js +36 -0
- package/dist/index.d.ts +10384 -0
- package/dist/index.js +3912 -0
- package/dist/issuer.d.ts +31 -0
- package/dist/issuer.js +8 -0
- package/dist/password-rules.d.ts +452 -0
- package/dist/password-rules.js +10 -0
- package/dist/redirect-url.d.ts +223 -0
- package/dist/redirect-url.js +26 -0
- package/dist/shared/chunk-2xak18qy.js +56 -0
- package/dist/shared/chunk-4rb5jsfz.js +102 -0
- package/dist/shared/chunk-4stpmame.js +58 -0
- package/dist/shared/chunk-7yqzghgm.js +108 -0
- package/dist/shared/chunk-9pvdavrw.js +9 -0
- package/dist/shared/chunk-cce3bhet.js +251 -0
- package/dist/shared/chunk-pcn6ax54.js +195 -0
- package/dist/shared/chunk-q309e9bk.js +19 -0
- package/dist/shared/chunk-r202nxfw.js +438 -0
- package/dist/shared/chunk-sc6qg3d2.js +219 -0
- package/dist/theme.d.ts +227 -0
- package/dist/theme.js +16 -0
- package/dist/webhook-signature.d.ts +103 -0
- package/dist/webhook-signature.js +22 -0
- package/openapi.json +30076 -0
- package/package.json +79 -4
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schemes that are never a custom-scheme redirect, whatever follows the colon: the two web
|
|
3
|
+
* schemes (which have rules of their own) and the ones a browser or an operating system
|
|
4
|
+
* handles itself, where a redirect would run script, open a file, start a call or hand the
|
|
5
|
+
* URL to something that is nobody's app.
|
|
6
|
+
*
|
|
7
|
+
* **Best effort, and no more.** The entries without a full stop are also refused by the
|
|
8
|
+
* rule that a custom scheme contains one; the ones with a full stop, and the families of
|
|
9
|
+
* {@link REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM}, are refused by this list alone, and a
|
|
10
|
+
* scheme an operating system handles that nobody put here passes. What bounds that is not
|
|
11
|
+
* the list: every entry of `urls.allowedRedirectUrls` is written by the operator by hand,
|
|
12
|
+
* and a redirect carries only a ticket that is useless without the binding (ADR 0044).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* REDIRECT_SCHEMES_NEVER_CUSTOM.includes('javascript') // true
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare const REDIRECT_SCHEMES_NEVER_CUSTOM: readonly ["http", "https", "javascript", "vbscript", "data", "file", "filesystem", "blob", "about", "mailto", "tel", "sms", "facetime", "geo", "maps", "intent", "content", "android-app", "market", "itms", "itms-apps", "itms-services", "app-settings", "ms-settings", "x-apple.systempreferences", "ftp", "ws", "wss", "chrome", "chrome-extension", "moz-extension", "resource", "jar", "cid", "mid", "view-source"];
|
|
20
|
+
/**
|
|
21
|
+
* Families of schemes that are never a custom-scheme redirect: every scheme that starts
|
|
22
|
+
* with one of these. They are written with full stops, so the reverse-domain rule lets
|
|
23
|
+
* them through, and each belongs to a platform and to no operator's app: the schemes of
|
|
24
|
+
* Windows' built-in apps (`microsoft.windows.camera`, `microsoft.windows.photos.crop`, …),
|
|
25
|
+
* Apple's `x-apple.` schemes (`x-apple.systempreferences`) and Apple's own bundle-id space
|
|
26
|
+
* (`com.apple.`, which no third party's app can be in).
|
|
27
|
+
*
|
|
28
|
+
* Best effort, like {@link REDIRECT_SCHEMES_NEVER_CUSTOM}: a family that is not here passes.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM.some((family) => 'com.apple.tv'.startsWith(family)) // true
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare const REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM: readonly ["microsoft.windows.", "x-apple.", "com.apple."];
|
|
36
|
+
/**
|
|
37
|
+
* The kinds of redirect URL an environment may list.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const kind: RedirectUrlKind = 'custom_scheme'
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
type RedirectUrlKind = "https" | "loopback" | "custom_scheme";
|
|
45
|
+
/**
|
|
46
|
+
* Whether `value` holds a character no redirect URL of any kind may hold:
|
|
47
|
+
*
|
|
48
|
+
* - whitespace (`\s`) or a wildcard (`*`);
|
|
49
|
+
* - a backslash or a control character (U+0000 to U+001F, U+007F to U+009F);
|
|
50
|
+
* - a character that draws nothing, which is what `hasInvisibleCharacter` says: the one
|
|
51
|
+
* class `[\p{Cf}\p{Variation_Selector}\p{Default_Ignorable_Code_Point}]`, so every format
|
|
52
|
+
* character (the zero-width space, joiner and non-joiner, the word joiner, the soft
|
|
53
|
+
* hyphen, the Mongolian vowel separator, the text-direction controls, the tag
|
|
54
|
+
* characters), the variation selectors (U+FE00 to U+FE0F, U+E0100 to U+E01EF) and whatever
|
|
55
|
+
* else Unicode ignores by default (the combining grapheme joiner, the Hangul fillers);
|
|
56
|
+
* - what `hasHiddenCharacter` refuses beside those: a private-use character (`Co`), an
|
|
57
|
+
* unassigned one (`Cn`), a lone surrogate.
|
|
58
|
+
*
|
|
59
|
+
* A `Location` header cannot carry the controls, and a reader cannot see the rest: two
|
|
60
|
+
* entries that differ by a zero-width space read the same on every screen. An entry is
|
|
61
|
+
* compared as the string it is, so it holds only what can be seen. A visible letter outside
|
|
62
|
+
* ASCII (`münchen.de`) and a percent-encoded octet (`%20`, `%E2%80%8B`) are not refused.
|
|
63
|
+
*
|
|
64
|
+
* The server asks this of every URL it is about to redirect to, listed or not, so that a
|
|
65
|
+
* stored entry from before a rule is refused when a sign-in starts and never at the
|
|
66
|
+
* provider's callback, where the state is already spent.
|
|
67
|
+
*
|
|
68
|
+
* @param value - A URL.
|
|
69
|
+
* @returns `true` when it holds such a character.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* hasForbiddenRedirectCharacter('https://a.com/\u{202E}x') // true
|
|
74
|
+
* hasForbiddenRedirectCharacter('https://a.com/x\u{200B}y') // true
|
|
75
|
+
* hasForbiddenRedirectCharacter('https://a.com/cb?x=1') // false
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
declare function hasForbiddenRedirectCharacter(value: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Which kind of redirect URL `value` is, or `null` when an environment may not list it.
|
|
81
|
+
*
|
|
82
|
+
* - `https`: any absolute URL that starts with `https://` (lower case) and has no
|
|
83
|
+
* credentials, fragment, wildcard, whitespace, backslash, control character (U+0000 to
|
|
84
|
+
* U+001F, U+007F to U+009F), character that draws nothing (`hasInvisibleCharacter`: `Cf`,
|
|
85
|
+
* the variation selectors, `Default_Ignorable_Code_Point`) or character
|
|
86
|
+
* `hasHiddenCharacter` refuses. A query is allowed.
|
|
87
|
+
* An app link is one of these.
|
|
88
|
+
* - `loopback`: the same over `http://` for `localhost`, `127.0.0.1` and `[::1]`.
|
|
89
|
+
* - `custom_scheme`: `<scheme>:/<path>` or `<scheme>://<host>/<path>` where the scheme is in
|
|
90
|
+
* reverse-domain form (lower case, **with a full stop**: `com.example.app`), is not one of
|
|
91
|
+
* {@link REDIRECT_SCHEMES_NEVER_CUSTOM} nor in a family of
|
|
92
|
+
* {@link REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM}, and what follows is slashes and the characters
|
|
93
|
+
* `A-Z a-z 0-9 . _ ~ -` only. So: no user name or password, no port, **no query**, no
|
|
94
|
+
* fragment (the server adds one), no wildcard, no percent-encoded octet, no `.` or `..`
|
|
95
|
+
* segment, and no control or invisible character.
|
|
96
|
+
*
|
|
97
|
+
* Nothing is normalised, for any kind: the string that is listed is the string a request
|
|
98
|
+
* must send, character for character.
|
|
99
|
+
*
|
|
100
|
+
* @param value - A URL as an operator wrote it.
|
|
101
|
+
* @returns Its kind, or `null`.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* redirectUrlKind('com.example.app:/oauth') // 'custom_scheme'
|
|
106
|
+
* redirectUrlKind('myapp://callback') // null: no full stop in the scheme
|
|
107
|
+
* redirectUrlKind('javascript:alert(1)') // null
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
declare function redirectUrlKind(value: string): RedirectUrlKind | null;
|
|
111
|
+
/**
|
|
112
|
+
* Whether an environment may list `value` in `urls.allowedRedirectUrls`.
|
|
113
|
+
*
|
|
114
|
+
* @param value - A URL as an operator wrote it.
|
|
115
|
+
* @returns `true` for any {@link RedirectUrlKind}.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* isRedirectUrl('https://app.example.com/oauth/callback') // true
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function isRedirectUrl(value: string): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Whether `value` is a custom-scheme redirect URL ({@link redirectUrlKind}).
|
|
125
|
+
*
|
|
126
|
+
* @param value - A URL.
|
|
127
|
+
* @returns `true` only for the `custom_scheme` kind.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* isCustomSchemeRedirectUrl('com.example.app:/oauth') // true
|
|
132
|
+
* isCustomSchemeRedirectUrl('https://app.example.com/oauth') // false
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare function isCustomSchemeRedirectUrl(value: string): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* The providers whose authorization code Tula binds to the attempt with PKCE (ADR 0026): the
|
|
138
|
+
* code is redeemable only with the verifier the attempt holds.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* OAUTH_PROVIDERS_WITH_PKCE.includes('google') // true
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
declare const OAUTH_PROVIDERS_WITH_PKCE: readonly ["google", "github", "microsoft", "discord", "x"];
|
|
146
|
+
/**
|
|
147
|
+
* The providers that do **not** bind their code with PKCE, as a fact about the provider and
|
|
148
|
+
* not a setting: Apple and LinkedIn document none, and Facebook's has it only in a flow that
|
|
149
|
+
* is not used (ADR 0026).
|
|
150
|
+
*
|
|
151
|
+
* A provider sign-in with one of them is never returned to a custom scheme
|
|
152
|
+
* ({@link customSchemeRedirectRefusal}). A new provider is added to this list or to
|
|
153
|
+
* {@link OAUTH_PROVIDERS_WITH_PKCE}; a test fails for one that is in neither.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* OAUTH_PROVIDERS_WITHOUT_PKCE.includes('apple') // true
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
declare const OAUTH_PROVIDERS_WITHOUT_PKCE: readonly ["apple", "linkedin", "facebook"];
|
|
161
|
+
/**
|
|
162
|
+
* Whether a provider's code is bound to the attempt with PKCE
|
|
163
|
+
* ({@link OAUTH_PROVIDERS_WITH_PKCE}). The one place that says so.
|
|
164
|
+
*
|
|
165
|
+
* @param provider - A provider's name.
|
|
166
|
+
* @returns `true` only for a provider on the list; `false` for any other string.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* bindsCodeWithPkce('google') // true
|
|
171
|
+
* bindsCodeWithPkce('apple') // false
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
declare function bindsCodeWithPkce(provider: string): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Why a listed custom-scheme redirect URL is still refused for one request, as the fixed word
|
|
177
|
+
* in `params.reason` of `request.redirect_not_allowed`:
|
|
178
|
+
*
|
|
179
|
+
* - `provider_without_pkce`: the provider's code is not bound with PKCE.
|
|
180
|
+
* - `client_not_native`: the attempt was not started as a native client (`x-tula-client: ios` or
|
|
181
|
+
* `android`).
|
|
182
|
+
* - `not_a_provider_sign_in`: the redirect is not for a provider sign-in (an emailed link).
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* const reason: CustomSchemeRedirectRefusal = CUSTOM_SCHEME_REDIRECT_REFUSALS[0]
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
declare const CUSTOM_SCHEME_REDIRECT_REFUSALS: readonly ["provider_without_pkce", "client_not_native", "not_a_provider_sign_in"];
|
|
190
|
+
/** One of {@link CUSTOM_SCHEME_REDIRECT_REFUSALS}. */
|
|
191
|
+
type CustomSchemeRedirectRefusal = (typeof CUSTOM_SCHEME_REDIRECT_REFUSALS)[number];
|
|
192
|
+
/**
|
|
193
|
+
* Why a redirect to `url` is refused for this use although the environment lists it, or
|
|
194
|
+
* `null` when it is not.
|
|
195
|
+
*
|
|
196
|
+
* Only a custom-scheme URL is ever refused here. Any app on the device can claim a scheme,
|
|
197
|
+
* so the app that receives the redirect may be another one. What then keeps a sign-in from
|
|
198
|
+
* being completed by it:
|
|
199
|
+
*
|
|
200
|
+
* - the provider's code is bound with PKCE, so a provider that is told to return to the
|
|
201
|
+
* scheme's owner cannot be made to hand a usable code to someone else. Without PKCE the
|
|
202
|
+
* redirect is refused, whatever else holds;
|
|
203
|
+
* - the attempt belongs to a native client. A browser page has no use for a custom scheme,
|
|
204
|
+
* and a `web` attempt ends in cookies.
|
|
205
|
+
*
|
|
206
|
+
* It depends on the URL, the provider and the client kind, never on a user.
|
|
207
|
+
*
|
|
208
|
+
* @param url - A redirect URL the environment lists.
|
|
209
|
+
* @param use - The client kind of the attempt, and the provider when it is a provider
|
|
210
|
+
* sign-in.
|
|
211
|
+
* @returns The reason, or `null`.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* customSchemeRedirectRefusal('com.example.app:/oauth', { client: 'ios', provider: 'apple' })
|
|
216
|
+
* // 'provider_without_pkce'
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
declare function customSchemeRedirectRefusal(url: string, use: {
|
|
220
|
+
client: string;
|
|
221
|
+
provider?: string;
|
|
222
|
+
}): CustomSchemeRedirectRefusal | null;
|
|
223
|
+
export { CUSTOM_SCHEME_REDIRECT_REFUSALS, CustomSchemeRedirectRefusal, OAUTH_PROVIDERS_WITHOUT_PKCE, OAUTH_PROVIDERS_WITH_PKCE, REDIRECT_SCHEMES_NEVER_CUSTOM, REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM, RedirectUrlKind, bindsCodeWithPkce, customSchemeRedirectRefusal, hasForbiddenRedirectCharacter, isCustomSchemeRedirectUrl, isRedirectUrl, redirectUrlKind };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REDIRECT_SCHEMES_NEVER_CUSTOM2,
|
|
3
|
+
REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM2,
|
|
4
|
+
hasForbiddenRedirectCharacter2,
|
|
5
|
+
redirectUrlKind2,
|
|
6
|
+
isRedirectUrl2,
|
|
7
|
+
isCustomSchemeRedirectUrl2,
|
|
8
|
+
OAUTH_PROVIDERS_WITH_PKCE2,
|
|
9
|
+
OAUTH_PROVIDERS_WITHOUT_PKCE2,
|
|
10
|
+
bindsCodeWithPkce2,
|
|
11
|
+
CUSTOM_SCHEME_REDIRECT_REFUSALS2,
|
|
12
|
+
customSchemeRedirectRefusal2
|
|
13
|
+
} from "./shared/chunk-r202nxfw.js";
|
|
14
|
+
export {
|
|
15
|
+
CUSTOM_SCHEME_REDIRECT_REFUSALS2 as CUSTOM_SCHEME_REDIRECT_REFUSALS,
|
|
16
|
+
OAUTH_PROVIDERS_WITHOUT_PKCE2 as OAUTH_PROVIDERS_WITHOUT_PKCE,
|
|
17
|
+
OAUTH_PROVIDERS_WITH_PKCE2 as OAUTH_PROVIDERS_WITH_PKCE,
|
|
18
|
+
REDIRECT_SCHEMES_NEVER_CUSTOM2 as REDIRECT_SCHEMES_NEVER_CUSTOM,
|
|
19
|
+
REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM2 as REDIRECT_SCHEME_FAMILIES_NEVER_CUSTOM,
|
|
20
|
+
bindsCodeWithPkce2 as bindsCodeWithPkce,
|
|
21
|
+
customSchemeRedirectRefusal2 as customSchemeRedirectRefusal,
|
|
22
|
+
hasForbiddenRedirectCharacter2 as hasForbiddenRedirectCharacter,
|
|
23
|
+
isCustomSchemeRedirectUrl2 as isCustomSchemeRedirectUrl,
|
|
24
|
+
isRedirectUrl2 as isRedirectUrl,
|
|
25
|
+
redirectUrlKind2 as redirectUrlKind
|
|
26
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/webhook-signature.ts
|
|
2
|
+
var WEBHOOK_ID_HEADER2 = "webhook-id";
|
|
3
|
+
var WEBHOOK_TIMESTAMP_HEADER2 = "webhook-timestamp";
|
|
4
|
+
var WEBHOOK_SIGNATURE_HEADER2 = "webhook-signature";
|
|
5
|
+
var WEBHOOK_SECRET_PREFIX2 = "whsec_";
|
|
6
|
+
var WEBHOOK_SIGNATURE_VERSION2 = "v1";
|
|
7
|
+
var WEBHOOK_TOLERANCE_SECONDS2 = 5 * 60;
|
|
8
|
+
var MIN_KEY_BYTES = 24;
|
|
9
|
+
var MAX_KEY_BYTES = 64;
|
|
10
|
+
var BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
11
|
+
function toBase64(bytes) {
|
|
12
|
+
let binary = "";
|
|
13
|
+
for (const byte of bytes) {
|
|
14
|
+
binary += String.fromCharCode(byte);
|
|
15
|
+
}
|
|
16
|
+
return btoa(binary);
|
|
17
|
+
}
|
|
18
|
+
function webhookSecretBytes2(secret) {
|
|
19
|
+
if (!secret.startsWith(WEBHOOK_SECRET_PREFIX2)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const encoded = secret.slice(WEBHOOK_SECRET_PREFIX2.length);
|
|
23
|
+
if (!BASE64.test(encoded) || encoded.length % 4 !== 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
let binary;
|
|
27
|
+
try {
|
|
28
|
+
binary = atob(encoded);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (binary.length < MIN_KEY_BYTES || binary.length > MAX_KEY_BYTES) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const bytes = new Uint8Array(binary.length);
|
|
36
|
+
for (let index = 0;index < binary.length; index++) {
|
|
37
|
+
bytes[index] = binary.charCodeAt(index);
|
|
38
|
+
}
|
|
39
|
+
return bytes;
|
|
40
|
+
}
|
|
41
|
+
function formatWebhookSecret2(key) {
|
|
42
|
+
if (key.length < MIN_KEY_BYTES || key.length > MAX_KEY_BYTES) {
|
|
43
|
+
throw new RangeError("a webhook signing key is 24 to 64 bytes");
|
|
44
|
+
}
|
|
45
|
+
return `${WEBHOOK_SECRET_PREFIX2}${toBase64(key)}`;
|
|
46
|
+
}
|
|
47
|
+
async function signWebhook2(key, id, timestamp, body) {
|
|
48
|
+
const hmac = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, [
|
|
49
|
+
"sign"
|
|
50
|
+
]);
|
|
51
|
+
const content = new TextEncoder().encode(`${id}.${timestamp}.${body}`);
|
|
52
|
+
const signature = new Uint8Array(await crypto.subtle.sign("HMAC", hmac, content));
|
|
53
|
+
return `${WEBHOOK_SIGNATURE_VERSION2},${toBase64(signature)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { WEBHOOK_ID_HEADER2, WEBHOOK_TIMESTAMP_HEADER2, WEBHOOK_SIGNATURE_HEADER2, WEBHOOK_SECRET_PREFIX2, WEBHOOK_SIGNATURE_VERSION2, WEBHOOK_TOLERANCE_SECONDS2, webhookSecretBytes2, formatWebhookSecret2, signWebhook2 };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/custom-claims.ts
|
|
2
|
+
var CUSTOM_CLAIMS_CLAIM2 = "ext";
|
|
3
|
+
var RESERVED_CLAIM_NAMES2 = [
|
|
4
|
+
"iss",
|
|
5
|
+
"sub",
|
|
6
|
+
"aud",
|
|
7
|
+
"exp",
|
|
8
|
+
"nbf",
|
|
9
|
+
"iat",
|
|
10
|
+
"jti",
|
|
11
|
+
"sid",
|
|
12
|
+
"pid",
|
|
13
|
+
"eid",
|
|
14
|
+
"v",
|
|
15
|
+
"auth_time",
|
|
16
|
+
"amr",
|
|
17
|
+
"sp",
|
|
18
|
+
"cnf",
|
|
19
|
+
CUSTOM_CLAIMS_CLAIM2
|
|
20
|
+
];
|
|
21
|
+
var MAX_CUSTOM_CLAIMS_BYTES2 = 1024;
|
|
22
|
+
var MAX_JWT_TEMPLATES2 = 10;
|
|
23
|
+
var MAX_JWT_TEMPLATE_CLAIMS2 = 16;
|
|
24
|
+
var MAX_CUSTOM_CLAIM_KEY_LENGTH2 = 32;
|
|
25
|
+
var MAX_CUSTOM_CLAIM_CONSTANT_LENGTH2 = 256;
|
|
26
|
+
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
27
|
+
var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
|
|
28
|
+
function isCustomClaimKey2(key) {
|
|
29
|
+
return typeof key === "string" && key.length <= MAX_CUSTOM_CLAIM_KEY_LENGTH2 && KEY.test(key) && !UNSAFE_KEYS.includes(key) && !RESERVED_CLAIM_NAMES2.includes(key);
|
|
30
|
+
}
|
|
31
|
+
function isCustomClaimValue2(value) {
|
|
32
|
+
return typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
33
|
+
}
|
|
34
|
+
function utf8Length(text) {
|
|
35
|
+
let bytes = 0;
|
|
36
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
37
|
+
const unit = text.charCodeAt(index);
|
|
38
|
+
if (unit < 128) {
|
|
39
|
+
bytes += 1;
|
|
40
|
+
} else if (unit < 2048) {
|
|
41
|
+
bytes += 2;
|
|
42
|
+
} else if (unit >= 55296 && unit <= 56319 && isLowSurrogate(text.charCodeAt(index + 1))) {
|
|
43
|
+
bytes += 4;
|
|
44
|
+
index += 1;
|
|
45
|
+
} else {
|
|
46
|
+
bytes += 3;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return bytes;
|
|
50
|
+
}
|
|
51
|
+
function isLowSurrogate(unit) {
|
|
52
|
+
return unit >= 56320 && unit <= 57343;
|
|
53
|
+
}
|
|
54
|
+
function customClaimsBytes2(claims) {
|
|
55
|
+
return utf8Length(JSON.stringify(claims));
|
|
56
|
+
}
|
|
57
|
+
function isPlainObject(value) {
|
|
58
|
+
if (typeof value !== "object" || value === null) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const prototype = Object.getPrototypeOf(value);
|
|
62
|
+
return prototype === Object.prototype || prototype === null;
|
|
63
|
+
}
|
|
64
|
+
function checkCustomClaims2(value) {
|
|
65
|
+
if (!isPlainObject(value)) {
|
|
66
|
+
return { problem: "invalid" };
|
|
67
|
+
}
|
|
68
|
+
const entries = [];
|
|
69
|
+
for (const key of Object.keys(value)) {
|
|
70
|
+
const claim = value[key];
|
|
71
|
+
if (!isCustomClaimKey2(key) || !isCustomClaimValue2(claim)) {
|
|
72
|
+
return { problem: "invalid" };
|
|
73
|
+
}
|
|
74
|
+
entries.push([key, claim]);
|
|
75
|
+
}
|
|
76
|
+
const claims = Object.fromEntries(entries);
|
|
77
|
+
return customClaimsBytes2(claims) > MAX_CUSTOM_CLAIMS_BYTES2 ? { problem: "too_large" } : { claims };
|
|
78
|
+
}
|
|
79
|
+
function readCustomClaims2(claims) {
|
|
80
|
+
if (!isPlainObject(claims) || !Object.hasOwn(claims, CUSTOM_CLAIMS_CLAIM2)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const raw = claims[CUSTOM_CLAIMS_CLAIM2];
|
|
84
|
+
if (!isPlainObject(raw)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const entries = [];
|
|
88
|
+
for (const key of Object.keys(raw)) {
|
|
89
|
+
const value = raw[key];
|
|
90
|
+
if (!isCustomClaimKey2(key) || !isCustomClaimValue2(value)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
entries.push([key, value]);
|
|
94
|
+
}
|
|
95
|
+
if (entries.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const read = Object.fromEntries(entries);
|
|
99
|
+
return customClaimsBytes2(read) <= MAX_CUSTOM_CLAIMS_BYTES2 ? Object.freeze(read) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { CUSTOM_CLAIMS_CLAIM2, RESERVED_CLAIM_NAMES2, MAX_CUSTOM_CLAIMS_BYTES2, MAX_JWT_TEMPLATES2, MAX_JWT_TEMPLATE_CLAIMS2, MAX_CUSTOM_CLAIM_KEY_LENGTH2, MAX_CUSTOM_CLAIM_CONSTANT_LENGTH2, isCustomClaimKey2, isCustomClaimValue2, customClaimsBytes2, checkCustomClaims2, readCustomClaims2 };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/device-binding.ts
|
|
2
|
+
var DPOP_HEADER2 = "DPoP";
|
|
3
|
+
var DPOP_NONCE_HEADER2 = "DPoP-Nonce";
|
|
4
|
+
var DPOP_PROOF_TYPE2 = "dpop+jwt";
|
|
5
|
+
var DPOP_ALGORITHMS2 = ["ES256"];
|
|
6
|
+
var MAX_DPOP_PROOF_LENGTH2 = 2048;
|
|
7
|
+
var encoder = new TextEncoder;
|
|
8
|
+
function base64url(bytes) {
|
|
9
|
+
let binary = "";
|
|
10
|
+
for (const byte of bytes) {
|
|
11
|
+
binary += String.fromCharCode(byte);
|
|
12
|
+
}
|
|
13
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
14
|
+
}
|
|
15
|
+
var THIRTY_TWO_BYTES = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
16
|
+
var COORDINATE = THIRTY_TWO_BYTES;
|
|
17
|
+
var THUMBPRINT = THIRTY_TWO_BYTES;
|
|
18
|
+
function isDevicePublicJwk2(value) {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const jwk = value;
|
|
23
|
+
return Object.keys(jwk).length === 4 && jwk.kty === "EC" && jwk.crv === "P-256" && typeof jwk.x === "string" && COORDINATE.test(jwk.x) && typeof jwk.y === "string" && COORDINATE.test(jwk.y);
|
|
24
|
+
}
|
|
25
|
+
function isKeyThumbprint2(value) {
|
|
26
|
+
return typeof value === "string" && THUMBPRINT.test(value);
|
|
27
|
+
}
|
|
28
|
+
async function jwkThumbprint2(jwk) {
|
|
29
|
+
const canonical = `{"crv":"${jwk.crv}","kty":"${jwk.kty}","x":"${jwk.x}","y":"${jwk.y}"}`;
|
|
30
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonical));
|
|
31
|
+
return base64url(new Uint8Array(digest));
|
|
32
|
+
}
|
|
33
|
+
async function createDpopProof2(key, input) {
|
|
34
|
+
const header = { typ: DPOP_PROOF_TYPE2, alg: DPOP_ALGORITHMS2[0], jwk: key.publicJwk };
|
|
35
|
+
const payload = {
|
|
36
|
+
htm: input.method,
|
|
37
|
+
htu: input.url,
|
|
38
|
+
iat: Math.floor((input.now ?? Date.now()) / 1000),
|
|
39
|
+
jti: input.jti ?? key.proofId?.() ?? base64url(crypto.getRandomValues(new Uint8Array(16))),
|
|
40
|
+
...input.nonce !== undefined && { nonce: input.nonce }
|
|
41
|
+
};
|
|
42
|
+
const signed = `${base64url(encoder.encode(JSON.stringify(header)))}.${base64url(encoder.encode(JSON.stringify(payload)))}`;
|
|
43
|
+
return `${signed}.${base64url(await key.sign(encoder.encode(signed)))}`;
|
|
44
|
+
}
|
|
45
|
+
async function generateSoftwareDeviceKey2() {
|
|
46
|
+
const algorithm = { name: "ECDSA", namedCurve: "P-256" };
|
|
47
|
+
const pair = await crypto.subtle.generateKey(algorithm, false, ["sign"]);
|
|
48
|
+
const { x, y } = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
49
|
+
return {
|
|
50
|
+
publicJwk: { kty: "EC", crv: "P-256", x, y },
|
|
51
|
+
async sign(data) {
|
|
52
|
+
const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, pair.privateKey, new Uint8Array(data));
|
|
53
|
+
return new Uint8Array(signature);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { DPOP_HEADER2, DPOP_NONCE_HEADER2, DPOP_PROOF_TYPE2, DPOP_ALGORITHMS2, MAX_DPOP_PROOF_LENGTH2, isDevicePublicJwk2, isKeyThumbprint2, jwkThumbprint2, createDpopProof2, generateSoftwareDeviceKey2 };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/event-types.ts
|
|
2
|
+
var ACTIVITY_TYPES2 = [
|
|
3
|
+
"user.created",
|
|
4
|
+
"user.email_verified",
|
|
5
|
+
"user.banned",
|
|
6
|
+
"user.unbanned",
|
|
7
|
+
"user.deleted",
|
|
8
|
+
"user.password_changed",
|
|
9
|
+
"user.mfa_enabled",
|
|
10
|
+
"user.mfa_disabled",
|
|
11
|
+
"user.backup_codes_regenerated",
|
|
12
|
+
"user.backup_code_used",
|
|
13
|
+
"user.identity_linked",
|
|
14
|
+
"user.identity_unlinked",
|
|
15
|
+
"user.email_added",
|
|
16
|
+
"user.passkey_added",
|
|
17
|
+
"user.passkey_renamed",
|
|
18
|
+
"user.passkey_removed",
|
|
19
|
+
"user.passkey_counter_regressed",
|
|
20
|
+
"user.phone_number_added",
|
|
21
|
+
"user.phone_number_removed",
|
|
22
|
+
"user.sms_factor_enabled",
|
|
23
|
+
"user.sms_factor_removed",
|
|
24
|
+
"session.created",
|
|
25
|
+
"session.revoked",
|
|
26
|
+
"session.reuse_detected",
|
|
27
|
+
"session.stepped_up",
|
|
28
|
+
"session.refresh_proof_refused",
|
|
29
|
+
"api_key.created",
|
|
30
|
+
"api_key.revoked",
|
|
31
|
+
"signing_key.rotated",
|
|
32
|
+
"environment.settings_updated",
|
|
33
|
+
"oauth_provider.updated",
|
|
34
|
+
"oauth_provider.deleted",
|
|
35
|
+
"webhook_endpoint.created",
|
|
36
|
+
"webhook_endpoint.updated",
|
|
37
|
+
"webhook_endpoint.deleted",
|
|
38
|
+
"webhook_endpoint.disabled",
|
|
39
|
+
"webhook_endpoint.secret_rotated",
|
|
40
|
+
"webhook_endpoint.previous_secret_revoked",
|
|
41
|
+
"hook.created",
|
|
42
|
+
"hook.updated",
|
|
43
|
+
"hook.deleted",
|
|
44
|
+
"native_app.created",
|
|
45
|
+
"native_app.updated",
|
|
46
|
+
"native_app.deleted",
|
|
47
|
+
"proposal.created",
|
|
48
|
+
"proposal.approved",
|
|
49
|
+
"proposal.executed",
|
|
50
|
+
"proposal.failed",
|
|
51
|
+
"sign_in_test.started",
|
|
52
|
+
"sign_in_test.finished"
|
|
53
|
+
];
|
|
54
|
+
var EVENT_SCHEMA_VERSION2 = 1;
|
|
55
|
+
var EVENT_TARGET_TYPES2 = {
|
|
56
|
+
"user.created": "user",
|
|
57
|
+
"user.email_verified": "user",
|
|
58
|
+
"user.banned": "user",
|
|
59
|
+
"user.unbanned": "user",
|
|
60
|
+
"user.deleted": "user",
|
|
61
|
+
"user.password_changed": "user",
|
|
62
|
+
"user.mfa_enabled": "user",
|
|
63
|
+
"user.mfa_disabled": "user",
|
|
64
|
+
"user.backup_codes_regenerated": "user",
|
|
65
|
+
"user.backup_code_used": "user",
|
|
66
|
+
"user.identity_linked": "user",
|
|
67
|
+
"user.email_added": "user",
|
|
68
|
+
"user.identity_unlinked": "user",
|
|
69
|
+
"user.passkey_added": "user",
|
|
70
|
+
"user.passkey_renamed": "user",
|
|
71
|
+
"user.passkey_removed": "user",
|
|
72
|
+
"user.passkey_counter_regressed": "user",
|
|
73
|
+
"user.phone_number_added": "user",
|
|
74
|
+
"user.phone_number_removed": "user",
|
|
75
|
+
"user.sms_factor_enabled": "user",
|
|
76
|
+
"user.sms_factor_removed": "user",
|
|
77
|
+
"session.created": "session",
|
|
78
|
+
"session.revoked": "session",
|
|
79
|
+
"session.reuse_detected": "session",
|
|
80
|
+
"session.stepped_up": "session",
|
|
81
|
+
"session.refresh_proof_refused": "session",
|
|
82
|
+
"api_key.created": "api_key",
|
|
83
|
+
"api_key.revoked": "api_key",
|
|
84
|
+
"signing_key.rotated": "signing_key",
|
|
85
|
+
"environment.settings_updated": "environment",
|
|
86
|
+
"oauth_provider.updated": "environment",
|
|
87
|
+
"oauth_provider.deleted": "environment",
|
|
88
|
+
"webhook_endpoint.created": "webhook_endpoint",
|
|
89
|
+
"webhook_endpoint.updated": "webhook_endpoint",
|
|
90
|
+
"webhook_endpoint.deleted": "webhook_endpoint",
|
|
91
|
+
"webhook_endpoint.disabled": "webhook_endpoint",
|
|
92
|
+
"webhook_endpoint.secret_rotated": "webhook_endpoint",
|
|
93
|
+
"webhook_endpoint.previous_secret_revoked": "webhook_endpoint",
|
|
94
|
+
"hook.created": "hook",
|
|
95
|
+
"hook.updated": "hook",
|
|
96
|
+
"hook.deleted": "hook",
|
|
97
|
+
"native_app.created": "native_app",
|
|
98
|
+
"native_app.updated": "native_app",
|
|
99
|
+
"native_app.deleted": "native_app",
|
|
100
|
+
"proposal.created": "proposal",
|
|
101
|
+
"proposal.approved": "proposal",
|
|
102
|
+
"proposal.executed": "proposal",
|
|
103
|
+
"proposal.failed": "proposal",
|
|
104
|
+
"sign_in_test.started": "environment",
|
|
105
|
+
"sign_in_test.finished": "environment"
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { ACTIVITY_TYPES2, EVENT_SCHEMA_VERSION2, EVENT_TARGET_TYPES2 };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/issuer.ts
|
|
2
|
+
function environmentIssuer2(apiUrl, environmentId) {
|
|
3
|
+
return `${apiUrl.replace(/\/+$/, "")}/v1/environments/${encodeURIComponent(environmentId)}`;
|
|
4
|
+
}
|
|
5
|
+
function jwksUrl2(issuer) {
|
|
6
|
+
return `${issuer.replace(/\/+$/, "")}/.well-known/jwks.json`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { environmentIssuer2, jwksUrl2 };
|