@openemail/sdk 0.0.4 → 0.0.6
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/README.md +54 -3
- package/index.cjs +262 -26
- package/index.d.cts +217 -9
- package/index.d.ts +217 -9
- package/index.js +255 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
## Intro to the Npm Package
|
|
46
46
|
|
|
47
|
-
The official TypeScript client for the OpenEmail API. A method for every one of the
|
|
47
|
+
The official TypeScript client for the OpenEmail API. A method for every one of the 197 endpoints, 282 in all once the paging and upload helpers are counted, typed end to end, with zero dependencies. It runs on Node 20+, Bun, Deno and Cloudflare Workers, and ships as ESM and CommonJS.
|
|
48
48
|
|
|
49
|
-
It carries a workspace API key, so it belongs on a server. The one exception is disposable inboxes, which need no
|
|
49
|
+
It carries a workspace API key or an OAuth access token, so it belongs on a server or in a tool that runs on your own machine. The one exception is disposable inboxes, which need no credential and work in a browser.
|
|
50
50
|
|
|
51
51
|
### Installing
|
|
52
52
|
```bash
|
|
@@ -158,6 +158,46 @@ const { items } = await temp.listMessages(inbox.id, { inboxToken: inbox.token })
|
|
|
158
158
|
|
|
159
159
|
`create` needs no credential and is the only call that returns the inbox token, so keep it.
|
|
160
160
|
|
|
161
|
+
### OAuth access tokens
|
|
162
|
+
An app a person connected to OpenEmail with OAuth, such as a command line tool or an agent, holds an access token rather than an API key. Pass it as `accessToken`:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { OpenEmail } from '@openemail/sdk'
|
|
166
|
+
|
|
167
|
+
export const openemail = new OpenEmail({
|
|
168
|
+
accessToken: async () => session.freshAccessToken()
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`accessToken` takes the token itself, or a function that returns it. The function runs before every request, so renew the token there when it is close to expiring and the client never has to be rebuilt. Pass `apiKey` or `accessToken`, not both. `createOpenEmail()` reads `OPENEMAIL_ACCESS_TOKEN` when you pass neither and `OPENEMAIL_API_KEY` is not set. `me.get()` answers `object: 'oauth_token'` for a token, with the connected app's `clientId` and `expiresAt`, when the person's approval of the app runs out.
|
|
173
|
+
|
|
174
|
+
A token acts for a person, so before a sensitive change, such as deleting a domain or changing a webhook, it is asked for the same verification code the web app asks for. The request fails with `isStepUpRequired`. Ask for a code, check it, then replay the request:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { OpenEmailApiError } from '@openemail/sdk'
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await openemail.domains.delete(domain.id)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (!(error instanceof OpenEmailApiError) || !error.isStepUpRequired) throw error
|
|
185
|
+
|
|
186
|
+
const challenge = await openemail.security.beginStepUp()
|
|
187
|
+
|
|
188
|
+
const code = await ask(challenge.method === 'email'
|
|
189
|
+
? `Enter the code we emailed to ${challenge.sentTo}`
|
|
190
|
+
: 'Enter the code from your authenticator app, or a backup code')
|
|
191
|
+
|
|
192
|
+
await openemail.security.verifyStepUp({ code })
|
|
193
|
+
await openemail.domains.delete(domain.id)
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
An emailed code works for 10 minutes, and `beginStepUp({ resend: true })` sends a fresh one. The email shows the name the app registered with, its client ID and the change it asked to make, so the person can check who is asking before they hand the code over. Once a code is verified the app is not asked again for 60 minutes, for any of the 14 changes that ask for one, through the REST API or through the MCP tools that make the same changes. `verifyStepUp` lists them. `security.stepUpStatus()` says whether it is verified right now. An app that cannot ask for a code, such as an MCP connector, can instead be allowed by the person for 60 minutes with Allow changes for 60 minutes in Account settings, Connected apps, on the website. API keys are never asked for a code.
|
|
198
|
+
|
|
199
|
+
Each app has its own budget of codes, so another app never uses it up, and neither does the web app, which keeps its own 10 an hour. An app can ask for 5 codes an hour and 20 in 24 hours, then gets 429 `step_up_throttled`. A code allows 5 tries, and after the fifth wrong one a plain `beginStepUp()` starts a fresh challenge. Ten wrong codes in 24 hours pause verification for the app, and 20 in 24 hours from all of a person's apps together pause it for every app they connected. Either way both calls answer 429 `step_up_locked`, with the time the pause ends in the message. Codes entered in the web app count toward neither pause, and the person can still verify there.
|
|
200
|
+
|
|
161
201
|
### Configuring
|
|
162
202
|
Pass an options object instead of the bare key when the defaults are not right:
|
|
163
203
|
|
|
@@ -176,7 +216,18 @@ export const openemail = new OpenEmail({
|
|
|
176
216
|
|
|
177
217
|
`createOpenEmail(options)` is the same constructor with one difference: anything you leave out is read from the environment. The shipped `openemail` takes the same options through `init(options)`, called once at startup.
|
|
178
218
|
|
|
179
|
-
`baseUrl` also comes from `OPENEMAIL_BASE_URL`,
|
|
219
|
+
`baseUrl` also comes from `OPENEMAIL_BASE_URL`. Use an `https:` origin: the client refuses to send an API key, an access token or an inbox token over plain `http:`, and throws before the request leaves, unless the server is on this machine at `localhost`, a `127.x.x.x` address or `::1`. A `baseUrl` on `0.0.0.0` throws when the client is built, since that is the address a server listens on: use `127.0.0.1` with the same port. Reads are retried on 408 and 5xx with backoff. A 429 is retried only when it carries a `Retry-After`, and any wait longer than a minute throws instead of sleeping. Writes that cannot safely repeat are not retried. Every method outside `tempMail` takes `{ signal, apiKey }` as its last argument, so one process can serve several workspaces with one client. The `tempMail` methods take `{ signal, inboxToken }` instead.
|
|
220
|
+
|
|
221
|
+
An endpoint no method wraps yet is one `raw.request()` away, with the client's credential, base URL, timeout and retry policy applied:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const result = await openemail.raw.request('/something-new', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
body: { name: 'Invoices' }
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The path must begin with a single `/`. Anything else, such as `//host/x` or `@host/x`, throws before a request is sent, and so does a path whose finished URL leaves the base URL's origin, so the credential it carries never reaches another host.
|
|
180
231
|
|
|
181
232
|
When a newer version is on npm the client says so once on a TTY. `OPENEMAIL_DISABLE_UPDATE_NOTICE=1` or `{ disableUpdateNotice: true }` turns that off.
|
|
182
233
|
|
package/index.cjs
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
const AUDIENCE_MEMBER_STATUSES = {
|
|
6
|
+
SUBSCRIBED: 'subscribed',
|
|
7
|
+
UNSUBSCRIBED: 'unsubscribed'
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
const BROADCAST_STATUSES = {
|
|
6
11
|
SCHEDULED: 'scheduled',
|
|
7
12
|
QUEUED: 'queued',
|
|
@@ -23,7 +28,7 @@ const BROADCAST_RECIPIENT_FILTERS = {
|
|
|
23
28
|
UNSUBSCRIBED: 'unsubscribed'
|
|
24
29
|
};
|
|
25
30
|
|
|
26
|
-
var version = "0.0.
|
|
31
|
+
var version = "0.0.6";
|
|
27
32
|
|
|
28
33
|
const BUILD_BASE_URL = 'https://api.openemail.uk';
|
|
29
34
|
|
|
@@ -37,8 +42,8 @@ const DEFAULTS = {
|
|
|
37
42
|
const BROWSER_REFUSAL = [
|
|
38
43
|
'OpenEmail refused to start in a browser.',
|
|
39
44
|
'',
|
|
40
|
-
' This client carries a workspace API key that can send mail and read
|
|
41
|
-
' you ship to a browser is readable by anyone who opens devtools.',
|
|
45
|
+
' This client carries a workspace API key or an OAuth access token that can send mail and read',
|
|
46
|
+
' the mailbox. Anything you ship to a browser is readable by anyone who opens devtools.',
|
|
42
47
|
'',
|
|
43
48
|
' Call it from a server, a serverless function or a script instead. Disposable inboxes are',
|
|
44
49
|
' the exception: createTempMail() needs no API key and is safe to use in a browser.',
|
|
@@ -48,6 +53,7 @@ const BROWSER_REFUSAL = [
|
|
|
48
53
|
].join('\n');
|
|
49
54
|
const ENV_VARS = {
|
|
50
55
|
API_KEY: 'OPENEMAIL_API_KEY',
|
|
56
|
+
ACCESS_TOKEN: 'OPENEMAIL_ACCESS_TOKEN',
|
|
51
57
|
BASE_URL: 'OPENEMAIL_BASE_URL'
|
|
52
58
|
};
|
|
53
59
|
const API_KEY_PREFIXES = {
|
|
@@ -58,10 +64,22 @@ const API_KEY_MODES = {
|
|
|
58
64
|
LIVE: 'live',
|
|
59
65
|
TEST: 'test'
|
|
60
66
|
};
|
|
67
|
+
const ACCESS_TOKEN_RULES = {
|
|
68
|
+
RESERVED_PREFIX: 'oe_',
|
|
69
|
+
MAX_LENGTH: 512
|
|
70
|
+
};
|
|
71
|
+
const CREDENTIAL_KINDS = {
|
|
72
|
+
API_KEY: 'apiKey',
|
|
73
|
+
OAUTH: 'oauth'
|
|
74
|
+
};
|
|
61
75
|
const CLIENT_MESSAGES = {
|
|
62
76
|
API_KEY_REQUIRED: `An OpenEmail API key is required. Pass { apiKey } or set ${ENV_VARS.API_KEY}. Create one in OpenEmail under Settings, API keys.`,
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
CREDENTIAL_REQUIRED: `An OpenEmail API key or OAuth access token is required. Pass { apiKey } or { accessToken }, or set ${ENV_VARS.API_KEY} or ${ENV_VARS.ACCESS_TOKEN}. Create a key in OpenEmail under Settings, API keys.`,
|
|
78
|
+
CREDENTIAL_CONFLICT: 'Pass { apiKey } or { accessToken }, not both. Every request carries one credential, so the client cannot tell which of the two you meant.',
|
|
79
|
+
API_KEY_SHAPE: 'is not an OpenEmail API key. The API accepts only keys beginning "oe_live_" or "oe_test_", so a session cookie, a session token or a key for another service is refused. Pass an OAuth access token as { accessToken } instead.',
|
|
80
|
+
ACCESS_TOKEN_SHAPE: `is not an OAuth access token. Pass the access token OpenEmail issued when the app was connected: a string of 1 to ${ACCESS_TOKEN_RULES.MAX_LENGTH} characters that does not begin "${ACCESS_TOKEN_RULES.RESERVED_PREFIX}". An API key goes in { apiKey } instead.`,
|
|
81
|
+
ACCESS_TOKEN_PROVIDED: 'The value the { accessToken } function resolved to',
|
|
82
|
+
BASE_URL_SHAPE: 'is not a usable base URL. Pass an https origin such as "https://api.openemail.uk". Plain http is for a server on this machine only, at localhost, a 127.x.x.x address or ::1, because the client never sends a credential over plain http to any other host.',
|
|
65
83
|
NO_FETCH: 'No global fetch is available. Pass one as { fetch }, or run on Node 20+, Bun, Deno, Cloudflare Workers or a browser.',
|
|
66
84
|
EMPTY_SEGMENT: 'An id must not be empty.',
|
|
67
85
|
FILENAME_REQUIRED: 'A file name is required. Pass it as { filename }, the name the file is stored and downloaded under.',
|
|
@@ -90,6 +108,18 @@ const CONTACT_PHOTO_TYPES = {
|
|
|
90
108
|
GIF: 'image/gif'
|
|
91
109
|
};
|
|
92
110
|
|
|
111
|
+
const DOMAIN_RECORD_STATUSES = {
|
|
112
|
+
FOUND: 'found',
|
|
113
|
+
MISSING: 'missing'
|
|
114
|
+
};
|
|
115
|
+
const DOMAIN_DMARC_STAGES = {
|
|
116
|
+
MISSING: 'missing',
|
|
117
|
+
INVALID: 'invalid',
|
|
118
|
+
MONITOR: 'monitor',
|
|
119
|
+
QUARANTINE: 'quarantine',
|
|
120
|
+
REJECT: 'reject'
|
|
121
|
+
};
|
|
122
|
+
|
|
93
123
|
const MESSAGE_ENCRYPTION_FORMATS = {
|
|
94
124
|
PGP_MIME: 'pgp-mime',
|
|
95
125
|
PGP_SIGNED: 'pgp-signed',
|
|
@@ -155,6 +185,10 @@ const FILE_USAGES = {
|
|
|
155
185
|
LINKED: 'linked',
|
|
156
186
|
SCHEDULED: 'scheduled'
|
|
157
187
|
};
|
|
188
|
+
const FILE_VISIBILITIES = {
|
|
189
|
+
PUBLIC: 'public',
|
|
190
|
+
PRIVATE: 'private'
|
|
191
|
+
};
|
|
158
192
|
|
|
159
193
|
const HTTP_METHODS = {
|
|
160
194
|
GET: 'GET',
|
|
@@ -535,6 +569,8 @@ const PATHS = {
|
|
|
535
569
|
TRACKING_STATS: '/tracking/stats',
|
|
536
570
|
CALENDAR_EVENTS: '/calendar/events',
|
|
537
571
|
SETTINGS: '/settings',
|
|
572
|
+
SECURITY_STEP_UP: '/security/step-up',
|
|
573
|
+
SECURITY_STEP_UP_VERIFY: '/security/step-up/verify',
|
|
538
574
|
ROLES: '/roles',
|
|
539
575
|
ROLES_PERMISSIONS: '/roles/permissions',
|
|
540
576
|
MEMBERS: '/members',
|
|
@@ -593,6 +629,7 @@ const SEGMENTS = {
|
|
|
593
629
|
TRASH: 'trash',
|
|
594
630
|
UNSNOOZE: 'unsnooze',
|
|
595
631
|
UPLOAD: 'upload',
|
|
632
|
+
VERIFY: 'verify',
|
|
596
633
|
VERSIONS: 'versions'
|
|
597
634
|
};
|
|
598
635
|
|
|
@@ -689,6 +726,20 @@ const API_SCOPES = {
|
|
|
689
726
|
KEYS_MANAGE: 'keys:manage'
|
|
690
727
|
};
|
|
691
728
|
|
|
729
|
+
const STEP_UP_METHODS = {
|
|
730
|
+
EMAIL: 'email',
|
|
731
|
+
TOTP: 'totp'
|
|
732
|
+
};
|
|
733
|
+
const STEP_UP_ERROR_CODES = {
|
|
734
|
+
STEP_UP_REQUIRED: 'step_up_required',
|
|
735
|
+
STEP_UP_NOT_APPLICABLE: 'step_up_not_applicable',
|
|
736
|
+
STEP_UP_THROTTLED: 'step_up_throttled',
|
|
737
|
+
STEP_UP_UNDELIVERABLE: 'step_up_undeliverable',
|
|
738
|
+
STEP_UP_CODE_INVALID: 'step_up_code_invalid',
|
|
739
|
+
STEP_UP_CODE_EXPIRED: 'step_up_code_expired',
|
|
740
|
+
STEP_UP_LOCKED: 'step_up_locked'
|
|
741
|
+
};
|
|
742
|
+
|
|
692
743
|
const SUPPRESSION_REASONS = {
|
|
693
744
|
BOUNCE: 'bounce',
|
|
694
745
|
COMPLAINT: 'complaint',
|
|
@@ -761,7 +812,12 @@ const WEBHOOK_SIGNATURE_HEADERS = WEBHOOK_HEADERS;
|
|
|
761
812
|
const CLIENT_REGISTRY_KEY = Symbol.for('openemail.sdk.client.v1');
|
|
762
813
|
const CLIENT_REGISTRY = globalThis;
|
|
763
814
|
|
|
764
|
-
const
|
|
815
|
+
const LOOPBACK_HOSTNAMES = ['localhost', '::1', '[::1]'];
|
|
816
|
+
const LOOPBACK_IPV4_PATTERN = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
817
|
+
const LOOPBACK_IPV4_ADDRESS = '127.0.0.1';
|
|
818
|
+
const LOOPBACK_IPV6_ADDRESS = '[::1]';
|
|
819
|
+
const UNSPECIFIED_HOSTNAMES = ['0.0.0.0', '::', '[::]'];
|
|
820
|
+
const LOCAL_HOSTS = [...LOOPBACK_HOSTNAMES, LOOPBACK_IPV4_ADDRESS, ...UNSPECIFIED_HOSTNAMES];
|
|
765
821
|
const ENDPOINT_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
766
822
|
|
|
767
823
|
const RETRYABLE_STATUSES = new Set(RETRY.STATUSES);
|
|
@@ -870,13 +926,36 @@ const RETRYABLE_METHODS = new Set(RETRY.METHODS);
|
|
|
870
926
|
scopes: [API_SCOPES.WEBHOOKS_READ]});
|
|
871
927
|
|
|
872
928
|
const IsApiKey = (value) => typeof value === 'string' && (value.startsWith(API_KEY_PREFIXES.LIVE) || value.startsWith(API_KEY_PREFIXES.TEST));
|
|
873
|
-
const
|
|
929
|
+
const IsAccessToken = (value) => typeof value === 'string'
|
|
930
|
+
&& value.length > 0
|
|
931
|
+
&& value.length <= ACCESS_TOKEN_RULES.MAX_LENGTH
|
|
932
|
+
&& !value.startsWith(ACCESS_TOKEN_RULES.RESERVED_PREFIX);
|
|
933
|
+
const IsPresent = (value) => value !== undefined && value !== null && value !== '';
|
|
934
|
+
const ModeOf = (apiKey) => apiKey?.startsWith(API_KEY_PREFIXES.TEST) ? API_KEY_MODES.TEST : API_KEY_MODES.LIVE;
|
|
874
935
|
const AssertApiKey = (apiKey, where) => {
|
|
875
936
|
if (!apiKey)
|
|
876
937
|
throw new Error(CLIENT_MESSAGES.API_KEY_REQUIRED);
|
|
877
938
|
if (!IsApiKey(apiKey))
|
|
878
939
|
throw new Error(`${where} ${CLIENT_MESSAGES.API_KEY_SHAPE}`);
|
|
879
940
|
};
|
|
941
|
+
const AssertAccessToken = (accessToken, where) => {
|
|
942
|
+
if (typeof accessToken === 'function')
|
|
943
|
+
return;
|
|
944
|
+
if (!IsAccessToken(accessToken))
|
|
945
|
+
throw new Error(`${where} ${CLIENT_MESSAGES.ACCESS_TOKEN_SHAPE}`);
|
|
946
|
+
};
|
|
947
|
+
const AssertCredential = (apiKey, accessToken, required) => {
|
|
948
|
+
const withKey = IsPresent(apiKey);
|
|
949
|
+
const withToken = IsPresent(accessToken);
|
|
950
|
+
if (withKey && withToken)
|
|
951
|
+
throw new Error(CLIENT_MESSAGES.CREDENTIAL_CONFLICT);
|
|
952
|
+
if (withKey)
|
|
953
|
+
AssertApiKey(apiKey, '{ apiKey }');
|
|
954
|
+
else if (withToken)
|
|
955
|
+
AssertAccessToken(accessToken, '{ accessToken }');
|
|
956
|
+
else if (required)
|
|
957
|
+
throw new Error(CLIENT_MESSAGES.CREDENTIAL_REQUIRED);
|
|
958
|
+
};
|
|
880
959
|
|
|
881
960
|
const IsBrowser = () => {
|
|
882
961
|
const scope = globalThis;
|
|
@@ -914,6 +993,9 @@ class OpenEmailApiError extends Error {
|
|
|
914
993
|
get isScopeMissing() {
|
|
915
994
|
return this.code === ERROR_CODES.INSUFFICIENT_SCOPE;
|
|
916
995
|
}
|
|
996
|
+
get isStepUpRequired() {
|
|
997
|
+
return this.code === STEP_UP_ERROR_CODES.STEP_UP_REQUIRED;
|
|
998
|
+
}
|
|
917
999
|
get isInvalidRequest() {
|
|
918
1000
|
return this.type === ERROR_TYPES.INVALID_REQUEST_ERROR;
|
|
919
1001
|
}
|
|
@@ -1013,8 +1095,25 @@ const Backoff = (attempt) => {
|
|
|
1013
1095
|
return Math.round(ceiling * (0.5 + Math.random() * 0.5));
|
|
1014
1096
|
};
|
|
1015
1097
|
|
|
1016
|
-
const
|
|
1098
|
+
const PATH_SHAPE = 'is not a usable request path. Pass a path on the API that begins with a single "/", such as "/threads". Anything else could send the request, and the credential it carries, to another host.';
|
|
1099
|
+
const OFF_ORIGIN = 'leaves the API origin, so the request was not sent. Pass a path on the API that begins with a single "/", such as "/threads".';
|
|
1100
|
+
const ApiUrl = (baseUrl, path) => {
|
|
1101
|
+
if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//') || path.startsWith('/\\')) {
|
|
1102
|
+
throw new Error(`${JSON.stringify(path)} ${PATH_SHAPE}`);
|
|
1103
|
+
}
|
|
1017
1104
|
const url = `${baseUrl.replace(/\/+$/, '')}${path}`;
|
|
1105
|
+
let origin = null;
|
|
1106
|
+
try {
|
|
1107
|
+
origin = new URL(url).origin;
|
|
1108
|
+
}
|
|
1109
|
+
catch { }
|
|
1110
|
+
if (origin === null || origin !== new URL(baseUrl).origin)
|
|
1111
|
+
throw new Error(`${JSON.stringify(path)} ${OFF_ORIGIN}`);
|
|
1112
|
+
return url;
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
const BuildUrl = (baseUrl, path, query) => {
|
|
1116
|
+
const url = ApiUrl(baseUrl, path);
|
|
1018
1117
|
if (!query)
|
|
1019
1118
|
return url;
|
|
1020
1119
|
const params = new URLSearchParams();
|
|
@@ -1027,6 +1126,8 @@ const BuildUrl = (baseUrl, path, query) => {
|
|
|
1027
1126
|
return serialized ? `${url}?${serialized}` : url;
|
|
1028
1127
|
};
|
|
1029
1128
|
|
|
1129
|
+
const CleartextCredentialMessage = (baseUrl) => `Refused to send a credential to ${baseUrl} over plain http, where anyone on the network can read it. Use an https base URL. Plain http is accepted only for a server on this machine: localhost, a 127.x.x.x address or ::1.`;
|
|
1130
|
+
|
|
1030
1131
|
const Deadline = (timeoutMs, signal) => {
|
|
1031
1132
|
const controller = new AbortController();
|
|
1032
1133
|
let expired = false;
|
|
@@ -1065,6 +1166,23 @@ const IdempotencyKey = () => {
|
|
|
1065
1166
|
return `${IDEMPOTENCY_KEY_PREFIX}${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`;
|
|
1066
1167
|
};
|
|
1067
1168
|
|
|
1169
|
+
const IsLoopbackHost = (hostname) => {
|
|
1170
|
+
const host = hostname.toLowerCase();
|
|
1171
|
+
return LOOPBACK_HOSTNAMES.includes(host) || LOOPBACK_IPV4_PATTERN.test(host);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const IsSecureOrigin = (url) => {
|
|
1175
|
+
try {
|
|
1176
|
+
const parsed = new URL(url);
|
|
1177
|
+
return parsed.protocol === 'https:' || (parsed.protocol === 'http:' && IsLoopbackHost(parsed.hostname));
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const IsUnspecifiedHost = (hostname) => UNSPECIFIED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
1185
|
+
|
|
1068
1186
|
const RetryAfter = (value) => {
|
|
1069
1187
|
if (!value)
|
|
1070
1188
|
return undefined;
|
|
@@ -1123,6 +1241,13 @@ const ToErrorProps = (response, body, rawText, retryAfterSeconds) => {
|
|
|
1123
1241
|
};
|
|
1124
1242
|
};
|
|
1125
1243
|
|
|
1244
|
+
const Substitute = (url) => {
|
|
1245
|
+
const suggestion = new URL(url.href);
|
|
1246
|
+
suggestion.hostname = url.hostname.startsWith('[') ? LOOPBACK_IPV6_ADDRESS : LOOPBACK_IPV4_ADDRESS;
|
|
1247
|
+
return suggestion.href.replace(/\/+$/, '');
|
|
1248
|
+
};
|
|
1249
|
+
const UnspecifiedHostMessage = (baseUrl, url) => `${JSON.stringify(baseUrl)} names ${url.hostname}, an address a server listens on, not one to send requests to. For a server on this machine, use ${Substitute(url)}.`;
|
|
1250
|
+
|
|
1126
1251
|
const Describe = (error) => error instanceof Error ? error.message : String(error);
|
|
1127
1252
|
const ParseJson = (text) => {
|
|
1128
1253
|
if (!text)
|
|
@@ -1134,34 +1259,45 @@ const ParseJson = (text) => {
|
|
|
1134
1259
|
return undefined;
|
|
1135
1260
|
}
|
|
1136
1261
|
};
|
|
1137
|
-
const
|
|
1138
|
-
const trimmed = value.trim().replace(/\/+$/, '');
|
|
1262
|
+
const ParseBaseUrl = (value) => {
|
|
1139
1263
|
try {
|
|
1140
|
-
const parsed = new URL(
|
|
1141
|
-
|
|
1142
|
-
|
|
1264
|
+
const parsed = new URL(value);
|
|
1265
|
+
return parsed.protocol === 'https:' || parsed.protocol === 'http:' ? parsed : undefined;
|
|
1266
|
+
}
|
|
1267
|
+
catch {
|
|
1268
|
+
return undefined;
|
|
1143
1269
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1270
|
+
};
|
|
1271
|
+
const ValidBaseUrl = (value) => {
|
|
1272
|
+
const trimmed = value.trim().replace(/\/+$/, '');
|
|
1273
|
+
const parsed = ParseBaseUrl(trimmed);
|
|
1274
|
+
if (!parsed)
|
|
1275
|
+
throw new Error(`${JSON.stringify(value)} ${CLIENT_MESSAGES.BASE_URL_SHAPE}`);
|
|
1276
|
+
if (IsUnspecifiedHost(parsed.hostname))
|
|
1277
|
+
throw new Error(UnspecifiedHostMessage(value, parsed));
|
|
1278
|
+
return trimmed;
|
|
1146
1279
|
};
|
|
1147
1280
|
class Transport {
|
|
1148
1281
|
#apiKey;
|
|
1282
|
+
#accessToken;
|
|
1149
1283
|
#inboxToken;
|
|
1150
1284
|
baseUrl;
|
|
1285
|
+
#secureOrigin;
|
|
1151
1286
|
fetchImpl;
|
|
1152
1287
|
maxRetries;
|
|
1153
1288
|
timeoutMs;
|
|
1154
1289
|
userAgent;
|
|
1155
1290
|
headers;
|
|
1156
1291
|
constructor(options) {
|
|
1157
|
-
|
|
1158
|
-
AssertApiKey(options.apiKey, '{ apiKey }');
|
|
1292
|
+
AssertCredential(options.apiKey, options.accessToken, false);
|
|
1159
1293
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
1160
1294
|
if (!fetchImpl)
|
|
1161
1295
|
throw new Error(CLIENT_MESSAGES.NO_FETCH);
|
|
1162
|
-
this.#apiKey = options.apiKey;
|
|
1296
|
+
this.#apiKey = IsPresent(options.apiKey) ? options.apiKey : undefined;
|
|
1297
|
+
this.#accessToken = IsPresent(options.accessToken) ? options.accessToken : undefined;
|
|
1163
1298
|
this.#inboxToken = options.inboxToken;
|
|
1164
1299
|
this.baseUrl = ValidBaseUrl(options.baseUrl ?? DEFAULTS.BASE_URL);
|
|
1300
|
+
this.#secureOrigin = IsSecureOrigin(this.baseUrl);
|
|
1165
1301
|
this.fetchImpl = fetchImpl;
|
|
1166
1302
|
this.maxRetries = Math.max(0, options.maxRetries ?? DEFAULTS.MAX_RETRIES);
|
|
1167
1303
|
this.timeoutMs = options.timeoutMs ?? DEFAULTS.TIMEOUT_MS;
|
|
@@ -1173,7 +1309,7 @@ class Transport {
|
|
|
1173
1309
|
async request(path, options = {}) {
|
|
1174
1310
|
const method = (options.method ?? HTTP_METHODS.GET).toUpperCase();
|
|
1175
1311
|
const url = BuildUrl(this.baseUrl, path, options.query);
|
|
1176
|
-
const headers = this.headersFor(options);
|
|
1312
|
+
const headers = await this.headersFor(options);
|
|
1177
1313
|
const body = options.raw !== undefined
|
|
1178
1314
|
? options.raw
|
|
1179
1315
|
: options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
@@ -1205,7 +1341,16 @@ class Transport {
|
|
|
1205
1341
|
throw new OpenEmailApiError(ToErrorProps(response, ParseJson(text), text, retryAfter));
|
|
1206
1342
|
}
|
|
1207
1343
|
}
|
|
1208
|
-
|
|
1344
|
+
async resolveAccessToken() {
|
|
1345
|
+
const source = this.#accessToken;
|
|
1346
|
+
if (typeof source !== 'function')
|
|
1347
|
+
return source;
|
|
1348
|
+
const token = await source();
|
|
1349
|
+
if (!IsAccessToken(token))
|
|
1350
|
+
throw new Error(`${CLIENT_MESSAGES.ACCESS_TOKEN_PROVIDED} ${CLIENT_MESSAGES.ACCESS_TOKEN_SHAPE}`);
|
|
1351
|
+
return token;
|
|
1352
|
+
}
|
|
1353
|
+
async headersFor(options) {
|
|
1209
1354
|
const headers = {
|
|
1210
1355
|
...this.headers,
|
|
1211
1356
|
[HEADER_KEYS.ACCEPT]: options.accept ?? CONTENT_TYPES.JSON
|
|
@@ -1219,7 +1364,10 @@ class Transport {
|
|
|
1219
1364
|
if (options.anonymous !== true) {
|
|
1220
1365
|
if (options.apiKey !== undefined)
|
|
1221
1366
|
AssertApiKey(options.apiKey, '{ apiKey } on this call');
|
|
1222
|
-
const
|
|
1367
|
+
const direct = options.inboxToken ?? options.apiKey ?? this.#inboxToken ?? this.#apiKey;
|
|
1368
|
+
if ((direct ?? this.#accessToken) && !this.#secureOrigin)
|
|
1369
|
+
throw new Error(CleartextCredentialMessage(this.baseUrl));
|
|
1370
|
+
const credential = direct ?? await this.resolveAccessToken();
|
|
1223
1371
|
if (credential)
|
|
1224
1372
|
headers[HEADER_KEYS.AUTHORIZATION] = `Bearer ${credential}`;
|
|
1225
1373
|
}
|
|
@@ -1342,6 +1490,9 @@ const BroadcastPath = (id) => `${PATHS.BROADCASTS}/${Segment(id)}`;
|
|
|
1342
1490
|
const BroadcastChildPath = (id, segment) => `${BroadcastPath(id)}/${segment}`;
|
|
1343
1491
|
const BroadcastRecipientPath = (id, emailId) => `${BroadcastChildPath(id, SEGMENTS.RECIPIENTS)}/${Segment(emailId)}`;
|
|
1344
1492
|
const DomainPath = (id) => `${PATHS.DOMAINS}/${Segment(id)}`;
|
|
1493
|
+
const DomainChildPath = (id, segment) => `${DomainPath(id)}/${segment}`;
|
|
1494
|
+
const DomainAddressesPath = (id) => `${DomainPath(id)}/${SEGMENTS.ADDRESSES}`;
|
|
1495
|
+
const DomainAddressPath = (id, addressId) => `${DomainAddressesPath(id)}/${Segment(addressId)}`;
|
|
1345
1496
|
const WebhookPath = (id) => `${PATHS.WEBHOOKS}/${Segment(id)}`;
|
|
1346
1497
|
const WebhookChildPath = (id, segment) => `${WebhookPath(id)}/${segment}`;
|
|
1347
1498
|
const WebhookDeliveryPath = (id, deliveryId) => `${WebhookChildPath(id, SEGMENTS.DELIVERIES)}/${Segment(deliveryId)}`;
|
|
@@ -1414,7 +1565,8 @@ const Addresses = (transport) => {
|
|
|
1414
1565
|
const MemberQuery = (options) => ({
|
|
1415
1566
|
[QUERY_KEYS.Q]: options.q,
|
|
1416
1567
|
[QUERY_KEYS.SOURCE]: options.source,
|
|
1417
|
-
[QUERY_KEYS.SORT]: options.sort
|
|
1568
|
+
[QUERY_KEYS.SORT]: options.sort,
|
|
1569
|
+
[QUERY_KEYS.STATUS]: options.statuses?.length ? options.statuses.join(LIST_SEPARATOR) : undefined
|
|
1418
1570
|
});
|
|
1419
1571
|
const GrowthQuery = (options) => ({
|
|
1420
1572
|
[QUERY_KEYS.AUDIENCE_IDS]: options.audienceIds?.join(LIST_SEPARATOR),
|
|
@@ -1732,12 +1884,52 @@ const Domains = (transport) => ({
|
|
|
1732
1884
|
listAll: (options = {}) => CollectAll(transport, PATHS.DOMAINS, CURSOR_STYLES.CURSOR, options),
|
|
1733
1885
|
iterate: (options = {}) => IteratePages(transport, PATHS.DOMAINS, CURSOR_STYLES.CURSOR, options),
|
|
1734
1886
|
get: (id, options = {}) => transport.request(DomainPath(id), { signal: options.signal, apiKey: options.apiKey }),
|
|
1887
|
+
create: (body, options = {}) => transport.request(PATHS.DOMAINS, {
|
|
1888
|
+
method: HTTP_METHODS.POST,
|
|
1889
|
+
body,
|
|
1890
|
+
signal: options.signal,
|
|
1891
|
+
apiKey: options.apiKey
|
|
1892
|
+
}),
|
|
1893
|
+
verify: (id, options = {}) => transport.request(DomainChildPath(id, SEGMENTS.VERIFY), {
|
|
1894
|
+
method: HTTP_METHODS.POST,
|
|
1895
|
+
repeatable: true,
|
|
1896
|
+
signal: options.signal,
|
|
1897
|
+
apiKey: options.apiKey
|
|
1898
|
+
}),
|
|
1735
1899
|
update: (id, patch, options = {}) => transport.request(DomainPath(id), {
|
|
1736
1900
|
method: HTTP_METHODS.PATCH,
|
|
1737
1901
|
body: patch,
|
|
1738
1902
|
repeatable: true,
|
|
1739
1903
|
signal: options.signal,
|
|
1740
1904
|
apiKey: options.apiKey
|
|
1905
|
+
}),
|
|
1906
|
+
delete: (id, options = {}) => transport.request(DomainPath(id), {
|
|
1907
|
+
method: HTTP_METHODS.DELETE,
|
|
1908
|
+
signal: options.signal,
|
|
1909
|
+
apiKey: options.apiKey
|
|
1910
|
+
}),
|
|
1911
|
+
listAddresses: (id, options = {}) => FetchPage(transport, DomainAddressesPath(id), CURSOR_STYLES.CURSOR, options),
|
|
1912
|
+
listAllAddresses: (id, options = {}) => CollectAll(transport, DomainAddressesPath(id), CURSOR_STYLES.CURSOR, options),
|
|
1913
|
+
iterateAddresses: (id, options = {}) => IteratePages(transport, DomainAddressesPath(id), CURSOR_STYLES.CURSOR, options),
|
|
1914
|
+
createAddress: (id, body, options = {}) => transport.request(DomainAddressesPath(id), {
|
|
1915
|
+
method: HTTP_METHODS.POST,
|
|
1916
|
+
body,
|
|
1917
|
+
repeatable: true,
|
|
1918
|
+
signal: options.signal,
|
|
1919
|
+
apiKey: options.apiKey
|
|
1920
|
+
}),
|
|
1921
|
+
getAddress: (id, addressId, options = {}) => transport.request(DomainAddressPath(id, addressId), { signal: options.signal, apiKey: options.apiKey }),
|
|
1922
|
+
updateAddress: (id, addressId, patch, options = {}) => transport.request(DomainAddressPath(id, addressId), {
|
|
1923
|
+
method: HTTP_METHODS.PATCH,
|
|
1924
|
+
body: patch,
|
|
1925
|
+
repeatable: true,
|
|
1926
|
+
signal: options.signal,
|
|
1927
|
+
apiKey: options.apiKey
|
|
1928
|
+
}),
|
|
1929
|
+
deleteAddress: (id, addressId, options = {}) => transport.request(DomainAddressPath(id, addressId), {
|
|
1930
|
+
method: HTTP_METHODS.DELETE,
|
|
1931
|
+
signal: options.signal,
|
|
1932
|
+
apiKey: options.apiKey
|
|
1741
1933
|
})
|
|
1742
1934
|
});
|
|
1743
1935
|
|
|
@@ -2233,6 +2425,22 @@ const Rules = (transport) => ({
|
|
|
2233
2425
|
iterateRuns: (options = {}) => IteratePages(transport, PATHS.RULES_RUNS, CURSOR_STYLES.CURSOR, options, RunsQuery(options))
|
|
2234
2426
|
});
|
|
2235
2427
|
|
|
2428
|
+
const Security = (transport) => ({
|
|
2429
|
+
stepUpStatus: (options = {}) => transport.request(PATHS.SECURITY_STEP_UP, { signal: options.signal, apiKey: options.apiKey }),
|
|
2430
|
+
beginStepUp: (body = {}, options = {}) => transport.request(PATHS.SECURITY_STEP_UP, {
|
|
2431
|
+
method: HTTP_METHODS.POST,
|
|
2432
|
+
body,
|
|
2433
|
+
signal: options.signal,
|
|
2434
|
+
apiKey: options.apiKey
|
|
2435
|
+
}),
|
|
2436
|
+
verifyStepUp: (body, options = {}) => transport.request(PATHS.SECURITY_STEP_UP_VERIFY, {
|
|
2437
|
+
method: HTTP_METHODS.POST,
|
|
2438
|
+
body,
|
|
2439
|
+
signal: options.signal,
|
|
2440
|
+
apiKey: options.apiKey
|
|
2441
|
+
})
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2236
2444
|
const Settings = (transport) => ({
|
|
2237
2445
|
get: (options = {}) => transport.request(PATHS.SETTINGS, {
|
|
2238
2446
|
query: { [QUERY_KEYS.ADDRESS]: options.address },
|
|
@@ -2621,6 +2829,7 @@ class OpenEmail {
|
|
|
2621
2829
|
transport;
|
|
2622
2830
|
mode;
|
|
2623
2831
|
me;
|
|
2832
|
+
security;
|
|
2624
2833
|
keys;
|
|
2625
2834
|
addresses;
|
|
2626
2835
|
languages;
|
|
@@ -2647,12 +2856,13 @@ class OpenEmail {
|
|
|
2647
2856
|
tempMail;
|
|
2648
2857
|
constructor(configuration) {
|
|
2649
2858
|
const options = typeof configuration === 'string' ? { apiKey: configuration } : configuration;
|
|
2650
|
-
|
|
2859
|
+
AssertCredential(options.apiKey, options.accessToken, true);
|
|
2651
2860
|
if (IsBrowser() && options.dangerouslyAllowBrowser !== true)
|
|
2652
2861
|
throw new Error(BROWSER_REFUSAL);
|
|
2653
2862
|
this.transport = new Transport(options);
|
|
2654
2863
|
this.mode = ModeOf(options.apiKey);
|
|
2655
2864
|
this.me = Me(this.transport);
|
|
2865
|
+
this.security = Security(this.transport);
|
|
2656
2866
|
this.keys = Keys(this.transport);
|
|
2657
2867
|
this.addresses = Addresses(this.transport);
|
|
2658
2868
|
this.languages = Languages(this.transport);
|
|
@@ -2694,6 +2904,19 @@ const FromEnvironment = (name) => {
|
|
|
2694
2904
|
}
|
|
2695
2905
|
};
|
|
2696
2906
|
|
|
2907
|
+
const IsLocalHost = (hostname) => {
|
|
2908
|
+
const host = hostname.toLowerCase();
|
|
2909
|
+
return LOCAL_HOSTS.includes(host) || LOOPBACK_IPV4_PATTERN.test(host);
|
|
2910
|
+
};
|
|
2911
|
+
|
|
2912
|
+
const HostnameOf = (authority) => {
|
|
2913
|
+
try {
|
|
2914
|
+
return new URL(`http://${authority}`).hostname;
|
|
2915
|
+
}
|
|
2916
|
+
catch {
|
|
2917
|
+
return '';
|
|
2918
|
+
}
|
|
2919
|
+
};
|
|
2697
2920
|
const NormaliseEndpoint = (value) => {
|
|
2698
2921
|
if (!value)
|
|
2699
2922
|
return undefined;
|
|
@@ -2702,15 +2925,18 @@ const NormaliseEndpoint = (value) => {
|
|
|
2702
2925
|
return undefined;
|
|
2703
2926
|
if (ENDPOINT_SCHEME_PATTERN.test(trimmed))
|
|
2704
2927
|
return trimmed;
|
|
2705
|
-
|
|
2706
|
-
return `${LOCAL_HOSTS.includes(host) ? 'http' : 'https'}://${trimmed}`;
|
|
2928
|
+
return `${IsLocalHost(HostnameOf(trimmed)) ? 'http' : 'https'}://${trimmed}`;
|
|
2707
2929
|
};
|
|
2708
2930
|
|
|
2709
2931
|
const CreateClient = (configuration = {}) => {
|
|
2710
2932
|
const options = typeof configuration === 'string' ? { apiKey: configuration } : configuration;
|
|
2933
|
+
const explicit = options.apiKey !== undefined || options.accessToken !== undefined;
|
|
2934
|
+
const apiKey = explicit ? options.apiKey : FromEnvironment(ENV_VARS.API_KEY);
|
|
2935
|
+
const accessToken = explicit || apiKey !== undefined ? options.accessToken : FromEnvironment(ENV_VARS.ACCESS_TOKEN);
|
|
2711
2936
|
return new OpenEmail({
|
|
2712
2937
|
...options,
|
|
2713
|
-
apiKey
|
|
2938
|
+
apiKey,
|
|
2939
|
+
accessToken,
|
|
2714
2940
|
baseUrl: NormaliseEndpoint(options.baseUrl ?? FromEnvironment(ENV_VARS.BASE_URL))
|
|
2715
2941
|
});
|
|
2716
2942
|
};
|
|
@@ -2718,6 +2944,7 @@ const CreateClient = (configuration = {}) => {
|
|
|
2718
2944
|
const CreateTempMail = (options = {}) => TempMail(new Transport({
|
|
2719
2945
|
...options,
|
|
2720
2946
|
apiKey: undefined,
|
|
2947
|
+
accessToken: undefined,
|
|
2721
2948
|
baseUrl: NormaliseEndpoint(options.baseUrl ?? FromEnvironment(ENV_VARS.BASE_URL))
|
|
2722
2949
|
}));
|
|
2723
2950
|
|
|
@@ -2845,22 +3072,28 @@ const createTempMail = CreateTempMail;
|
|
|
2845
3072
|
const verifyWebhookSignature = VerifyWebhookSignature;
|
|
2846
3073
|
const toBase64 = ToBase64;
|
|
2847
3074
|
const isApiKey = IsApiKey;
|
|
3075
|
+
const isAccessToken = IsAccessToken;
|
|
2848
3076
|
const isSealed = IsSealed;
|
|
2849
3077
|
const resolveLanguage = ResolveLanguage;
|
|
2850
3078
|
const languageByCode = LanguageByCode;
|
|
2851
3079
|
const isRtlLanguage = IsRtlLanguage;
|
|
2852
3080
|
|
|
2853
3081
|
exports.API_SCOPES = API_SCOPES;
|
|
3082
|
+
exports.AUDIENCE_MEMBER_STATUSES = AUDIENCE_MEMBER_STATUSES;
|
|
2854
3083
|
exports.BROADCAST_RECIPIENT_FILTERS = BROADCAST_RECIPIENT_FILTERS;
|
|
2855
3084
|
exports.BROADCAST_STATUSES = BROADCAST_STATUSES;
|
|
2856
3085
|
exports.CONTACT_BLOCK_LISTS = CONTACT_BLOCK_LISTS;
|
|
2857
3086
|
exports.CONTACT_PHOTO_TYPES = CONTACT_PHOTO_TYPES;
|
|
2858
3087
|
exports.CONTACT_THREAD_SORTS = CONTACT_THREAD_SORTS;
|
|
3088
|
+
exports.CREDENTIAL_KINDS = CREDENTIAL_KINDS;
|
|
3089
|
+
exports.DOMAIN_DMARC_STAGES = DOMAIN_DMARC_STAGES;
|
|
3090
|
+
exports.DOMAIN_RECORD_STATUSES = DOMAIN_RECORD_STATUSES;
|
|
2859
3091
|
exports.ERROR_TYPES = ERROR_TYPES;
|
|
2860
3092
|
exports.FILE_DIRECTIONS = FILE_DIRECTIONS;
|
|
2861
3093
|
exports.FILE_KINDS = FILE_KINDS;
|
|
2862
3094
|
exports.FILE_SORTS = FILE_SORTS;
|
|
2863
3095
|
exports.FILE_USAGES = FILE_USAGES;
|
|
3096
|
+
exports.FILE_VISIBILITIES = FILE_VISIBILITIES;
|
|
2864
3097
|
exports.LANGUAGES = LANGUAGES;
|
|
2865
3098
|
exports.MESSAGE_ENCRYPTION_FORMATS = MESSAGE_ENCRYPTION_FORMATS;
|
|
2866
3099
|
exports.OpenEmail = OpenEmail;
|
|
@@ -2874,6 +3107,8 @@ exports.PROVIDER_IMPORT_STATUSES = PROVIDER_IMPORT_STATUSES;
|
|
|
2874
3107
|
exports.RULE_ACTIONS = RULE_ACTIONS;
|
|
2875
3108
|
exports.RULE_FIELDS = RULE_FIELDS;
|
|
2876
3109
|
exports.RULE_OPERATORS = RULE_OPERATORS;
|
|
3110
|
+
exports.STEP_UP_ERROR_CODES = STEP_UP_ERROR_CODES;
|
|
3111
|
+
exports.STEP_UP_METHODS = STEP_UP_METHODS;
|
|
2877
3112
|
exports.SUPPRESSION_REASONS = SUPPRESSION_REASONS;
|
|
2878
3113
|
exports.THREAD_SORTS = THREAD_SORTS;
|
|
2879
3114
|
exports.VERSION = VERSION;
|
|
@@ -2886,6 +3121,7 @@ exports.createTempMail = createTempMail;
|
|
|
2886
3121
|
exports.default = OpenEmail;
|
|
2887
3122
|
exports.getClient = getClient;
|
|
2888
3123
|
exports.init = init;
|
|
3124
|
+
exports.isAccessToken = isAccessToken;
|
|
2889
3125
|
exports.isApiKey = isApiKey;
|
|
2890
3126
|
exports.isRtlLanguage = isRtlLanguage;
|
|
2891
3127
|
exports.isSealed = isSealed;
|