@cross-deck/node 1.2.0 → 1.3.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/CHANGELOG.md +102 -0
- package/dist/auto-events/index.d.mts +1 -1
- package/dist/auto-events/index.d.ts +1 -1
- package/dist/{crossdeck-server-BZVZEuS-.d.mts → crossdeck-server-DhnHvUhh.d.mts} +37 -21
- package/dist/{crossdeck-server-BZVZEuS-.d.ts → crossdeck-server-DhnHvUhh.d.ts} +37 -21
- package/dist/index.cjs +145 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +34 -7
- package/dist/index.d.ts +34 -7
- package/dist/index.mjs +145 -54
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as CrossdeckAuthenticationError, h as CrossdeckConfigurationError, i as CrossdeckError, j as CrossdeckErrorPayload, k as CrossdeckErrorType, l as CrossdeckInternalError, m as CrossdeckNetworkError, n as CrossdeckPermissionError, o as CrossdeckRateLimitError, p as CrossdeckServer, q as CrossdeckServerOptions, r as CrossdeckValidationError, D as DEFAULT_BASE_URL, s as DEFAULT_TIMEOUT_MS, t as Diagnostics, E as EntitlementCacheOptions, u as EntitlementMutationResult, v as EntitlementStore, w as EntitlementsListResponse, x as EntitlementsListener, y as Environment, z as ErrorCaptureConfig, F as ErrorLevel, G as EventProperties, H as ForgetResult, I as GrantDuration, J as GrantEntitlementInput, K as GroupMembership, L as HeartbeatResponse, M as HttpRequestInfo, N as HttpResponseInfo, O as HttpRetriesConfig, P as IdentifyOptions, Q as IdentityHints, R as IngestOptions, S as IngestResponse, T as PublicEntitlement, U as PurchaseResult, V as RequestOptions, W as RevokeEntitlementInput, X as RuntimeHost, Y as RuntimeInfo, Z as
|
|
1
|
+
export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as CrossdeckAuthenticationError, h as CrossdeckConfigurationError, i as CrossdeckError, j as CrossdeckErrorPayload, k as CrossdeckErrorType, l as CrossdeckInternalError, m as CrossdeckNetworkError, n as CrossdeckPermissionError, o as CrossdeckRateLimitError, p as CrossdeckServer, q as CrossdeckServerOptions, r as CrossdeckValidationError, D as DEFAULT_BASE_URL, s as DEFAULT_TIMEOUT_MS, t as Diagnostics, E as EntitlementCacheOptions, u as EntitlementMutationResult, v as EntitlementStore, w as EntitlementsListResponse, x as EntitlementsListener, y as Environment, z as ErrorCaptureConfig, F as ErrorLevel, G as EventProperties, H as ForgetResult, I as GrantDuration, J as GrantEntitlementInput, K as GroupMembership, L as HeartbeatResponse, M as HttpRequestInfo, N as HttpResponseInfo, O as HttpRetriesConfig, P as IdentifyOptions, Q as IdentityHints, R as IngestOptions, S as IngestResponse, T as PublicEntitlement, U as PurchaseResult, V as RequestOptions, W as RevokeEntitlementInput, X as RuntimeHost, Y as RuntimeInfo, Z as ServerEvent, _ as StackFrame, $ as StoredEntitlements, a0 as SyncPurchaseInput, a1 as makeCrossdeckError } from './crossdeck-server-DhnHvUhh.mjs';
|
|
2
2
|
import 'node:events';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth: the `version` field in this package's
|
|
8
|
+
* package.json. The sync script writes this file so that
|
|
9
|
+
* `SDK_VERSION` is a plain TypeScript literal at runtime — no
|
|
10
|
+
* runtime JSON-import gotcha (Node ESM requires
|
|
11
|
+
* `with { type: "json" }` to import JSON as ESM, and the published
|
|
12
|
+
* dist file would otherwise fail to load).
|
|
13
|
+
*
|
|
14
|
+
* Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
|
|
15
|
+
* CI gate) flags this file when it falls out of sync with package.json.
|
|
16
|
+
* Bumping `package.json` without re-running the sync script fails CI.
|
|
17
|
+
*
|
|
18
|
+
* Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
|
|
19
|
+
*/
|
|
20
|
+
declare const SDK_VERSION = "1.3.1";
|
|
21
|
+
declare const SDK_NAME = "@cross-deck/node";
|
|
22
|
+
|
|
4
23
|
/**
|
|
5
24
|
* Machine-readable index of every error code `@cross-deck/node` can
|
|
6
25
|
* throw, with a short description and a hint on what action to take.
|
|
@@ -289,16 +308,24 @@ declare function signWebhookPayload(payload: string, secret: string, timestampSe
|
|
|
289
308
|
* name: "checkout.started",
|
|
290
309
|
* developerUserId: userId,
|
|
291
310
|
* properties: scrubPiiFromProperties({
|
|
292
|
-
* url: req.url, // might contain "/users/wes@…/" — gets
|
|
311
|
+
* url: req.url, // might contain "/users/wes@…/" — gets <email>
|
|
293
312
|
* lastError: e.message, // might contain card numbers
|
|
294
313
|
* }),
|
|
295
314
|
* });
|
|
296
315
|
*/
|
|
297
316
|
/**
|
|
298
317
|
* Scrub a single string value: replace email-shaped substrings with
|
|
299
|
-
*
|
|
300
|
-
* the original string (===) when nothing matched
|
|
301
|
-
*
|
|
318
|
+
* `<email>` and card-number-shaped substrings with `<card>`. Returns
|
|
319
|
+
* the original string (===) when nothing matched.
|
|
320
|
+
*
|
|
321
|
+
* Implementation note: we call `.replace()` unconditionally rather than
|
|
322
|
+
* gating on `.test()`. The /g regexes are module-level so `.test()`
|
|
323
|
+
* carries `lastIndex` state between calls — a prior match leaves
|
|
324
|
+
* `lastIndex` mid-string and the next `.test()` can falsely return
|
|
325
|
+
* false on a string that DOES match. `.replace(/g)` always scans the
|
|
326
|
+
* full string regardless of `lastIndex`, so dropping the test-guard
|
|
327
|
+
* removes the sharp edge at zero cost (when nothing matches, replace
|
|
328
|
+
* returns the same `(===)` string).
|
|
302
329
|
*/
|
|
303
330
|
declare function scrubPii(value: string): string;
|
|
304
331
|
/**
|
|
@@ -339,7 +366,7 @@ declare function scrubPiiFromProperties(properties: Record<string, unknown>): Re
|
|
|
339
366
|
* - `sdk.no_durable_store`
|
|
340
367
|
* - `sdk.super_property_registered`
|
|
341
368
|
*/
|
|
342
|
-
type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
|
|
369
|
+
type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_permanent_failure" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
|
|
343
370
|
interface DebugContext {
|
|
344
371
|
[key: string]: unknown;
|
|
345
372
|
}
|
|
@@ -348,4 +375,4 @@ interface DebugLogger {
|
|
|
348
375
|
emit(signal: DebugSignal, message: string, context?: DebugContext): void;
|
|
349
376
|
}
|
|
350
377
|
|
|
351
|
-
export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
|
|
378
|
+
export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, SDK_NAME, SDK_VERSION, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as CrossdeckAuthenticationError, h as CrossdeckConfigurationError, i as CrossdeckError, j as CrossdeckErrorPayload, k as CrossdeckErrorType, l as CrossdeckInternalError, m as CrossdeckNetworkError, n as CrossdeckPermissionError, o as CrossdeckRateLimitError, p as CrossdeckServer, q as CrossdeckServerOptions, r as CrossdeckValidationError, D as DEFAULT_BASE_URL, s as DEFAULT_TIMEOUT_MS, t as Diagnostics, E as EntitlementCacheOptions, u as EntitlementMutationResult, v as EntitlementStore, w as EntitlementsListResponse, x as EntitlementsListener, y as Environment, z as ErrorCaptureConfig, F as ErrorLevel, G as EventProperties, H as ForgetResult, I as GrantDuration, J as GrantEntitlementInput, K as GroupMembership, L as HeartbeatResponse, M as HttpRequestInfo, N as HttpResponseInfo, O as HttpRetriesConfig, P as IdentifyOptions, Q as IdentityHints, R as IngestOptions, S as IngestResponse, T as PublicEntitlement, U as PurchaseResult, V as RequestOptions, W as RevokeEntitlementInput, X as RuntimeHost, Y as RuntimeInfo, Z as
|
|
1
|
+
export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as CrossdeckAuthenticationError, h as CrossdeckConfigurationError, i as CrossdeckError, j as CrossdeckErrorPayload, k as CrossdeckErrorType, l as CrossdeckInternalError, m as CrossdeckNetworkError, n as CrossdeckPermissionError, o as CrossdeckRateLimitError, p as CrossdeckServer, q as CrossdeckServerOptions, r as CrossdeckValidationError, D as DEFAULT_BASE_URL, s as DEFAULT_TIMEOUT_MS, t as Diagnostics, E as EntitlementCacheOptions, u as EntitlementMutationResult, v as EntitlementStore, w as EntitlementsListResponse, x as EntitlementsListener, y as Environment, z as ErrorCaptureConfig, F as ErrorLevel, G as EventProperties, H as ForgetResult, I as GrantDuration, J as GrantEntitlementInput, K as GroupMembership, L as HeartbeatResponse, M as HttpRequestInfo, N as HttpResponseInfo, O as HttpRetriesConfig, P as IdentifyOptions, Q as IdentityHints, R as IngestOptions, S as IngestResponse, T as PublicEntitlement, U as PurchaseResult, V as RequestOptions, W as RevokeEntitlementInput, X as RuntimeHost, Y as RuntimeInfo, Z as ServerEvent, _ as StackFrame, $ as StoredEntitlements, a0 as SyncPurchaseInput, a1 as makeCrossdeckError } from './crossdeck-server-DhnHvUhh.js';
|
|
2
2
|
import 'node:events';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth: the `version` field in this package's
|
|
8
|
+
* package.json. The sync script writes this file so that
|
|
9
|
+
* `SDK_VERSION` is a plain TypeScript literal at runtime — no
|
|
10
|
+
* runtime JSON-import gotcha (Node ESM requires
|
|
11
|
+
* `with { type: "json" }` to import JSON as ESM, and the published
|
|
12
|
+
* dist file would otherwise fail to load).
|
|
13
|
+
*
|
|
14
|
+
* Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
|
|
15
|
+
* CI gate) flags this file when it falls out of sync with package.json.
|
|
16
|
+
* Bumping `package.json` without re-running the sync script fails CI.
|
|
17
|
+
*
|
|
18
|
+
* Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
|
|
19
|
+
*/
|
|
20
|
+
declare const SDK_VERSION = "1.3.1";
|
|
21
|
+
declare const SDK_NAME = "@cross-deck/node";
|
|
22
|
+
|
|
4
23
|
/**
|
|
5
24
|
* Machine-readable index of every error code `@cross-deck/node` can
|
|
6
25
|
* throw, with a short description and a hint on what action to take.
|
|
@@ -289,16 +308,24 @@ declare function signWebhookPayload(payload: string, secret: string, timestampSe
|
|
|
289
308
|
* name: "checkout.started",
|
|
290
309
|
* developerUserId: userId,
|
|
291
310
|
* properties: scrubPiiFromProperties({
|
|
292
|
-
* url: req.url, // might contain "/users/wes@…/" — gets
|
|
311
|
+
* url: req.url, // might contain "/users/wes@…/" — gets <email>
|
|
293
312
|
* lastError: e.message, // might contain card numbers
|
|
294
313
|
* }),
|
|
295
314
|
* });
|
|
296
315
|
*/
|
|
297
316
|
/**
|
|
298
317
|
* Scrub a single string value: replace email-shaped substrings with
|
|
299
|
-
*
|
|
300
|
-
* the original string (===) when nothing matched
|
|
301
|
-
*
|
|
318
|
+
* `<email>` and card-number-shaped substrings with `<card>`. Returns
|
|
319
|
+
* the original string (===) when nothing matched.
|
|
320
|
+
*
|
|
321
|
+
* Implementation note: we call `.replace()` unconditionally rather than
|
|
322
|
+
* gating on `.test()`. The /g regexes are module-level so `.test()`
|
|
323
|
+
* carries `lastIndex` state between calls — a prior match leaves
|
|
324
|
+
* `lastIndex` mid-string and the next `.test()` can falsely return
|
|
325
|
+
* false on a string that DOES match. `.replace(/g)` always scans the
|
|
326
|
+
* full string regardless of `lastIndex`, so dropping the test-guard
|
|
327
|
+
* removes the sharp edge at zero cost (when nothing matches, replace
|
|
328
|
+
* returns the same `(===)` string).
|
|
302
329
|
*/
|
|
303
330
|
declare function scrubPii(value: string): string;
|
|
304
331
|
/**
|
|
@@ -339,7 +366,7 @@ declare function scrubPiiFromProperties(properties: Record<string, unknown>): Re
|
|
|
339
366
|
* - `sdk.no_durable_store`
|
|
340
367
|
* - `sdk.super_property_registered`
|
|
341
368
|
*/
|
|
342
|
-
type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
|
|
369
|
+
type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_permanent_failure" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
|
|
343
370
|
interface DebugContext {
|
|
344
371
|
[key: string]: unknown;
|
|
345
372
|
}
|
|
@@ -348,4 +375,4 @@ interface DebugLogger {
|
|
|
348
375
|
emit(signal: DebugSignal, message: string, context?: DebugContext): void;
|
|
349
376
|
}
|
|
350
377
|
|
|
351
|
-
export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
|
|
378
|
+
export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, SDK_NAME, SDK_VERSION, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
|
package/dist/index.mjs
CHANGED
|
@@ -316,9 +316,11 @@ function byteLength(s) {
|
|
|
316
316
|
return s.length * 4;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
// src/
|
|
319
|
+
// src/_version.ts
|
|
320
|
+
var SDK_VERSION = "1.3.1";
|
|
320
321
|
var SDK_NAME = "@cross-deck/node";
|
|
321
|
-
|
|
322
|
+
|
|
323
|
+
// src/http.ts
|
|
322
324
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
323
325
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
324
326
|
var CROSSDECK_API_VERSION = "2025-01-01";
|
|
@@ -831,6 +833,7 @@ var EventQueue = class {
|
|
|
831
833
|
sdk: env.sdk
|
|
832
834
|
};
|
|
833
835
|
if (env.appId) body.appId = env.appId;
|
|
836
|
+
if (env.environment) body.environment = env.environment;
|
|
834
837
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
835
838
|
body,
|
|
836
839
|
idempotencyKey: batchId
|
|
@@ -849,6 +852,20 @@ var EventQueue = class {
|
|
|
849
852
|
} catch (err) {
|
|
850
853
|
const message = err instanceof Error ? err.message : String(err);
|
|
851
854
|
this.lastError = message;
|
|
855
|
+
if (isPermanent4xx(err)) {
|
|
856
|
+
const droppedCount = batch.length;
|
|
857
|
+
this.pendingBatch = null;
|
|
858
|
+
this.pendingBatchId = null;
|
|
859
|
+
this.inFlight -= droppedCount;
|
|
860
|
+
this.dropped += droppedCount;
|
|
861
|
+
this.cfg.onDrop?.(droppedCount);
|
|
862
|
+
this.cfg.onPermanentFailure?.({
|
|
863
|
+
status: err.status ?? 0,
|
|
864
|
+
droppedCount,
|
|
865
|
+
lastError: message
|
|
866
|
+
});
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
852
869
|
const retryAfterMs = extractRetryAfterMs(err);
|
|
853
870
|
const delay = this.retry.nextDelay(retryAfterMs);
|
|
854
871
|
this.scheduleRetry(delay);
|
|
@@ -927,6 +944,14 @@ function extractRetryAfterMs(err) {
|
|
|
927
944
|
}
|
|
928
945
|
return void 0;
|
|
929
946
|
}
|
|
947
|
+
function isPermanent4xx(err) {
|
|
948
|
+
if (!err || typeof err !== "object") return false;
|
|
949
|
+
const status = err.status;
|
|
950
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
951
|
+
if (status < 400 || status >= 500) return false;
|
|
952
|
+
if (status === 408 || status === 429) return false;
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
930
955
|
function defaultScheduler(fn, ms) {
|
|
931
956
|
const id = setTimeout(fn, ms);
|
|
932
957
|
if (typeof id.unref === "function") {
|
|
@@ -1216,16 +1241,18 @@ var ErrorTracker = class {
|
|
|
1216
1241
|
const url = typeof input === "string" ? input : input?.url ?? "";
|
|
1217
1242
|
const method = (init.method || "GET").toUpperCase();
|
|
1218
1243
|
const start = Date.now();
|
|
1219
|
-
tracker.opts.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1244
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
1245
|
+
tracker.opts.breadcrumbs.add({
|
|
1246
|
+
timestamp: start,
|
|
1247
|
+
category: "http",
|
|
1248
|
+
message: `${method} ${url}`,
|
|
1249
|
+
data: { url, method }
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1225
1252
|
try {
|
|
1226
1253
|
const response = await origFetch(...args);
|
|
1227
1254
|
if (response.status >= 500 && tracker.opts.isConsented()) {
|
|
1228
|
-
if (!url.
|
|
1255
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
1229
1256
|
tracker.captureHttp({
|
|
1230
1257
|
url,
|
|
1231
1258
|
method,
|
|
@@ -1343,9 +1370,10 @@ var ErrorTracker = class {
|
|
|
1343
1370
|
if (!this.passesSample(err)) return;
|
|
1344
1371
|
if (!this.passesRateLimit(err)) return;
|
|
1345
1372
|
let finalErr = err;
|
|
1346
|
-
|
|
1373
|
+
const hook = this.opts.beforeSend?.();
|
|
1374
|
+
if (hook) {
|
|
1347
1375
|
try {
|
|
1348
|
-
finalErr =
|
|
1376
|
+
finalErr = hook(err);
|
|
1349
1377
|
} catch {
|
|
1350
1378
|
finalErr = err;
|
|
1351
1379
|
}
|
|
@@ -1571,6 +1599,22 @@ function safeClone(v) {
|
|
|
1571
1599
|
function safeStringify3(v) {
|
|
1572
1600
|
return coerceErrorPayload(v).message;
|
|
1573
1601
|
}
|
|
1602
|
+
function extractSelfHostname(baseUrl) {
|
|
1603
|
+
if (!baseUrl || typeof baseUrl !== "string") return null;
|
|
1604
|
+
try {
|
|
1605
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
1606
|
+
} catch {
|
|
1607
|
+
return null;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function isSelfRequest(requestUrl, selfHostname) {
|
|
1611
|
+
if (!selfHostname || !requestUrl) return false;
|
|
1612
|
+
try {
|
|
1613
|
+
return new URL(requestUrl).hostname.toLowerCase() === selfHostname;
|
|
1614
|
+
} catch {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1574
1618
|
|
|
1575
1619
|
// src/runtime-info.ts
|
|
1576
1620
|
import { hostname as osHostname, platform as osPlatform, release as osRelease } from "os";
|
|
@@ -2465,6 +2509,11 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2465
2509
|
intervalMs: options.eventFlushIntervalMs ?? 1500,
|
|
2466
2510
|
envelope: () => ({
|
|
2467
2511
|
appId: this.appId,
|
|
2512
|
+
// Ship env on every batch so the backend can cross-check
|
|
2513
|
+
// against the API-key-derived env and reject mismatches
|
|
2514
|
+
// loudly (env_mismatch). Web has always done this; node now
|
|
2515
|
+
// matches so defence-in-depth is symmetric across SDKs.
|
|
2516
|
+
environment: this.env,
|
|
2468
2517
|
sdk: { name: SDK_NAME, version: this.sdkVersion }
|
|
2469
2518
|
}),
|
|
2470
2519
|
onDrop: (count) => {
|
|
@@ -2480,6 +2529,20 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2480
2529
|
nextRetryMs: info.delayMs
|
|
2481
2530
|
});
|
|
2482
2531
|
},
|
|
2532
|
+
onPermanentFailure: (info) => {
|
|
2533
|
+
const headline = `[crossdeck] Event batch DROPPED (status ${info.status}): ${info.lastError}. ${info.droppedCount} event(s) lost \u2014 check your secret key + app config.`;
|
|
2534
|
+
console.error(headline);
|
|
2535
|
+
this.debug.emit(
|
|
2536
|
+
"sdk.flush_permanent_failure",
|
|
2537
|
+
headline,
|
|
2538
|
+
{ ...info }
|
|
2539
|
+
);
|
|
2540
|
+
this.emit("queue.permanent_failure", {
|
|
2541
|
+
status: info.status,
|
|
2542
|
+
droppedCount: info.droppedCount,
|
|
2543
|
+
error: info.lastError
|
|
2544
|
+
});
|
|
2545
|
+
},
|
|
2483
2546
|
onFirstFlushSuccess: () => {
|
|
2484
2547
|
this.debug.emit("sdk.first_event_sent", "First batch landed.");
|
|
2485
2548
|
}
|
|
@@ -2494,14 +2557,20 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2494
2557
|
report: (err) => this.reportCapturedError(err),
|
|
2495
2558
|
getContext: () => ({ ...this.errorContext }),
|
|
2496
2559
|
getTags: () => ({ ...this.errorTags }),
|
|
2497
|
-
|
|
2498
|
-
//
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2560
|
+
// GETTER, not a captured value — `setErrorBeforeSend()` mutates
|
|
2561
|
+
// `this.errorBeforeSend` after init() and the tracker MUST pick
|
|
2562
|
+
// up the new hook on the next error. Pre-fix we worked around
|
|
2563
|
+
// a captured-by-value field with `Object.defineProperty` on the
|
|
2564
|
+
// tracker's private opts; the contract is now a real getter so
|
|
2565
|
+
// we just hand it the closure and the hack is gone.
|
|
2566
|
+
beforeSend: () => this.errorBeforeSend,
|
|
2567
|
+
isConsented: () => true,
|
|
2568
|
+
// Derived from the configured baseUrl at construction time.
|
|
2569
|
+
// Used by the fetch wrapper to skip captureHttp on Crossdeck's
|
|
2570
|
+
// own requests — pre-fix the skip was hardcoded to
|
|
2571
|
+
// `api.cross-deck.com` and broke for customers on staging /
|
|
2572
|
+
// regional / self-hosted base URLs (recursive capture loop).
|
|
2573
|
+
selfHostname: extractSelfHostname(this.baseUrl)
|
|
2505
2574
|
});
|
|
2506
2575
|
this.errorTracker.install();
|
|
2507
2576
|
}
|
|
@@ -2514,6 +2583,7 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2514
2583
|
});
|
|
2515
2584
|
this.flushOnExit.install();
|
|
2516
2585
|
}
|
|
2586
|
+
this.emitDurabilityWarning();
|
|
2517
2587
|
if (options.testMode !== true && options.bootHeartbeat !== false) {
|
|
2518
2588
|
setImmediate(() => {
|
|
2519
2589
|
void this.heartbeat().catch((err) => {
|
|
@@ -2523,25 +2593,16 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2523
2593
|
{ message: err instanceof Error ? err.message : String(err) }
|
|
2524
2594
|
);
|
|
2525
2595
|
});
|
|
2526
|
-
this.
|
|
2596
|
+
this.emitBootTelemetryEvent();
|
|
2527
2597
|
});
|
|
2528
2598
|
}
|
|
2529
2599
|
}
|
|
2530
2600
|
/**
|
|
2531
|
-
* Emit the
|
|
2532
|
-
* is serverless
|
|
2533
|
-
*
|
|
2534
|
-
*
|
|
2535
|
-
*
|
|
2536
|
-
* carries no request body, so it cannot transport a structured
|
|
2537
|
-
* `durability` fact. The event pipeline can — every `track()` event
|
|
2538
|
-
* lands as an aggregatable document the backend can query, so
|
|
2539
|
-
* Crossdeck can compute fleet-wide "% serverless-with-no-durable-
|
|
2540
|
-
* store" from `sdk.boot` events (denominator = all `sdk.boot`,
|
|
2541
|
-
* numerator = those with `durability.coldStartDurable === false`).
|
|
2542
|
-
* The event rides the existing batched + retried + idempotent queue
|
|
2543
|
-
* and is drained by flush-on-exit, so it survives a serverless
|
|
2544
|
-
* teardown — it is NOT a local-only debug log.
|
|
2601
|
+
* Emit the honest "no cold-start durability" warning when the runtime
|
|
2602
|
+
* is serverless AND no `entitlementStore` is wired. Local-only debug
|
|
2603
|
+
* signal — no network call, no phone-home. Safe to fire from the
|
|
2604
|
+
* constructor before `setImmediate` because there is no I/O on this
|
|
2605
|
+
* path.
|
|
2545
2606
|
*
|
|
2546
2607
|
* `isServerless` AND no store is the gap: a cold start begins with an
|
|
2547
2608
|
* empty in-memory cache and a brief Crossdeck outage in that window
|
|
@@ -2549,14 +2610,15 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2549
2610
|
* unavoidable without a store — so the SDK STATES it (a
|
|
2550
2611
|
* `sdk.no_durable_store` debug warning) rather than hiding it.
|
|
2551
2612
|
*
|
|
2552
|
-
*
|
|
2553
|
-
*
|
|
2554
|
-
* the
|
|
2613
|
+
* Audit P1 #9: this used to live INSIDE `emitBootTelemetry()` which
|
|
2614
|
+
* itself sat inside the `bootHeartbeat` gate, so any developer who
|
|
2615
|
+
* set `bootHeartbeat: false` silently disabled the entire reason
|
|
2616
|
+
* `entitlementStore` exists. Now split: warning fires
|
|
2617
|
+
* unconditionally; the boot phone-home stays gated.
|
|
2555
2618
|
*/
|
|
2556
|
-
|
|
2619
|
+
emitDurabilityWarning() {
|
|
2557
2620
|
const isServerless = this.runtime.isServerless;
|
|
2558
2621
|
const hasStore = this.entitlementStore !== null;
|
|
2559
|
-
const coldStartDurable = hasStore || !isServerless;
|
|
2560
2622
|
if (isServerless && !hasStore) {
|
|
2561
2623
|
this.debug.emit(
|
|
2562
2624
|
"sdk.no_durable_store",
|
|
@@ -2564,6 +2626,26 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2564
2626
|
{ host: this.runtime.host, isServerless, durableStore: false }
|
|
2565
2627
|
);
|
|
2566
2628
|
}
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Emit the one-time `sdk.boot` telemetry event — the aggregatable
|
|
2632
|
+
* fact the backend pivots on (compute fleet-wide
|
|
2633
|
+
* "% serverless-with-no-durable-store"). Rides the batched + retried
|
|
2634
|
+
* + idempotent queue and is drained by flush-on-exit, so it survives
|
|
2635
|
+
* a serverless teardown.
|
|
2636
|
+
*
|
|
2637
|
+
* Why a `track()` event and not the heartbeat: `GET /v1/sdk/heartbeat`
|
|
2638
|
+
* carries no request body, so it cannot transport a structured
|
|
2639
|
+
* `durability` fact.
|
|
2640
|
+
*
|
|
2641
|
+
* Gated by `bootHeartbeat` (and `testMode`) because it IS a phone-
|
|
2642
|
+
* home — the unconditional surface is `emitDurabilityWarning()`,
|
|
2643
|
+
* which has no network call.
|
|
2644
|
+
*/
|
|
2645
|
+
emitBootTelemetryEvent() {
|
|
2646
|
+
const isServerless = this.runtime.isServerless;
|
|
2647
|
+
const hasStore = this.entitlementStore !== null;
|
|
2648
|
+
const coldStartDurable = hasStore || !isServerless;
|
|
2567
2649
|
try {
|
|
2568
2650
|
this.track({
|
|
2569
2651
|
name: "sdk.boot",
|
|
@@ -2875,7 +2957,13 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2875
2957
|
const normalized = events.map((event) => this.normalizeIngestEvent(event));
|
|
2876
2958
|
const body = {
|
|
2877
2959
|
events: normalized,
|
|
2878
|
-
sdk: { name: SDK_NAME, version: this.sdkVersion }
|
|
2960
|
+
sdk: { name: SDK_NAME, version: this.sdkVersion },
|
|
2961
|
+
// Match the queue's batch envelope (see event-queue.ts) — backend
|
|
2962
|
+
// cross-checks `environment` against the API-key-derived env and
|
|
2963
|
+
// rejects mismatches loudly (env_mismatch). Pre-fix this direct
|
|
2964
|
+
// ingest path skipped env, so a "live key, env: sandbox"
|
|
2965
|
+
// misconfig fell through silently for the bulk-import path.
|
|
2966
|
+
environment: this.env
|
|
2879
2967
|
};
|
|
2880
2968
|
if (this.appId) body.appId = this.appId;
|
|
2881
2969
|
return this.http.request("POST", "/events", {
|
|
@@ -2940,8 +3028,9 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
2940
3028
|
message: "syncPurchases requires a signedTransactionInfo string."
|
|
2941
3029
|
});
|
|
2942
3030
|
}
|
|
3031
|
+
const rail = input.rail ?? "apple";
|
|
2943
3032
|
return this.http.request("POST", "/purchases/sync", {
|
|
2944
|
-
body: {
|
|
3033
|
+
body: { ...input, rail },
|
|
2945
3034
|
signal: options?.signal,
|
|
2946
3035
|
timeoutMs: options?.timeoutMs
|
|
2947
3036
|
});
|
|
@@ -3536,10 +3625,21 @@ var CrossdeckServer = class extends EventEmitter {
|
|
|
3536
3625
|
* Resolve any hint shape (canonical customerId / userId hint /
|
|
3537
3626
|
* anonymousId hint / raw string) to a `crossdeckCustomerId` if we
|
|
3538
3627
|
* have a cache entry for it.
|
|
3628
|
+
*
|
|
3629
|
+
* String overload is STRICT on the canonical-id shape. Pre-fix
|
|
3630
|
+
* `isFresh(raw)` treated any string with a cache entry as a valid
|
|
3631
|
+
* canonical id — if tenant A's userId happened to collide with
|
|
3632
|
+
* tenant B's crossdeckCustomerId, A's call would resolve to B's
|
|
3633
|
+
* cached entitlements. Bounded by the `cdcust_` prefix convention
|
|
3634
|
+
* (which both SDKs and the backend mint, see
|
|
3635
|
+
* backend/src/lib/customers.ts) — anything else is treated purely
|
|
3636
|
+
* as an alias lookup, never as a canonical id. Audit P1 #19.
|
|
3539
3637
|
*/
|
|
3540
3638
|
resolveCacheCustomerId(hint) {
|
|
3541
3639
|
if (typeof hint === "string") {
|
|
3542
|
-
if (this.entitlementCache.isFresh(hint))
|
|
3640
|
+
if (hint.startsWith("cdcust_") && this.entitlementCache.isFresh(hint)) {
|
|
3641
|
+
return hint;
|
|
3642
|
+
}
|
|
3543
3643
|
return this.customerIdAliases.get(hint) ?? null;
|
|
3544
3644
|
}
|
|
3545
3645
|
if (hint.customerId) return hint.customerId;
|
|
@@ -3915,20 +4015,11 @@ function normaliseSecrets(input) {
|
|
|
3915
4015
|
// src/consent.ts
|
|
3916
4016
|
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
3917
4017
|
var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
3918
|
-
var REPLACEMENT_EMAIL = "
|
|
3919
|
-
var REPLACEMENT_CARD = "
|
|
4018
|
+
var REPLACEMENT_EMAIL = "<email>";
|
|
4019
|
+
var REPLACEMENT_CARD = "<card>";
|
|
3920
4020
|
function scrubPii(value) {
|
|
3921
4021
|
if (!value) return value;
|
|
3922
|
-
|
|
3923
|
-
if (EMAIL_PATTERN.test(out)) {
|
|
3924
|
-
out = out.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL);
|
|
3925
|
-
}
|
|
3926
|
-
EMAIL_PATTERN.lastIndex = 0;
|
|
3927
|
-
if (CARD_PATTERN.test(out)) {
|
|
3928
|
-
out = out.replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
3929
|
-
}
|
|
3930
|
-
CARD_PATTERN.lastIndex = 0;
|
|
3931
|
-
return out;
|
|
4022
|
+
return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
3932
4023
|
}
|
|
3933
4024
|
function scrubPiiFromProperties(properties) {
|
|
3934
4025
|
const out = {};
|