@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 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 178 endpoints, 259 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.
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 key and work in a browser.
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`, for a server you run yourself. 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.
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.4";
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 the mailbox. Anything',
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
- 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.',
64
- BASE_URL_SHAPE: 'is not a usable base URL. Pass an origin such as "https://api.openemail.uk", or the http origin of a server you run yourself.',
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 LOCAL_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
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 ModeOf = (apiKey) => apiKey.startsWith(API_KEY_PREFIXES.TEST) ? API_KEY_MODES.TEST : API_KEY_MODES.LIVE;
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 BuildUrl = (baseUrl, path, query) => {
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 ValidBaseUrl = (value) => {
1138
- const trimmed = value.trim().replace(/\/+$/, '');
1262
+ const ParseBaseUrl = (value) => {
1139
1263
  try {
1140
- const parsed = new URL(trimmed);
1141
- if (parsed.protocol === 'https:' || parsed.protocol === 'http:')
1142
- return trimmed;
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
- catch { }
1145
- throw new Error(`${JSON.stringify(value)} ${CLIENT_MESSAGES.BASE_URL_SHAPE}`);
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
- if (options.apiKey !== undefined)
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
- headersFor(options) {
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 credential = options.inboxToken ?? options.apiKey ?? this.#inboxToken ?? this.#apiKey;
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
- AssertApiKey(options.apiKey, '{ apiKey }');
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
- const host = trimmed.split(':')[0];
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: options.apiKey ?? FromEnvironment(ENV_VARS.API_KEY),
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;