@dataships/sdk 0.0.0-stage → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,763 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
8
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
9
+
10
+ // src/auth/headers.ts
11
+ var SERVER_KEY_PREFIX = "ds_sk_";
12
+ var isServerKey = (key) => key.startsWith(SERVER_KEY_PREFIX);
13
+ var isServerKeyAuth = (auth) => "serverKey" in auth;
14
+ var isUnsafeServerKeyAuth = (auth) => "unsafeServerKey" in auth;
15
+ var isTokenProviderAuth = (auth) => typeof auth.getToken === "function";
16
+ var resolveTokenProviderAuth = async (auth) => {
17
+ const token = await auth.getToken();
18
+ if (!token) {
19
+ throw new Error("DatashipsClient: auth.getToken() returned an empty token");
20
+ }
21
+ return { publicKey: auth.publicKey, token };
22
+ };
23
+ var UNSAFE_SERVER_KEY_APP_MARKER = "unsafe-server-key";
24
+ var UNSAFE_SERVER_KEY_WARNING = "Dataships SDK: the merchant SERVER KEY is being used in the browser (auth.unsafeServerKey). Anyone who views this page's source can act as this merchant until the key is revoked. Mint a short-lived token on your server and pass auth: { publicKey, token } instead.";
25
+ var assertBrowserAuth = (auth) => {
26
+ if (isUnsafeServerKeyAuth(auth)) {
27
+ if (!auth.unsafeServerKey || !isServerKey(auth.unsafeServerKey)) {
28
+ throw new Error(
29
+ "DatashipsClient: auth.unsafeServerKey must be a server key (ds_sk_\u2026); a public key belongs in auth: { publicKey, token }"
30
+ );
31
+ }
32
+ warnUnsafeServerKey();
33
+ return;
34
+ }
35
+ if (isServerKey(auth.publicKey)) {
36
+ throw new Error(
37
+ "DatashipsClient: a server key (ds_sk_\u2026) must never be used in the browser; use the public key with a merchant-signed token, or DatashipsServerClient from '@dataships/sdk/server' on your server"
38
+ );
39
+ }
40
+ const hasProvider = "getToken" in auth && auth.getToken !== void 0;
41
+ if (hasProvider && "token" in auth && auth.token !== void 0) {
42
+ throw new Error(
43
+ "DatashipsClient: pass either auth.token or auth.getToken, not both"
44
+ );
45
+ }
46
+ if (hasProvider) {
47
+ if (!isTokenProviderAuth(auth)) {
48
+ throw new Error("DatashipsClient: auth.getToken must be a function");
49
+ }
50
+ return;
51
+ }
52
+ if (!auth.token) {
53
+ throw new Error(
54
+ "DatashipsClient: auth.token is required with a public key (the merchant's server mints it), or auth.getToken to fetch one per request"
55
+ );
56
+ }
57
+ };
58
+ var warnUnsafeServerKey = () => {
59
+ const c = globalThis.console;
60
+ c?.warn?.(UNSAFE_SERVER_KEY_WARNING);
61
+ };
62
+ var authHeaders = (auth) => {
63
+ if (isUnsafeServerKeyAuth(auth)) return { "X-Api-Key": auth.unsafeServerKey };
64
+ return isServerKeyAuth(auth) ? { "X-Api-Key": auth.serverKey } : { "X-Api-Key": auth.publicKey, Authorization: `Bearer ${auth.token}` };
65
+ };
66
+
67
+ // src/baseUrl.ts
68
+ var PRODUCTION_BASE_URL = "https://api.dataships.io";
69
+ var resolveBaseUrl = (env = "production", baseUrl) => {
70
+ if (baseUrl) return baseUrl;
71
+ if (env === "production") return PRODUCTION_BASE_URL;
72
+ throw new Error(
73
+ `Dataships SDK: env "${env}" has no default base URL; pass \`baseUrl\``
74
+ );
75
+ };
76
+
77
+ // src/transport/retry.ts
78
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
79
+ 400,
80
+ 401,
81
+ 403,
82
+ 404,
83
+ 417,
84
+ 422,
85
+ 426
86
+ ]);
87
+ var isTerminalStatus = (status) => status !== void 0 && TERMINAL_STATUSES.has(status);
88
+ var CONSENT_SESSION_ALREADY_PROCESSED = "CONSENT_SESSION_ALREADY_PROCESSED";
89
+ var TERMINAL_ERROR_CODES = /* @__PURE__ */ new Set([
90
+ CONSENT_SESSION_ALREADY_PROCESSED
91
+ ]);
92
+ var isTerminalErrorCode = (errorCode) => errorCode !== void 0 && TERMINAL_ERROR_CODES.has(errorCode);
93
+ var MAX_FAILURES = 4;
94
+ var backoffMs = (failureCount) => failureCount >= MAX_FAILURES ? 0 : failureCount * 2e3;
95
+ var defaultRetryPolicy = {
96
+ shouldRetry: (failureCount, status, errorCode) => !isTerminalStatus(status) && !isTerminalErrorCode(errorCode) && failureCount < MAX_FAILURES,
97
+ delayMs: backoffMs
98
+ };
99
+ var upsertSessionRetryPolicy = defaultRetryPolicy;
100
+ var noRetryPolicy = {
101
+ shouldRetry: () => false,
102
+ delayMs: () => 0
103
+ };
104
+
105
+ // src/transport/errors.ts
106
+ var DatashipsError = class extends Error {
107
+ constructor(args) {
108
+ super(`Dataships request failed (${args.status || "network"})`);
109
+ this.name = "DatashipsError";
110
+ this.status = args.status;
111
+ this.body = args.body;
112
+ this.challenge = args.challenge ?? null;
113
+ this.code = errorCodeFromBody(args.body);
114
+ this.reason = reasonFromBody(args.body);
115
+ }
116
+ /** 401 (missing / refused credential) or 403 (unknown, revoked or disabled key). */
117
+ get isAuthenticationFailure() {
118
+ return this.status === 401 || this.status === 403;
119
+ }
120
+ /** 409 `CONSENT_SESSION_ALREADY_PROCESSED`: the session is closed; no later call on it can succeed. */
121
+ get isSessionProcessed() {
122
+ return this.code === CONSENT_SESSION_ALREADY_PROCESSED;
123
+ }
124
+ };
125
+ var errorCodeFromBody = (body) => {
126
+ if (typeof body !== "object" || body === null) return null;
127
+ const errors = body.errors;
128
+ if (!Array.isArray(errors)) return null;
129
+ const code = errors[0]?.code;
130
+ return typeof code === "string" ? code : null;
131
+ };
132
+ var reasonFromBody = (body) => {
133
+ if (typeof body !== "object" || body === null) return null;
134
+ const message = body.message;
135
+ return typeof message === "string" && message.length > 0 ? message : null;
136
+ };
137
+
138
+ // src/session/consentSession.ts
139
+ var CONSENT_SESSION_START_PATH = "/v1/consent-session";
140
+ var CONSENT_SESSION_CONSENT_PATH = "/v1/consent-session/consent";
141
+ var CONSENT_SESSION_HEARTBEAT_PATH = "/v1/consent-session/heartbeat";
142
+ var CONSENT_SESSION_TRIGGER_VERIFICATION_CODE_PATH = "/v1/consent-session/trigger-verification-code";
143
+ var CONSENT_SESSION_SUBMIT_VERIFICATION_CODE_PATH = "/v1/consent-session/submit-verification-code";
144
+ var CONSENT_SESSION_SUBMIT_PATH = "/v1/consent-session/submit";
145
+ var COUNTRY_NOT_FOUND_CODE = "COUNTRY_NOT_FOUND";
146
+ var SUBMIT_VERIFICATION_CODE_ERROR_CODES = [
147
+ "CODE_EXPIRED",
148
+ "CODE_INVALID",
149
+ "NUMBER_MAX_ATTEMPTS_EXCEEDED"
150
+ ];
151
+ var verificationErrorCodeFromBody = errorCodeFromBody;
152
+ var buildUpsertRequest = (args) => ({
153
+ identifier: args.identifier,
154
+ identifierType: args.identifierType,
155
+ locale: args.locale,
156
+ ...args.userIp ? { userIp: args.userIp } : {},
157
+ ...args.format ? { format: args.format } : {},
158
+ integration: {
159
+ ...args.context,
160
+ type: args.integrationType
161
+ }
162
+ });
163
+ var isCountryNotFound = (error) => error.status === 422 && error.code === COUNTRY_NOT_FOUND_CODE;
164
+ var emptyUpsertResponse = () => ({
165
+ consentSessionId: null,
166
+ consents: [],
167
+ userVerificationRequired: null,
168
+ lastHeartbeat: null,
169
+ identifier: null
170
+ });
171
+ var buildSaveConsentRequest = (args) => ({
172
+ consentSessionId: args.consentSessionId,
173
+ channel: args.channel,
174
+ consent: args.consent
175
+ });
176
+ var buildHeartbeatRequest = (args) => ({
177
+ consentSessionId: args.consentSessionId
178
+ });
179
+ var buildTriggerVerificationCodeRequest = (args) => ({
180
+ consentSessionId: args.consentSessionId
181
+ });
182
+ var buildSubmitVerificationCodeRequest = (args) => ({
183
+ consentSessionId: args.consentSessionId,
184
+ verificationCode: args.verificationCode,
185
+ referenceId: args.referenceId
186
+ });
187
+ var buildSubmitRequest = (args) => {
188
+ const details = { ...args.details };
189
+ if (details.submittedAt instanceof Date) details.submittedAt = details.submittedAt.toISOString();
190
+ if (typeof details.billingCountryCode === "string") {
191
+ details.billingCountryCode = details.billingCountryCode.trim().toUpperCase();
192
+ }
193
+ return {
194
+ datashipsSessionId: args.datashipsSessionId,
195
+ user: normaliseSubmitUser(args.user),
196
+ integration: { ...details, type: args.integrationType }
197
+ };
198
+ };
199
+ var normaliseSubmitUser = (user) => {
200
+ const firstName = user?.firstName?.trim() ?? "";
201
+ const lastName = user?.lastName?.trim() ?? "";
202
+ if (!firstName || !lastName) {
203
+ throw new Error(
204
+ "submitSession: user.firstName and user.lastName are required (core-api refuses a blank name)"
205
+ );
206
+ }
207
+ return { firstName, lastName };
208
+ };
209
+
210
+ // src/session/operations.ts
211
+ var upsertSession = async (args) => {
212
+ const { adapter, input } = args;
213
+ const identifier = input.identifier ?? (await adapter.getIdentifier(input.identifierType)).identifier;
214
+ const context = await adapter.getSessionContext();
215
+ const body = buildUpsertRequest({
216
+ integrationType: adapter.integrationType,
217
+ locale: adapter.getLocale(),
218
+ identifier,
219
+ identifierType: input.identifierType,
220
+ userIp: input.userIp,
221
+ format: input.format,
222
+ context
223
+ });
224
+ try {
225
+ const response = await args.post({
226
+ path: CONSENT_SESSION_START_PATH,
227
+ body,
228
+ retry: args.retry
229
+ });
230
+ return response ?? emptyUpsertResponse();
231
+ } catch (error) {
232
+ if (error instanceof DatashipsError && isCountryNotFound(error)) {
233
+ return emptyUpsertResponse();
234
+ }
235
+ throw error;
236
+ }
237
+ };
238
+ var saveConsent = (args) => args.post({
239
+ path: CONSENT_SESSION_CONSENT_PATH,
240
+ body: buildSaveConsentRequest({
241
+ consentSessionId: args.consentSessionId,
242
+ channel: args.channel,
243
+ consent: args.consent
244
+ }),
245
+ retry: args.retry
246
+ });
247
+ var heartbeat = (args) => args.post({
248
+ path: CONSENT_SESSION_HEARTBEAT_PATH,
249
+ body: buildHeartbeatRequest({ consentSessionId: args.consentSessionId }),
250
+ retry: args.retry
251
+ });
252
+ var triggerVerificationCode = (args) => args.post({
253
+ path: CONSENT_SESSION_TRIGGER_VERIFICATION_CODE_PATH,
254
+ body: buildTriggerVerificationCodeRequest({
255
+ consentSessionId: args.consentSessionId
256
+ }),
257
+ retry: noRetryPolicy
258
+ });
259
+ var submitVerificationCode = async (args) => {
260
+ try {
261
+ await args.post({
262
+ path: CONSENT_SESSION_SUBMIT_VERIFICATION_CODE_PATH,
263
+ body: buildSubmitVerificationCodeRequest({
264
+ consentSessionId: args.consentSessionId,
265
+ verificationCode: args.verificationCode,
266
+ referenceId: args.referenceId
267
+ }),
268
+ retry: noRetryPolicy
269
+ });
270
+ return { verified: true, errorCode: null };
271
+ } catch (error) {
272
+ if (error instanceof DatashipsError && error.status === 400) {
273
+ return {
274
+ verified: false,
275
+ errorCode: verificationErrorCodeFromBody(error.body)
276
+ };
277
+ }
278
+ throw error;
279
+ }
280
+ };
281
+ var submitSession = async (args) => {
282
+ const { adapter, input } = args;
283
+ const datashipsSessionId = input.datashipsSessionId ?? adapter.getDatashipsSessionId?.();
284
+ if (!datashipsSessionId) {
285
+ throw new Error(
286
+ "submitSession: datashipsSessionId is required; this adapter holds no beacon of its own"
287
+ );
288
+ }
289
+ await args.post({
290
+ path: CONSENT_SESSION_SUBMIT_PATH,
291
+ body: buildSubmitRequest({
292
+ datashipsSessionId,
293
+ integrationType: adapter.integrationType,
294
+ user: input.user,
295
+ details: input.details
296
+ }),
297
+ retry: args.retry
298
+ });
299
+ if (input.datashipsSessionId === void 0 && input.resetBeacon !== false) {
300
+ adapter.resetDatashipsSessionId?.();
301
+ }
302
+ };
303
+
304
+ // src/transport/fetch.ts
305
+ var postJson = async ({
306
+ url,
307
+ headers,
308
+ body,
309
+ fetch,
310
+ retry
311
+ }) => {
312
+ let attempt = 0;
313
+ while (true) {
314
+ let response;
315
+ let networkError;
316
+ const attemptHeaders = typeof headers === "function" ? await headers() : headers;
317
+ try {
318
+ response = await fetch(url, {
319
+ method: "POST",
320
+ headers: { "Content-Type": "application/json", ...attemptHeaders },
321
+ body: JSON.stringify(body)
322
+ });
323
+ } catch (err) {
324
+ networkError = err;
325
+ }
326
+ const status = response?.status;
327
+ const ok = response?.ok ?? false;
328
+ if (ok && response) {
329
+ const text = await response.text();
330
+ return text ? JSON.parse(text) : void 0;
331
+ }
332
+ const errorBody = response ? await readBody(response) : null;
333
+ const errorCode = errorCodeFromBody(errorBody) ?? void 0;
334
+ if (!retry.shouldRetry(attempt, status, errorCode)) {
335
+ if (networkError) throw networkError;
336
+ throw new DatashipsError({
337
+ status: status ?? 0,
338
+ body: errorBody,
339
+ challenge: response?.headers.get("WWW-Authenticate") ?? null
340
+ });
341
+ }
342
+ attempt += 1;
343
+ const delay = retry.delayMs(attempt);
344
+ if (delay > 0) await new Promise((r) => setTimeout(r, delay));
345
+ }
346
+ };
347
+ var readBody = async (response) => {
348
+ const text = await response.text();
349
+ if (!text) return null;
350
+ try {
351
+ return JSON.parse(text);
352
+ } catch {
353
+ return text;
354
+ }
355
+ };
356
+
357
+ // src/client.ts
358
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 210 * 1e3;
359
+ var DEFAULT_HEARTBEAT_DURATION_MS = 10 * 60 * 1e3;
360
+ var _auth, _fetch, _upsertSessionRetry, _defaultRetry, _DatashipsClient_instances, previewUpsertResponse_fn, credentialHeaders_fn, requireAuth_fn, requireConsentSessionId_fn, sendHeartbeat_fn, post_fn;
361
+ var DatashipsClient = class {
362
+ constructor(opts) {
363
+ __privateAdd(this, _DatashipsClient_instances);
364
+ __privateAdd(this, _auth);
365
+ __privateAdd(this, _fetch);
366
+ __privateAdd(this, _upsertSessionRetry);
367
+ __privateAdd(this, _defaultRetry);
368
+ this.adapter = opts.adapter;
369
+ if (opts.auth) assertBrowserAuth(opts.auth);
370
+ if (!opts.auth && !opts.adapter.previewMode) {
371
+ throw new Error(
372
+ "DatashipsClient: `auth: { publicKey, token }` (or `{ publicKey, getToken }`) is required unless the adapter runs in preview mode"
373
+ );
374
+ }
375
+ this.publicKey = opts.auth && !isUnsafeServerKeyAuth(opts.auth) ? opts.auth.publicKey : null;
376
+ __privateSet(this, _auth, opts.auth ?? null);
377
+ this.app = opts.app;
378
+ this.version = opts.version;
379
+ this.baseUrl = resolveBaseUrl(opts.env, opts.baseUrl);
380
+ __privateSet(this, _fetch, opts.fetch ?? globalThis.fetch.bind(globalThis));
381
+ __privateSet(this, _upsertSessionRetry, opts.retry?.upsertSession ?? upsertSessionRetryPolicy);
382
+ __privateSet(this, _defaultRetry, opts.retry?.default ?? defaultRetryPolicy);
383
+ }
384
+ /** The credentials in use, or null for a preview-mode client constructed without them. */
385
+ get auth() {
386
+ return __privateGet(this, _auth);
387
+ }
388
+ /** Whether this client runs on the unsafe server-key escape hatch. */
389
+ get isUnsafeServerKey() {
390
+ return __privateGet(this, _auth) !== null && isUnsafeServerKeyAuth(__privateGet(this, _auth));
391
+ }
392
+ /**
393
+ * Replace the Bearer token before it expires. The next call and any running
394
+ * heartbeat use it; a missed rotation surfaces as a 401 on the next call.
395
+ * Not applicable under `auth.getToken`, which already fetches one per request.
396
+ */
397
+ setToken(token) {
398
+ if (!__privateGet(this, _auth)) {
399
+ throw new Error(
400
+ "DatashipsClient: setToken() requires a client constructed with `auth`"
401
+ );
402
+ }
403
+ if (isUnsafeServerKeyAuth(__privateGet(this, _auth))) {
404
+ throw new Error(
405
+ "DatashipsClient: setToken() does not apply under auth.unsafeServerKey; there is no token to rotate"
406
+ );
407
+ }
408
+ if (isTokenProviderAuth(__privateGet(this, _auth))) {
409
+ throw new Error(
410
+ "DatashipsClient: setToken() does not apply under auth.getToken; the provider is called before every request"
411
+ );
412
+ }
413
+ if (!token) {
414
+ throw new Error("DatashipsClient: token must be a non-empty string");
415
+ }
416
+ __privateSet(this, _auth, { ...__privateGet(this, _auth), token });
417
+ }
418
+ /**
419
+ * Open or update the consent session for one identifier type
420
+ * (`POST /v1/consent-session`, contract v2). The client only states which
421
+ * identifier type it holds; the server decides the channels and returns one
422
+ * entry per channel to render. An empty `consents` means render nothing:
423
+ * that also covers a 204 No Content (a surface that never collects, no
424
+ * eligible channel, or a checkout that already asked on an earlier step) and
425
+ * a user IP the API could not geolocate (422 COUNTRY_NOT_FOUND), both of
426
+ * which resolve instead of throwing.
427
+ */
428
+ async upsertSession(input) {
429
+ const preview = __privateMethod(this, _DatashipsClient_instances, previewUpsertResponse_fn).call(this, input.identifierType, input.format);
430
+ if (preview) return preview;
431
+ return upsertSession({
432
+ adapter: this.adapter,
433
+ input,
434
+ post: __privateMethod(this, _DatashipsClient_instances, post_fn).bind(this),
435
+ retry: __privateGet(this, _upsertSessionRetry)
436
+ });
437
+ }
438
+ async saveConsent(input) {
439
+ if (this.adapter.previewMode) return;
440
+ await saveConsent({
441
+ consentSessionId: __privateMethod(this, _DatashipsClient_instances, requireConsentSessionId_fn).call(this, input.session),
442
+ channel: input.channel,
443
+ consent: input.consent,
444
+ post: __privateMethod(this, _DatashipsClient_instances, post_fn).bind(this),
445
+ retry: __privateGet(this, _defaultRetry)
446
+ });
447
+ }
448
+ /**
449
+ * Ask the backend to send an OTP verification code to the phone-channel
450
+ * session's number. `referenceId` must be echoed back on
451
+ * {@link submitVerificationCode}. Never retried — a retry could send the
452
+ * buyer duplicate codes.
453
+ *
454
+ * In preview mode no code is sent and `{ userVerificationTriggered: false }` is
455
+ * returned, mirroring a store without verification enabled.
456
+ */
457
+ async triggerVerificationCode(input) {
458
+ if (this.adapter.previewMode) {
459
+ return {
460
+ userVerificationTriggered: false,
461
+ referenceId: null,
462
+ codeLength: null,
463
+ resendAfterSeconds: null
464
+ };
465
+ }
466
+ return triggerVerificationCode({
467
+ consentSessionId: input.consentSessionId,
468
+ post: __privateMethod(this, _DatashipsClient_instances, post_fn).bind(this)
469
+ });
470
+ }
471
+ /**
472
+ * Submit the OTP code the buyer received. Resolves
473
+ * `{ verified: true, errorCode: null }` on 2xx. A 400 rejection resolves
474
+ * `{ verified: false, errorCode }` with the body's `errors[0].code` (see
475
+ * `SUBMIT_VERIFICATION_CODE_ERROR_CODES`), or `null` when the body carries
476
+ * none. Any other failure throws. Never retried — a rejection is definitive.
477
+ *
478
+ * In preview mode resolves verified without a network call.
479
+ */
480
+ async submitVerificationCode(input) {
481
+ if (this.adapter.previewMode) return { verified: true, errorCode: null };
482
+ return submitVerificationCode({
483
+ ...input,
484
+ post: __privateMethod(this, _DatashipsClient_instances, post_fn).bind(this)
485
+ });
486
+ }
487
+ /**
488
+ * Complete the sessions opened under a beacon: the placed order (or the
489
+ * flow's equivalent) lands on every sibling session and processing is
490
+ * queued (`POST /v1/consent-session/submit`).
491
+ *
492
+ * `user.firstName` / `lastName` are required (the backend refuses a blank
493
+ * name; the SDK refuses it before sending). The beacon defaults to the
494
+ * adapter's own `datashipsSessionId`, which is then discarded once the
495
+ * submit is accepted. Idempotent on the backend; a
496
+ * beacon that matches no session is accepted silently. Retried on 409 and
497
+ * server errors like any other call.
498
+ *
499
+ * In preview mode nothing is sent; the beacon is still discarded so a
500
+ * styled sandbox behaves like a real checkout.
501
+ */
502
+ async submitSession(input) {
503
+ const adapter = this.adapter;
504
+ if (adapter.previewMode) {
505
+ if (input.datashipsSessionId === void 0 && input.resetBeacon !== false) {
506
+ adapter.resetDatashipsSessionId?.();
507
+ }
508
+ return;
509
+ }
510
+ await submitSession({
511
+ adapter,
512
+ input,
513
+ post: __privateMethod(this, _DatashipsClient_instances, post_fn).bind(this),
514
+ retry: __privateGet(this, _defaultRetry)
515
+ });
516
+ }
517
+ /**
518
+ * Begin a periodic heartbeat. Returns an idempotent stop function. The
519
+ * scheduler stops itself at the duration limit, when the credential is
520
+ * refused (401 / 403) and when the session has already been processed; every
521
+ * failure is reported through `onError`.
522
+ */
523
+ startHeartbeat(session, opts = {}) {
524
+ if (this.adapter.previewMode) return () => {
525
+ };
526
+ const intervalMs = opts.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
527
+ const durationMs = opts.durationMs ?? DEFAULT_HEARTBEAT_DURATION_MS;
528
+ const startedAt = Date.now();
529
+ let stopped = false;
530
+ const stop = () => {
531
+ if (stopped) return;
532
+ stopped = true;
533
+ clearInterval(handle);
534
+ };
535
+ const handle = setInterval(() => {
536
+ if (Date.now() - startedAt >= durationMs) {
537
+ stop();
538
+ return;
539
+ }
540
+ __privateMethod(this, _DatashipsClient_instances, sendHeartbeat_fn).call(this, session).catch((error) => {
541
+ opts.onError?.(error);
542
+ if (error instanceof DatashipsError && (error.isAuthenticationFailure || error.isSessionProcessed)) {
543
+ stop();
544
+ }
545
+ });
546
+ }, intervalMs);
547
+ return stop;
548
+ }
549
+ };
550
+ _auth = new WeakMap();
551
+ _fetch = new WeakMap();
552
+ _upsertSessionRetry = new WeakMap();
553
+ _defaultRetry = new WeakMap();
554
+ _DatashipsClient_instances = new WeakSet();
555
+ /**
556
+ * In preview mode, return the adapter's synthetic upsert response so the
557
+ * widget can be styled without live calls. Returns null when the adapter is
558
+ * live (or hasn't opted in), in which case the caller proceeds with the real
559
+ * network request.
560
+ */
561
+ previewUpsertResponse_fn = function(identifierType, format) {
562
+ if (this.adapter.previewMode && this.adapter.getPreviewUpsertResponse) {
563
+ return this.adapter.getPreviewUpsertResponse(identifierType, format);
564
+ }
565
+ return null;
566
+ };
567
+ credentialHeaders_fn = async function() {
568
+ const auth = __privateMethod(this, _DatashipsClient_instances, requireAuth_fn).call(this);
569
+ return authHeaders(
570
+ isTokenProviderAuth(auth) ? await resolveTokenProviderAuth(auth) : auth
571
+ );
572
+ };
573
+ requireAuth_fn = function() {
574
+ if (!__privateGet(this, _auth)) {
575
+ throw new Error(
576
+ "DatashipsClient: no credentials configured; pass `auth: { publicKey, token }`"
577
+ );
578
+ }
579
+ return __privateGet(this, _auth);
580
+ };
581
+ requireConsentSessionId_fn = function(session) {
582
+ if (!session.consentSessionId) {
583
+ throw new Error(
584
+ "DatashipsClient: consentSessionId is required"
585
+ );
586
+ }
587
+ return session.consentSessionId;
588
+ };
589
+ sendHeartbeat_fn = async function(session) {
590
+ await heartbeat({
591
+ consentSessionId: __privateMethod(this, _DatashipsClient_instances, requireConsentSessionId_fn).call(this, session),
592
+ post: __privateMethod(this, _DatashipsClient_instances, post_fn).bind(this),
593
+ retry: __privateGet(this, _defaultRetry)
594
+ });
595
+ };
596
+ post_fn = async function(args) {
597
+ const staticHeaders = {
598
+ // The marker lets the backend tell an exposed key in a page from a genuine server call.
599
+ "X-Dataships-App": `${this.app} (SDK: ${this.adapter.name}${this.isUnsafeServerKey ? `; ${UNSAFE_SERVER_KEY_APP_MARKER}` : ""})`,
600
+ "X-Dataships-Version": `${this.version} (SDK: ${"2.0.1"})`
601
+ };
602
+ return postJson({
603
+ url: `${this.baseUrl}${args.path}`,
604
+ // Resolved per attempt so a token provider hands every retry a fresh token.
605
+ headers: async () => ({
606
+ ...await __privateMethod(this, _DatashipsClient_instances, credentialHeaders_fn).call(this),
607
+ ...staticHeaders
608
+ }),
609
+ body: args.body,
610
+ fetch: __privateGet(this, _fetch),
611
+ retry: args.retry
612
+ });
613
+ };
614
+
615
+ // src/utils/sha256.ts
616
+ var sha256Hex = async (input) => {
617
+ const buf = new TextEncoder().encode(input);
618
+ const digest = await crypto.subtle.digest("SHA-256", buf);
619
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
620
+ };
621
+
622
+ // src/utils/uuid.ts
623
+ var randomUuid = (cryptoObj = globalThis.crypto) => {
624
+ if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
625
+ return cryptoObj.randomUUID();
626
+ }
627
+ const bytes = new Uint8Array(16);
628
+ if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
629
+ cryptoObj.getRandomValues(bytes);
630
+ } else {
631
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
632
+ }
633
+ bytes[6] = bytes[6] & 15 | 64;
634
+ bytes[8] = bytes[8] & 63 | 128;
635
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
636
+ return hex.slice(0, 4).join("") + "-" + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10, 16).join("");
637
+ };
638
+
639
+ // src/session/previewUpsertResponse.ts
640
+ var DEFAULT_EMAIL_OPT_IN_TEXT = "We would like to email you news and promotions. Tick to opt in.";
641
+ var DEFAULT_EMAIL_OPT_IN_CONSENT_DESCRIPTION = "Click here for an overview of how your personal data is processed for marketing purposes or see our Privacy Policy for further information.";
642
+ var DEFAULT_SMS_OPT_IN_TEXT = "We would like to text you news and promotions. Tick to opt in.";
643
+ var DEFAULT_SMS_OPT_IN_CONSENT_DESCRIPTION = "Based on the channel preferences set above, you agree to receive recurring automated marketing messages (e.g. AI content, cart reminders) from {{COMPANY_NAME}} at the number you provide. Consent is not a condition of purchase. We may share info with service providers per our Privacy Policy. Reply STOP to cancel or click the unsubscribe link (where available). For help reply HELP or email {{PRIVACY_EMAIL}}. Msg frequency varies. Msg & data rates may apply. By proceeding, you also agree to our Terms of Service (incl. arbitration) & Privacy Policy.";
644
+ var fakeEmailUpsertResponse = {
645
+ consentSessionId: "01HQB3QRZVV0737R0R25BTQBGF",
646
+ consents: [
647
+ {
648
+ channel: "EMAIL",
649
+ consentType: "OPT_IN",
650
+ checked: false,
651
+ widgetConfig: {
652
+ text: DEFAULT_EMAIL_OPT_IN_TEXT,
653
+ consentDescription: DEFAULT_EMAIL_OPT_IN_CONSENT_DESCRIPTION,
654
+ marketingInformation: '<h1>Marketing Information</h1>\n<p>We may send you direct marketing communications, product information and promotional offers. You can withdraw your consent at any time. See our <a href="https://dataships.io/privacy-centre">Privacy Policy</a> or contact us at <a href="mailto:privacy@example.com">privacy@example.com</a> for further information.</p>\n',
655
+ policyURL: "https://dataships.io/privacy-centre",
656
+ mobileTermsOfServiceURL: null,
657
+ privacyEmail: "privacy@example.com",
658
+ placeholderValues: null
659
+ }
660
+ }
661
+ ],
662
+ userVerificationRequired: null,
663
+ lastHeartbeat: "2026-06-17T00:00:00Z",
664
+ identifier: null
665
+ };
666
+ var fakePhoneUpsertResponse = {
667
+ consentSessionId: "01HQB3QRZVV0737R0R25BTQBGG",
668
+ consents: [
669
+ {
670
+ channel: "SMS",
671
+ consentType: "OPT_IN",
672
+ checked: false,
673
+ widgetConfig: {
674
+ text: DEFAULT_SMS_OPT_IN_TEXT,
675
+ consentDescription: DEFAULT_SMS_OPT_IN_CONSENT_DESCRIPTION,
676
+ marketingInformation: '<h1>Marketing Information</h1>\n<p>We may send you marketing text messages about products and promotional offers. Message frequency varies. You can withdraw your consent at any time by replying STOP. See our <a href="https://dataships.io/privacy-centre">Privacy Policy</a> or contact us at <a href="mailto:privacy@example.com">privacy@example.com</a> for further information.</p>\n',
677
+ policyURL: "https://dataships.io/privacy-centre",
678
+ mobileTermsOfServiceURL: "https://dataships.io/mobile-terms",
679
+ privacyEmail: "privacy@example.com",
680
+ placeholderValues: null
681
+ }
682
+ }
683
+ ],
684
+ userVerificationRequired: false,
685
+ lastHeartbeat: "2026-06-17T00:00:00Z",
686
+ identifier: null
687
+ };
688
+ var MARKETING_MODAL_HREF = "#dataships-marketing-modal";
689
+ var CHECKBOX_LABEL_BOLD_PHRASES = ["consent", "object", "opt in", "opt out", "STOP", "HELP"];
690
+ var CONSENT_DESCRIPTION_BOLD_PHRASES = ["STOP", "HELP"];
691
+ var escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
692
+ var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
693
+ var wrapPhrase = (html, phrase, wrap) => html.replace(
694
+ new RegExp(`${escapeRegExp(escapeHtml(phrase))}(?![^<]*>)(?![^<]*</a>)`, "gi"),
695
+ wrap
696
+ );
697
+ var toHtmlCopy = (text, widgetConfig, boldPhrases) => {
698
+ let html = escapeHtml(text);
699
+ const links = [
700
+ ["Terms of Service", widgetConfig.mobileTermsOfServiceURL],
701
+ ["Privacy Policy", widgetConfig.policyURL],
702
+ ["Click here", MARKETING_MODAL_HREF]
703
+ ];
704
+ for (const [phrase, href] of links) {
705
+ if (!href) continue;
706
+ html = wrapPhrase(html, phrase, (match) => `<a href="${escapeHtml(href)}">${match}</a>`);
707
+ }
708
+ for (const phrase of boldPhrases) {
709
+ html = wrapPhrase(html, phrase, (match) => `<strong>${match}</strong>`);
710
+ }
711
+ return `<p>${html}</p>`;
712
+ };
713
+ var withHtmlCopy = (response) => ({
714
+ ...response,
715
+ consents: response.consents.map((consent) => {
716
+ const { widgetConfig } = consent;
717
+ return {
718
+ ...consent,
719
+ widgetConfig: {
720
+ ...widgetConfig,
721
+ text: widgetConfig.text === null ? null : toHtmlCopy(widgetConfig.text, widgetConfig, CHECKBOX_LABEL_BOLD_PHRASES),
722
+ consentDescription: widgetConfig.consentDescription === null ? null : toHtmlCopy(
723
+ widgetConfig.consentDescription,
724
+ widgetConfig,
725
+ CONSENT_DESCRIPTION_BOLD_PHRASES
726
+ )
727
+ }
728
+ };
729
+ })
730
+ });
731
+ var previewUpsertResponse = (identifierType, format = "json") => {
732
+ const response = identifierType === "PHONE" ? fakePhoneUpsertResponse : fakeEmailUpsertResponse;
733
+ return format === "html" ? withHtmlCopy(response) : response;
734
+ };
735
+
736
+ // src/types.ts
737
+ var SUPPORTED_LOCALES = [
738
+ "en",
739
+ "es",
740
+ "de",
741
+ "pt",
742
+ "ro",
743
+ "hr",
744
+ "da",
745
+ "fi",
746
+ "fr",
747
+ "hu",
748
+ "it",
749
+ "nb",
750
+ "pl",
751
+ "sk",
752
+ "sv",
753
+ "nl",
754
+ "el",
755
+ "cs",
756
+ "zh",
757
+ "ja",
758
+ "is"
759
+ ];
760
+
761
+ export { CONSENT_SESSION_ALREADY_PROCESSED, CONSENT_SESSION_SUBMIT_PATH, COUNTRY_NOT_FOUND_CODE, DatashipsClient, DatashipsError, DatashipsError as DatashipsHttpError, SUBMIT_VERIFICATION_CODE_ERROR_CODES, SUPPORTED_LOCALES, UNSAFE_SERVER_KEY_APP_MARKER, UNSAFE_SERVER_KEY_WARNING, authHeaders, buildSubmitRequest, defaultRetryPolicy, fakeEmailUpsertResponse, fakePhoneUpsertResponse, noRetryPolicy, normaliseSubmitUser, previewUpsertResponse, randomUuid, sha256Hex, upsertSessionRetryPolicy, verificationErrorCodeFromBody };
762
+ //# sourceMappingURL=index.js.map
763
+ //# sourceMappingURL=index.js.map