@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/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ const AUDIENCE_MEMBER_STATUSES = {
2
+ SUBSCRIBED: 'subscribed',
3
+ UNSUBSCRIBED: 'unsubscribed'
4
+ };
5
+
1
6
  const BROADCAST_STATUSES = {
2
7
  SCHEDULED: 'scheduled',
3
8
  QUEUED: 'queued',
@@ -19,7 +24,7 @@ const BROADCAST_RECIPIENT_FILTERS = {
19
24
  UNSUBSCRIBED: 'unsubscribed'
20
25
  };
21
26
 
22
- var version = "0.0.4";
27
+ var version = "0.0.6";
23
28
 
24
29
  const BUILD_BASE_URL = 'https://api.openemail.uk';
25
30
 
@@ -33,8 +38,8 @@ const DEFAULTS = {
33
38
  const BROWSER_REFUSAL = [
34
39
  'OpenEmail refused to start in a browser.',
35
40
  '',
36
- ' This client carries a workspace API key that can send mail and read the mailbox. Anything',
37
- ' you ship to a browser is readable by anyone who opens devtools.',
41
+ ' This client carries a workspace API key or an OAuth access token that can send mail and read',
42
+ ' the mailbox. Anything you ship to a browser is readable by anyone who opens devtools.',
38
43
  '',
39
44
  ' Call it from a server, a serverless function or a script instead. Disposable inboxes are',
40
45
  ' the exception: createTempMail() needs no API key and is safe to use in a browser.',
@@ -44,6 +49,7 @@ const BROWSER_REFUSAL = [
44
49
  ].join('\n');
45
50
  const ENV_VARS = {
46
51
  API_KEY: 'OPENEMAIL_API_KEY',
52
+ ACCESS_TOKEN: 'OPENEMAIL_ACCESS_TOKEN',
47
53
  BASE_URL: 'OPENEMAIL_BASE_URL'
48
54
  };
49
55
  const API_KEY_PREFIXES = {
@@ -54,10 +60,22 @@ const API_KEY_MODES = {
54
60
  LIVE: 'live',
55
61
  TEST: 'test'
56
62
  };
63
+ const ACCESS_TOKEN_RULES = {
64
+ RESERVED_PREFIX: 'oe_',
65
+ MAX_LENGTH: 512
66
+ };
67
+ const CREDENTIAL_KINDS = {
68
+ API_KEY: 'apiKey',
69
+ OAUTH: 'oauth'
70
+ };
57
71
  const CLIENT_MESSAGES = {
58
72
  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.`,
59
- 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.',
60
- 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.',
73
+ 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.`,
74
+ CREDENTIAL_CONFLICT: 'Pass { apiKey } or { accessToken }, not both. Every request carries one credential, so the client cannot tell which of the two you meant.',
75
+ 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.',
76
+ 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.`,
77
+ ACCESS_TOKEN_PROVIDED: 'The value the { accessToken } function resolved to',
78
+ 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.',
61
79
  NO_FETCH: 'No global fetch is available. Pass one as { fetch }, or run on Node 20+, Bun, Deno, Cloudflare Workers or a browser.',
62
80
  EMPTY_SEGMENT: 'An id must not be empty.',
63
81
  FILENAME_REQUIRED: 'A file name is required. Pass it as { filename }, the name the file is stored and downloaded under.',
@@ -86,6 +104,18 @@ const CONTACT_PHOTO_TYPES = {
86
104
  GIF: 'image/gif'
87
105
  };
88
106
 
107
+ const DOMAIN_RECORD_STATUSES = {
108
+ FOUND: 'found',
109
+ MISSING: 'missing'
110
+ };
111
+ const DOMAIN_DMARC_STAGES = {
112
+ MISSING: 'missing',
113
+ INVALID: 'invalid',
114
+ MONITOR: 'monitor',
115
+ QUARANTINE: 'quarantine',
116
+ REJECT: 'reject'
117
+ };
118
+
89
119
  const MESSAGE_ENCRYPTION_FORMATS = {
90
120
  PGP_MIME: 'pgp-mime',
91
121
  PGP_SIGNED: 'pgp-signed',
@@ -151,6 +181,10 @@ const FILE_USAGES = {
151
181
  LINKED: 'linked',
152
182
  SCHEDULED: 'scheduled'
153
183
  };
184
+ const FILE_VISIBILITIES = {
185
+ PUBLIC: 'public',
186
+ PRIVATE: 'private'
187
+ };
154
188
 
155
189
  const HTTP_METHODS = {
156
190
  GET: 'GET',
@@ -531,6 +565,8 @@ const PATHS = {
531
565
  TRACKING_STATS: '/tracking/stats',
532
566
  CALENDAR_EVENTS: '/calendar/events',
533
567
  SETTINGS: '/settings',
568
+ SECURITY_STEP_UP: '/security/step-up',
569
+ SECURITY_STEP_UP_VERIFY: '/security/step-up/verify',
534
570
  ROLES: '/roles',
535
571
  ROLES_PERMISSIONS: '/roles/permissions',
536
572
  MEMBERS: '/members',
@@ -589,6 +625,7 @@ const SEGMENTS = {
589
625
  TRASH: 'trash',
590
626
  UNSNOOZE: 'unsnooze',
591
627
  UPLOAD: 'upload',
628
+ VERIFY: 'verify',
592
629
  VERSIONS: 'versions'
593
630
  };
594
631
 
@@ -685,6 +722,20 @@ const API_SCOPES = {
685
722
  KEYS_MANAGE: 'keys:manage'
686
723
  };
687
724
 
725
+ const STEP_UP_METHODS = {
726
+ EMAIL: 'email',
727
+ TOTP: 'totp'
728
+ };
729
+ const STEP_UP_ERROR_CODES = {
730
+ STEP_UP_REQUIRED: 'step_up_required',
731
+ STEP_UP_NOT_APPLICABLE: 'step_up_not_applicable',
732
+ STEP_UP_THROTTLED: 'step_up_throttled',
733
+ STEP_UP_UNDELIVERABLE: 'step_up_undeliverable',
734
+ STEP_UP_CODE_INVALID: 'step_up_code_invalid',
735
+ STEP_UP_CODE_EXPIRED: 'step_up_code_expired',
736
+ STEP_UP_LOCKED: 'step_up_locked'
737
+ };
738
+
688
739
  const SUPPRESSION_REASONS = {
689
740
  BOUNCE: 'bounce',
690
741
  COMPLAINT: 'complaint',
@@ -757,7 +808,12 @@ const WEBHOOK_SIGNATURE_HEADERS = WEBHOOK_HEADERS;
757
808
  const CLIENT_REGISTRY_KEY = Symbol.for('openemail.sdk.client.v1');
758
809
  const CLIENT_REGISTRY = globalThis;
759
810
 
760
- const LOCAL_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
811
+ const LOOPBACK_HOSTNAMES = ['localhost', '::1', '[::1]'];
812
+ const LOOPBACK_IPV4_PATTERN = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
813
+ const LOOPBACK_IPV4_ADDRESS = '127.0.0.1';
814
+ const LOOPBACK_IPV6_ADDRESS = '[::1]';
815
+ const UNSPECIFIED_HOSTNAMES = ['0.0.0.0', '::', '[::]'];
816
+ const LOCAL_HOSTS = [...LOOPBACK_HOSTNAMES, LOOPBACK_IPV4_ADDRESS, ...UNSPECIFIED_HOSTNAMES];
761
817
  const ENDPOINT_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
762
818
 
763
819
  const RETRYABLE_STATUSES = new Set(RETRY.STATUSES);
@@ -866,13 +922,36 @@ const RETRYABLE_METHODS = new Set(RETRY.METHODS);
866
922
  scopes: [API_SCOPES.WEBHOOKS_READ]});
867
923
 
868
924
  const IsApiKey = (value) => typeof value === 'string' && (value.startsWith(API_KEY_PREFIXES.LIVE) || value.startsWith(API_KEY_PREFIXES.TEST));
869
- const ModeOf = (apiKey) => apiKey.startsWith(API_KEY_PREFIXES.TEST) ? API_KEY_MODES.TEST : API_KEY_MODES.LIVE;
925
+ const IsAccessToken = (value) => typeof value === 'string'
926
+ && value.length > 0
927
+ && value.length <= ACCESS_TOKEN_RULES.MAX_LENGTH
928
+ && !value.startsWith(ACCESS_TOKEN_RULES.RESERVED_PREFIX);
929
+ const IsPresent = (value) => value !== undefined && value !== null && value !== '';
930
+ const ModeOf = (apiKey) => apiKey?.startsWith(API_KEY_PREFIXES.TEST) ? API_KEY_MODES.TEST : API_KEY_MODES.LIVE;
870
931
  const AssertApiKey = (apiKey, where) => {
871
932
  if (!apiKey)
872
933
  throw new Error(CLIENT_MESSAGES.API_KEY_REQUIRED);
873
934
  if (!IsApiKey(apiKey))
874
935
  throw new Error(`${where} ${CLIENT_MESSAGES.API_KEY_SHAPE}`);
875
936
  };
937
+ const AssertAccessToken = (accessToken, where) => {
938
+ if (typeof accessToken === 'function')
939
+ return;
940
+ if (!IsAccessToken(accessToken))
941
+ throw new Error(`${where} ${CLIENT_MESSAGES.ACCESS_TOKEN_SHAPE}`);
942
+ };
943
+ const AssertCredential = (apiKey, accessToken, required) => {
944
+ const withKey = IsPresent(apiKey);
945
+ const withToken = IsPresent(accessToken);
946
+ if (withKey && withToken)
947
+ throw new Error(CLIENT_MESSAGES.CREDENTIAL_CONFLICT);
948
+ if (withKey)
949
+ AssertApiKey(apiKey, '{ apiKey }');
950
+ else if (withToken)
951
+ AssertAccessToken(accessToken, '{ accessToken }');
952
+ else if (required)
953
+ throw new Error(CLIENT_MESSAGES.CREDENTIAL_REQUIRED);
954
+ };
876
955
 
877
956
  const IsBrowser = () => {
878
957
  const scope = globalThis;
@@ -910,6 +989,9 @@ class OpenEmailApiError extends Error {
910
989
  get isScopeMissing() {
911
990
  return this.code === ERROR_CODES.INSUFFICIENT_SCOPE;
912
991
  }
992
+ get isStepUpRequired() {
993
+ return this.code === STEP_UP_ERROR_CODES.STEP_UP_REQUIRED;
994
+ }
913
995
  get isInvalidRequest() {
914
996
  return this.type === ERROR_TYPES.INVALID_REQUEST_ERROR;
915
997
  }
@@ -1009,8 +1091,25 @@ const Backoff = (attempt) => {
1009
1091
  return Math.round(ceiling * (0.5 + Math.random() * 0.5));
1010
1092
  };
1011
1093
 
1012
- const BuildUrl = (baseUrl, path, query) => {
1094
+ 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.';
1095
+ 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".';
1096
+ const ApiUrl = (baseUrl, path) => {
1097
+ if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//') || path.startsWith('/\\')) {
1098
+ throw new Error(`${JSON.stringify(path)} ${PATH_SHAPE}`);
1099
+ }
1013
1100
  const url = `${baseUrl.replace(/\/+$/, '')}${path}`;
1101
+ let origin = null;
1102
+ try {
1103
+ origin = new URL(url).origin;
1104
+ }
1105
+ catch { }
1106
+ if (origin === null || origin !== new URL(baseUrl).origin)
1107
+ throw new Error(`${JSON.stringify(path)} ${OFF_ORIGIN}`);
1108
+ return url;
1109
+ };
1110
+
1111
+ const BuildUrl = (baseUrl, path, query) => {
1112
+ const url = ApiUrl(baseUrl, path);
1014
1113
  if (!query)
1015
1114
  return url;
1016
1115
  const params = new URLSearchParams();
@@ -1023,6 +1122,8 @@ const BuildUrl = (baseUrl, path, query) => {
1023
1122
  return serialized ? `${url}?${serialized}` : url;
1024
1123
  };
1025
1124
 
1125
+ 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.`;
1126
+
1026
1127
  const Deadline = (timeoutMs, signal) => {
1027
1128
  const controller = new AbortController();
1028
1129
  let expired = false;
@@ -1061,6 +1162,23 @@ const IdempotencyKey = () => {
1061
1162
  return `${IDEMPOTENCY_KEY_PREFIX}${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`;
1062
1163
  };
1063
1164
 
1165
+ const IsLoopbackHost = (hostname) => {
1166
+ const host = hostname.toLowerCase();
1167
+ return LOOPBACK_HOSTNAMES.includes(host) || LOOPBACK_IPV4_PATTERN.test(host);
1168
+ };
1169
+
1170
+ const IsSecureOrigin = (url) => {
1171
+ try {
1172
+ const parsed = new URL(url);
1173
+ return parsed.protocol === 'https:' || (parsed.protocol === 'http:' && IsLoopbackHost(parsed.hostname));
1174
+ }
1175
+ catch {
1176
+ return false;
1177
+ }
1178
+ };
1179
+
1180
+ const IsUnspecifiedHost = (hostname) => UNSPECIFIED_HOSTNAMES.includes(hostname.toLowerCase());
1181
+
1064
1182
  const RetryAfter = (value) => {
1065
1183
  if (!value)
1066
1184
  return undefined;
@@ -1119,6 +1237,13 @@ const ToErrorProps = (response, body, rawText, retryAfterSeconds) => {
1119
1237
  };
1120
1238
  };
1121
1239
 
1240
+ const Substitute = (url) => {
1241
+ const suggestion = new URL(url.href);
1242
+ suggestion.hostname = url.hostname.startsWith('[') ? LOOPBACK_IPV6_ADDRESS : LOOPBACK_IPV4_ADDRESS;
1243
+ return suggestion.href.replace(/\/+$/, '');
1244
+ };
1245
+ 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)}.`;
1246
+
1122
1247
  const Describe = (error) => error instanceof Error ? error.message : String(error);
1123
1248
  const ParseJson = (text) => {
1124
1249
  if (!text)
@@ -1130,34 +1255,45 @@ const ParseJson = (text) => {
1130
1255
  return undefined;
1131
1256
  }
1132
1257
  };
1133
- const ValidBaseUrl = (value) => {
1134
- const trimmed = value.trim().replace(/\/+$/, '');
1258
+ const ParseBaseUrl = (value) => {
1135
1259
  try {
1136
- const parsed = new URL(trimmed);
1137
- if (parsed.protocol === 'https:' || parsed.protocol === 'http:')
1138
- return trimmed;
1260
+ const parsed = new URL(value);
1261
+ return parsed.protocol === 'https:' || parsed.protocol === 'http:' ? parsed : undefined;
1262
+ }
1263
+ catch {
1264
+ return undefined;
1139
1265
  }
1140
- catch { }
1141
- throw new Error(`${JSON.stringify(value)} ${CLIENT_MESSAGES.BASE_URL_SHAPE}`);
1266
+ };
1267
+ const ValidBaseUrl = (value) => {
1268
+ const trimmed = value.trim().replace(/\/+$/, '');
1269
+ const parsed = ParseBaseUrl(trimmed);
1270
+ if (!parsed)
1271
+ throw new Error(`${JSON.stringify(value)} ${CLIENT_MESSAGES.BASE_URL_SHAPE}`);
1272
+ if (IsUnspecifiedHost(parsed.hostname))
1273
+ throw new Error(UnspecifiedHostMessage(value, parsed));
1274
+ return trimmed;
1142
1275
  };
1143
1276
  class Transport {
1144
1277
  #apiKey;
1278
+ #accessToken;
1145
1279
  #inboxToken;
1146
1280
  baseUrl;
1281
+ #secureOrigin;
1147
1282
  fetchImpl;
1148
1283
  maxRetries;
1149
1284
  timeoutMs;
1150
1285
  userAgent;
1151
1286
  headers;
1152
1287
  constructor(options) {
1153
- if (options.apiKey !== undefined)
1154
- AssertApiKey(options.apiKey, '{ apiKey }');
1288
+ AssertCredential(options.apiKey, options.accessToken, false);
1155
1289
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
1156
1290
  if (!fetchImpl)
1157
1291
  throw new Error(CLIENT_MESSAGES.NO_FETCH);
1158
- this.#apiKey = options.apiKey;
1292
+ this.#apiKey = IsPresent(options.apiKey) ? options.apiKey : undefined;
1293
+ this.#accessToken = IsPresent(options.accessToken) ? options.accessToken : undefined;
1159
1294
  this.#inboxToken = options.inboxToken;
1160
1295
  this.baseUrl = ValidBaseUrl(options.baseUrl ?? DEFAULTS.BASE_URL);
1296
+ this.#secureOrigin = IsSecureOrigin(this.baseUrl);
1161
1297
  this.fetchImpl = fetchImpl;
1162
1298
  this.maxRetries = Math.max(0, options.maxRetries ?? DEFAULTS.MAX_RETRIES);
1163
1299
  this.timeoutMs = options.timeoutMs ?? DEFAULTS.TIMEOUT_MS;
@@ -1169,7 +1305,7 @@ class Transport {
1169
1305
  async request(path, options = {}) {
1170
1306
  const method = (options.method ?? HTTP_METHODS.GET).toUpperCase();
1171
1307
  const url = BuildUrl(this.baseUrl, path, options.query);
1172
- const headers = this.headersFor(options);
1308
+ const headers = await this.headersFor(options);
1173
1309
  const body = options.raw !== undefined
1174
1310
  ? options.raw
1175
1311
  : options.body === undefined ? undefined : JSON.stringify(options.body);
@@ -1201,7 +1337,16 @@ class Transport {
1201
1337
  throw new OpenEmailApiError(ToErrorProps(response, ParseJson(text), text, retryAfter));
1202
1338
  }
1203
1339
  }
1204
- headersFor(options) {
1340
+ async resolveAccessToken() {
1341
+ const source = this.#accessToken;
1342
+ if (typeof source !== 'function')
1343
+ return source;
1344
+ const token = await source();
1345
+ if (!IsAccessToken(token))
1346
+ throw new Error(`${CLIENT_MESSAGES.ACCESS_TOKEN_PROVIDED} ${CLIENT_MESSAGES.ACCESS_TOKEN_SHAPE}`);
1347
+ return token;
1348
+ }
1349
+ async headersFor(options) {
1205
1350
  const headers = {
1206
1351
  ...this.headers,
1207
1352
  [HEADER_KEYS.ACCEPT]: options.accept ?? CONTENT_TYPES.JSON
@@ -1215,7 +1360,10 @@ class Transport {
1215
1360
  if (options.anonymous !== true) {
1216
1361
  if (options.apiKey !== undefined)
1217
1362
  AssertApiKey(options.apiKey, '{ apiKey } on this call');
1218
- const credential = options.inboxToken ?? options.apiKey ?? this.#inboxToken ?? this.#apiKey;
1363
+ const direct = options.inboxToken ?? options.apiKey ?? this.#inboxToken ?? this.#apiKey;
1364
+ if ((direct ?? this.#accessToken) && !this.#secureOrigin)
1365
+ throw new Error(CleartextCredentialMessage(this.baseUrl));
1366
+ const credential = direct ?? await this.resolveAccessToken();
1219
1367
  if (credential)
1220
1368
  headers[HEADER_KEYS.AUTHORIZATION] = `Bearer ${credential}`;
1221
1369
  }
@@ -1338,6 +1486,9 @@ const BroadcastPath = (id) => `${PATHS.BROADCASTS}/${Segment(id)}`;
1338
1486
  const BroadcastChildPath = (id, segment) => `${BroadcastPath(id)}/${segment}`;
1339
1487
  const BroadcastRecipientPath = (id, emailId) => `${BroadcastChildPath(id, SEGMENTS.RECIPIENTS)}/${Segment(emailId)}`;
1340
1488
  const DomainPath = (id) => `${PATHS.DOMAINS}/${Segment(id)}`;
1489
+ const DomainChildPath = (id, segment) => `${DomainPath(id)}/${segment}`;
1490
+ const DomainAddressesPath = (id) => `${DomainPath(id)}/${SEGMENTS.ADDRESSES}`;
1491
+ const DomainAddressPath = (id, addressId) => `${DomainAddressesPath(id)}/${Segment(addressId)}`;
1341
1492
  const WebhookPath = (id) => `${PATHS.WEBHOOKS}/${Segment(id)}`;
1342
1493
  const WebhookChildPath = (id, segment) => `${WebhookPath(id)}/${segment}`;
1343
1494
  const WebhookDeliveryPath = (id, deliveryId) => `${WebhookChildPath(id, SEGMENTS.DELIVERIES)}/${Segment(deliveryId)}`;
@@ -1410,7 +1561,8 @@ const Addresses = (transport) => {
1410
1561
  const MemberQuery = (options) => ({
1411
1562
  [QUERY_KEYS.Q]: options.q,
1412
1563
  [QUERY_KEYS.SOURCE]: options.source,
1413
- [QUERY_KEYS.SORT]: options.sort
1564
+ [QUERY_KEYS.SORT]: options.sort,
1565
+ [QUERY_KEYS.STATUS]: options.statuses?.length ? options.statuses.join(LIST_SEPARATOR) : undefined
1414
1566
  });
1415
1567
  const GrowthQuery = (options) => ({
1416
1568
  [QUERY_KEYS.AUDIENCE_IDS]: options.audienceIds?.join(LIST_SEPARATOR),
@@ -1728,12 +1880,52 @@ const Domains = (transport) => ({
1728
1880
  listAll: (options = {}) => CollectAll(transport, PATHS.DOMAINS, CURSOR_STYLES.CURSOR, options),
1729
1881
  iterate: (options = {}) => IteratePages(transport, PATHS.DOMAINS, CURSOR_STYLES.CURSOR, options),
1730
1882
  get: (id, options = {}) => transport.request(DomainPath(id), { signal: options.signal, apiKey: options.apiKey }),
1883
+ create: (body, options = {}) => transport.request(PATHS.DOMAINS, {
1884
+ method: HTTP_METHODS.POST,
1885
+ body,
1886
+ signal: options.signal,
1887
+ apiKey: options.apiKey
1888
+ }),
1889
+ verify: (id, options = {}) => transport.request(DomainChildPath(id, SEGMENTS.VERIFY), {
1890
+ method: HTTP_METHODS.POST,
1891
+ repeatable: true,
1892
+ signal: options.signal,
1893
+ apiKey: options.apiKey
1894
+ }),
1731
1895
  update: (id, patch, options = {}) => transport.request(DomainPath(id), {
1732
1896
  method: HTTP_METHODS.PATCH,
1733
1897
  body: patch,
1734
1898
  repeatable: true,
1735
1899
  signal: options.signal,
1736
1900
  apiKey: options.apiKey
1901
+ }),
1902
+ delete: (id, options = {}) => transport.request(DomainPath(id), {
1903
+ method: HTTP_METHODS.DELETE,
1904
+ signal: options.signal,
1905
+ apiKey: options.apiKey
1906
+ }),
1907
+ listAddresses: (id, options = {}) => FetchPage(transport, DomainAddressesPath(id), CURSOR_STYLES.CURSOR, options),
1908
+ listAllAddresses: (id, options = {}) => CollectAll(transport, DomainAddressesPath(id), CURSOR_STYLES.CURSOR, options),
1909
+ iterateAddresses: (id, options = {}) => IteratePages(transport, DomainAddressesPath(id), CURSOR_STYLES.CURSOR, options),
1910
+ createAddress: (id, body, options = {}) => transport.request(DomainAddressesPath(id), {
1911
+ method: HTTP_METHODS.POST,
1912
+ body,
1913
+ repeatable: true,
1914
+ signal: options.signal,
1915
+ apiKey: options.apiKey
1916
+ }),
1917
+ getAddress: (id, addressId, options = {}) => transport.request(DomainAddressPath(id, addressId), { signal: options.signal, apiKey: options.apiKey }),
1918
+ updateAddress: (id, addressId, patch, options = {}) => transport.request(DomainAddressPath(id, addressId), {
1919
+ method: HTTP_METHODS.PATCH,
1920
+ body: patch,
1921
+ repeatable: true,
1922
+ signal: options.signal,
1923
+ apiKey: options.apiKey
1924
+ }),
1925
+ deleteAddress: (id, addressId, options = {}) => transport.request(DomainAddressPath(id, addressId), {
1926
+ method: HTTP_METHODS.DELETE,
1927
+ signal: options.signal,
1928
+ apiKey: options.apiKey
1737
1929
  })
1738
1930
  });
1739
1931
 
@@ -2229,6 +2421,22 @@ const Rules = (transport) => ({
2229
2421
  iterateRuns: (options = {}) => IteratePages(transport, PATHS.RULES_RUNS, CURSOR_STYLES.CURSOR, options, RunsQuery(options))
2230
2422
  });
2231
2423
 
2424
+ const Security = (transport) => ({
2425
+ stepUpStatus: (options = {}) => transport.request(PATHS.SECURITY_STEP_UP, { signal: options.signal, apiKey: options.apiKey }),
2426
+ beginStepUp: (body = {}, options = {}) => transport.request(PATHS.SECURITY_STEP_UP, {
2427
+ method: HTTP_METHODS.POST,
2428
+ body,
2429
+ signal: options.signal,
2430
+ apiKey: options.apiKey
2431
+ }),
2432
+ verifyStepUp: (body, options = {}) => transport.request(PATHS.SECURITY_STEP_UP_VERIFY, {
2433
+ method: HTTP_METHODS.POST,
2434
+ body,
2435
+ signal: options.signal,
2436
+ apiKey: options.apiKey
2437
+ })
2438
+ });
2439
+
2232
2440
  const Settings = (transport) => ({
2233
2441
  get: (options = {}) => transport.request(PATHS.SETTINGS, {
2234
2442
  query: { [QUERY_KEYS.ADDRESS]: options.address },
@@ -2617,6 +2825,7 @@ class OpenEmail {
2617
2825
  transport;
2618
2826
  mode;
2619
2827
  me;
2828
+ security;
2620
2829
  keys;
2621
2830
  addresses;
2622
2831
  languages;
@@ -2643,12 +2852,13 @@ class OpenEmail {
2643
2852
  tempMail;
2644
2853
  constructor(configuration) {
2645
2854
  const options = typeof configuration === 'string' ? { apiKey: configuration } : configuration;
2646
- AssertApiKey(options.apiKey, '{ apiKey }');
2855
+ AssertCredential(options.apiKey, options.accessToken, true);
2647
2856
  if (IsBrowser() && options.dangerouslyAllowBrowser !== true)
2648
2857
  throw new Error(BROWSER_REFUSAL);
2649
2858
  this.transport = new Transport(options);
2650
2859
  this.mode = ModeOf(options.apiKey);
2651
2860
  this.me = Me(this.transport);
2861
+ this.security = Security(this.transport);
2652
2862
  this.keys = Keys(this.transport);
2653
2863
  this.addresses = Addresses(this.transport);
2654
2864
  this.languages = Languages(this.transport);
@@ -2690,6 +2900,19 @@ const FromEnvironment = (name) => {
2690
2900
  }
2691
2901
  };
2692
2902
 
2903
+ const IsLocalHost = (hostname) => {
2904
+ const host = hostname.toLowerCase();
2905
+ return LOCAL_HOSTS.includes(host) || LOOPBACK_IPV4_PATTERN.test(host);
2906
+ };
2907
+
2908
+ const HostnameOf = (authority) => {
2909
+ try {
2910
+ return new URL(`http://${authority}`).hostname;
2911
+ }
2912
+ catch {
2913
+ return '';
2914
+ }
2915
+ };
2693
2916
  const NormaliseEndpoint = (value) => {
2694
2917
  if (!value)
2695
2918
  return undefined;
@@ -2698,15 +2921,18 @@ const NormaliseEndpoint = (value) => {
2698
2921
  return undefined;
2699
2922
  if (ENDPOINT_SCHEME_PATTERN.test(trimmed))
2700
2923
  return trimmed;
2701
- const host = trimmed.split(':')[0];
2702
- return `${LOCAL_HOSTS.includes(host) ? 'http' : 'https'}://${trimmed}`;
2924
+ return `${IsLocalHost(HostnameOf(trimmed)) ? 'http' : 'https'}://${trimmed}`;
2703
2925
  };
2704
2926
 
2705
2927
  const CreateClient = (configuration = {}) => {
2706
2928
  const options = typeof configuration === 'string' ? { apiKey: configuration } : configuration;
2929
+ const explicit = options.apiKey !== undefined || options.accessToken !== undefined;
2930
+ const apiKey = explicit ? options.apiKey : FromEnvironment(ENV_VARS.API_KEY);
2931
+ const accessToken = explicit || apiKey !== undefined ? options.accessToken : FromEnvironment(ENV_VARS.ACCESS_TOKEN);
2707
2932
  return new OpenEmail({
2708
2933
  ...options,
2709
- apiKey: options.apiKey ?? FromEnvironment(ENV_VARS.API_KEY),
2934
+ apiKey,
2935
+ accessToken,
2710
2936
  baseUrl: NormaliseEndpoint(options.baseUrl ?? FromEnvironment(ENV_VARS.BASE_URL))
2711
2937
  });
2712
2938
  };
@@ -2714,6 +2940,7 @@ const CreateClient = (configuration = {}) => {
2714
2940
  const CreateTempMail = (options = {}) => TempMail(new Transport({
2715
2941
  ...options,
2716
2942
  apiKey: undefined,
2943
+ accessToken: undefined,
2717
2944
  baseUrl: NormaliseEndpoint(options.baseUrl ?? FromEnvironment(ENV_VARS.BASE_URL))
2718
2945
  }));
2719
2946
 
@@ -2841,9 +3068,10 @@ const createTempMail = CreateTempMail;
2841
3068
  const verifyWebhookSignature = VerifyWebhookSignature;
2842
3069
  const toBase64 = ToBase64;
2843
3070
  const isApiKey = IsApiKey;
3071
+ const isAccessToken = IsAccessToken;
2844
3072
  const isSealed = IsSealed;
2845
3073
  const resolveLanguage = ResolveLanguage;
2846
3074
  const languageByCode = LanguageByCode;
2847
3075
  const isRtlLanguage = IsRtlLanguage;
2848
3076
 
2849
- export { API_SCOPES, BROADCAST_RECIPIENT_FILTERS, BROADCAST_STATUSES, CONTACT_BLOCK_LISTS, CONTACT_PHOTO_TYPES, CONTACT_THREAD_SORTS, ERROR_TYPES, FILE_DIRECTIONS, FILE_KINDS, FILE_SORTS, FILE_USAGES, LANGUAGES, MESSAGE_ENCRYPTION_FORMATS, OpenEmail, OpenEmailApiError, OpenEmailNetworkError, PAGE_LIMITS, PEOPLE_SORTS, PROVIDER_IMPORT_DOMAIN_STATES, PROVIDER_IMPORT_RESOURCES, PROVIDER_IMPORT_STATUSES, RULE_ACTIONS, RULE_FIELDS, RULE_OPERATORS, SUPPRESSION_REASONS, THREAD_SORTS, VERSION, WEBHOOK_EVENTS, WEBHOOK_REPLAY_ERROR_CODES, WEBHOOK_SIGNATURE_HEADERS, createClient, createOpenEmail, createTempMail, OpenEmail as default, getClient, init, isApiKey, isRtlLanguage, isSealed, languageByCode, openemail, resetClient, resolveLanguage, toBase64, verifyWebhookSignature };
3077
+ export { API_SCOPES, AUDIENCE_MEMBER_STATUSES, BROADCAST_RECIPIENT_FILTERS, BROADCAST_STATUSES, CONTACT_BLOCK_LISTS, CONTACT_PHOTO_TYPES, CONTACT_THREAD_SORTS, CREDENTIAL_KINDS, DOMAIN_DMARC_STAGES, DOMAIN_RECORD_STATUSES, ERROR_TYPES, FILE_DIRECTIONS, FILE_KINDS, FILE_SORTS, FILE_USAGES, FILE_VISIBILITIES, LANGUAGES, MESSAGE_ENCRYPTION_FORMATS, OpenEmail, OpenEmailApiError, OpenEmailNetworkError, PAGE_LIMITS, PEOPLE_SORTS, PROVIDER_IMPORT_DOMAIN_STATES, PROVIDER_IMPORT_RESOURCES, PROVIDER_IMPORT_STATUSES, RULE_ACTIONS, RULE_FIELDS, RULE_OPERATORS, STEP_UP_ERROR_CODES, STEP_UP_METHODS, SUPPRESSION_REASONS, THREAD_SORTS, VERSION, WEBHOOK_EVENTS, WEBHOOK_REPLAY_ERROR_CODES, WEBHOOK_SIGNATURE_HEADERS, createClient, createOpenEmail, createTempMail, OpenEmail as default, getClient, init, isAccessToken, isApiKey, isRtlLanguage, isSealed, languageByCode, openemail, resetClient, resolveLanguage, toBase64, verifyWebhookSignature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openemail/sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "author": "OpenEmail",
5
5
  "license": "UNLICENSED",
6
6
  "description": "The official TypeScript SDK for the OpenEmail API. Send email and broadcasts, work with threads, drafts, labels, templates, rules, webhooks, tracking, contacts, audiences, domains, suppressions, files, API keys, roles, members, mailbox imports and disposable inboxes from code.",
@@ -39,7 +39,7 @@
39
39
  "sideEffects": false,
40
40
  "homepage": "https://openemail.uk/docs/sdk",
41
41
  "bugs": {
42
- "url": "https://openemail.uk/support"
42
+ "url": "https://openemail.uk/contact"
43
43
  },
44
44
  "keywords": [
45
45
  "openemail",