@cross-deck/node 0.1.0 → 1.1.0
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 +139 -0
- package/README.md +406 -124
- package/dist/auto-events/index.cjs +354 -0
- package/dist/auto-events/index.cjs.map +1 -0
- package/dist/auto-events/index.d.mts +316 -0
- package/dist/auto-events/index.d.ts +316 -0
- package/dist/auto-events/index.mjs +322 -0
- package/dist/auto-events/index.mjs.map +1 -0
- package/dist/crossdeck-server-BXQaFjVx.d.mts +1414 -0
- package/dist/crossdeck-server-BXQaFjVx.d.ts +1414 -0
- package/dist/index.cjs +3069 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +339 -178
- package/dist/index.d.ts +339 -178
- package/dist/index.mjs +3054 -180
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,187 +1,348 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
interface PurchaseResult {
|
|
45
|
-
object: "purchase_result";
|
|
46
|
-
crossdeckCustomerId: string;
|
|
47
|
-
env: Environment;
|
|
48
|
-
entitlements: PublicEntitlement[];
|
|
49
|
-
}
|
|
50
|
-
interface ForgetResult {
|
|
51
|
-
object: "forgot";
|
|
52
|
-
crossdeckCustomerId: string | null;
|
|
53
|
-
queuedAt: number;
|
|
54
|
-
env: Environment;
|
|
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 EntitlementsListResponse, w as EntitlementsListener, x as Environment, y as ErrorCaptureConfig, z as ErrorLevel, F as EventProperties, G as ForgetResult, H as GrantDuration, I as GrantEntitlementInput, J as GroupMembership, K as HeartbeatResponse, L as HttpRequestInfo, M as HttpResponseInfo, N as HttpRetriesConfig, O as IdentifyOptions, P as IdentityHints, Q as IngestOptions, R as IngestResponse, S as PublicEntitlement, T as PurchaseResult, U as RequestOptions, V as RevokeEntitlementInput, W as RuntimeHost, X as RuntimeInfo, Y as SDK_NAME, Z as SDK_VERSION, _ as ServerEvent, $ as StackFrame, a0 as SyncPurchaseInput, a1 as makeCrossdeckError } from './crossdeck-server-BXQaFjVx.js';
|
|
2
|
+
import 'node:events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Machine-readable index of every error code `@cross-deck/node` can
|
|
6
|
+
* throw, with a short description and a hint on what action to take.
|
|
7
|
+
* Mirrors `@cross-deck/web/src/error-codes.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Stripe publishes the same surface at stripe.com/docs/error-codes;
|
|
10
|
+
* developers love it because every code has a canonical "what does
|
|
11
|
+
* this mean / what should I do" answer.
|
|
12
|
+
*
|
|
13
|
+
* Differences from web:
|
|
14
|
+
* - Drops browser-only config codes (`invalid_public_key`,
|
|
15
|
+
* `missing_app_id`, `invalid_environment`, `environment_mismatch`,
|
|
16
|
+
* `not_initialized`).
|
|
17
|
+
* - Adds `invalid_secret_key` (Node SDK takes a secret key, not a
|
|
18
|
+
* publishable key + env declaration).
|
|
19
|
+
* - Adds Node-lifecycle codes (`flush_on_exit_failed`,
|
|
20
|
+
* `webhook_invalid_signature`, `webhook_replay_window_exceeded`,
|
|
21
|
+
* `webhook_missing_secret`).
|
|
22
|
+
*
|
|
23
|
+
* Adding a new error code:
|
|
24
|
+
* 1. Throw it as `CrossdeckError.code` in the call site.
|
|
25
|
+
* 2. Add an entry here so the dashboard + AI assistants can render
|
|
26
|
+
* the canonical fix.
|
|
27
|
+
*
|
|
28
|
+
* Keep entries terse — consumers surface this in tooltips and
|
|
29
|
+
* automated tickets, not in long-form docs.
|
|
30
|
+
*/
|
|
31
|
+
interface ErrorCodeEntry {
|
|
32
|
+
/** The string thrown as `CrossdeckError.code`. */
|
|
33
|
+
code: string;
|
|
34
|
+
/** Broad category — `CrossdeckError.type`. */
|
|
35
|
+
type: "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
|
|
36
|
+
/** One-sentence description. Surfaced verbatim in dashboards. */
|
|
37
|
+
description: string;
|
|
38
|
+
/** What the developer should do. Imperative phrasing. */
|
|
39
|
+
resolution: string;
|
|
40
|
+
/** True for codes the SDK can auto-recover from (no developer action). */
|
|
41
|
+
retryable: boolean;
|
|
55
42
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Internal source-of-truth with literal types preserved via `as const`.
|
|
45
|
+
* The exported `CROSSDECK_ERROR_CODES` widens to `readonly
|
|
46
|
+
* ErrorCodeEntry[]` for the public surface (callers iterating it
|
|
47
|
+
* shouldn't depend on literal positions), while `CrossdeckErrorCode`
|
|
48
|
+
* below derives the literal union from this constant for type-safe
|
|
49
|
+
* code narrowing.
|
|
50
|
+
*/
|
|
51
|
+
declare const _CROSSDECK_ERROR_CODES: readonly [{
|
|
52
|
+
readonly code: "invalid_secret_key";
|
|
53
|
+
readonly type: "configuration_error";
|
|
54
|
+
readonly description: "The secret key passed to new CrossdeckServer({ secretKey }) doesn't start with cd_sk_.";
|
|
55
|
+
readonly resolution: "Copy the key from your Crossdeck dashboard → API keys page. Server SDKs use cd_sk_test_… (sandbox) or cd_sk_live_… (production). Never ship this key to a browser.";
|
|
56
|
+
readonly retryable: false;
|
|
57
|
+
}, {
|
|
58
|
+
readonly code: "missing_user_id";
|
|
59
|
+
readonly type: "invalid_request_error";
|
|
60
|
+
readonly description: "identify() / aliasIdentity() called with an empty userId.";
|
|
61
|
+
readonly resolution: "Pass a stable, non-empty user identifier from your auth layer — never a hardcoded placeholder.";
|
|
62
|
+
readonly retryable: false;
|
|
63
|
+
}, {
|
|
64
|
+
readonly code: "missing_anonymous_id";
|
|
65
|
+
readonly type: "invalid_request_error";
|
|
66
|
+
readonly description: "aliasIdentity() called with an empty anonymousId.";
|
|
67
|
+
readonly resolution: "Pass the anonymousId originally minted by the web SDK on this user's device.";
|
|
68
|
+
readonly retryable: false;
|
|
69
|
+
}, {
|
|
70
|
+
readonly code: "missing_customer_id";
|
|
71
|
+
readonly type: "invalid_request_error";
|
|
72
|
+
readonly description: "An operation that requires a Crossdeck customer ID was called with an empty value.";
|
|
73
|
+
readonly resolution: "Pass the customerId returned from a prior identify() / getEntitlements() call.";
|
|
74
|
+
readonly retryable: false;
|
|
75
|
+
}, {
|
|
76
|
+
readonly code: "missing_identity";
|
|
77
|
+
readonly type: "invalid_request_error";
|
|
78
|
+
readonly description: "An ingest / forget / entitlements call received no identity hints.";
|
|
79
|
+
readonly resolution: "Pass at least one of customerId, userId, or anonymousId on the call (or per-event for ingest).";
|
|
80
|
+
readonly retryable: false;
|
|
81
|
+
}, {
|
|
82
|
+
readonly code: "missing_event_name";
|
|
83
|
+
readonly type: "invalid_request_error";
|
|
84
|
+
readonly description: "track() / ingest() received an event without a name.";
|
|
85
|
+
readonly resolution: "Pass a non-empty string as the event name. The wire shape is { name, properties? }.";
|
|
86
|
+
readonly retryable: false;
|
|
87
|
+
}, {
|
|
88
|
+
readonly code: "missing_events";
|
|
89
|
+
readonly type: "invalid_request_error";
|
|
90
|
+
readonly description: "ingest() received an empty array.";
|
|
91
|
+
readonly resolution: "Pass at least one event. Use server.track(event) to send a single event.";
|
|
92
|
+
readonly retryable: false;
|
|
93
|
+
}, {
|
|
94
|
+
readonly code: "missing_event_id";
|
|
95
|
+
readonly type: "invalid_request_error";
|
|
96
|
+
readonly description: "getAuditEntry() called with an empty eventId.";
|
|
97
|
+
readonly resolution: "Pass the eventId from the audit row you want to inspect.";
|
|
98
|
+
readonly retryable: false;
|
|
99
|
+
}, {
|
|
100
|
+
readonly code: "missing_signed_transaction_info";
|
|
101
|
+
readonly type: "invalid_request_error";
|
|
102
|
+
readonly description: "syncPurchases() called without StoreKit 2 signed transaction info.";
|
|
103
|
+
readonly resolution: "Pass the JWS string from Transaction.currentEntitlements / Transaction.updates.";
|
|
104
|
+
readonly retryable: false;
|
|
105
|
+
}, {
|
|
106
|
+
readonly code: "missing_group_type";
|
|
107
|
+
readonly type: "invalid_request_error";
|
|
108
|
+
readonly description: "group(type, id) called with an empty type.";
|
|
109
|
+
readonly resolution: "Pass a non-empty group type (e.g. \"org\", \"team\", \"plan\") as the first argument.";
|
|
110
|
+
readonly retryable: false;
|
|
111
|
+
}, {
|
|
112
|
+
readonly code: "serialization_failed";
|
|
113
|
+
readonly type: "invalid_request_error";
|
|
114
|
+
readonly description: "An event payload or trait bag could not be JSON-serialised even after sanitisation.";
|
|
115
|
+
readonly resolution: "Inspect the payload for non-JSON-friendly values (functions, symbols, deeply circular refs). The SDK's validator drops these by default, so this usually means a bug — file an issue with the payload shape.";
|
|
116
|
+
readonly retryable: false;
|
|
117
|
+
}, {
|
|
118
|
+
readonly code: "fetch_failed";
|
|
119
|
+
readonly type: "network_error";
|
|
120
|
+
readonly description: "The underlying fetch() call failed (typically a network outage, DNS, or refused connection).";
|
|
121
|
+
readonly resolution: "Check the host's outbound network. The SDK retries automatically with exponential backoff + jitter for queued events.";
|
|
122
|
+
readonly retryable: true;
|
|
123
|
+
}, {
|
|
124
|
+
readonly code: "request_timeout";
|
|
125
|
+
readonly type: "network_error";
|
|
126
|
+
readonly description: "A request was aborted after the configured timeoutMs (default 15s).";
|
|
127
|
+
readonly resolution: "Check the host's network. Increase timeoutMs in CrossdeckServer options if you're on a known-slow link.";
|
|
128
|
+
readonly retryable: true;
|
|
129
|
+
}, {
|
|
130
|
+
readonly code: "invalid_json_response";
|
|
131
|
+
readonly type: "internal_error";
|
|
132
|
+
readonly description: "The server returned a 2xx with an unparseable body.";
|
|
133
|
+
readonly resolution: "Likely a transient backend bug. Retry; if it persists, contact support with the requestId.";
|
|
134
|
+
readonly retryable: true;
|
|
135
|
+
}, {
|
|
136
|
+
readonly code: "flush_on_exit_failed";
|
|
137
|
+
readonly type: "internal_error";
|
|
138
|
+
readonly description: "The on-exit drain (beforeExit / SIGTERM / SIGINT) did not complete before flushOnExitTimeoutMs.";
|
|
139
|
+
readonly resolution: "Increase flushOnExitTimeoutMs in CrossdeckServer options. Default is 2000ms; serverless runtimes typically allow 5-10s before SIGKILL. If events are dropping silently in production, raise this.";
|
|
140
|
+
readonly retryable: false;
|
|
141
|
+
}, {
|
|
142
|
+
readonly code: "webhook_invalid_signature";
|
|
143
|
+
readonly type: "authentication_error";
|
|
144
|
+
readonly description: "The webhook signature header did not verify against the supplied secret.";
|
|
145
|
+
readonly resolution: "Confirm the secret matches the one in your Crossdeck dashboard → Webhooks page. If the request is genuinely from Crossdeck, the secret is wrong, stale, or recently rotated.";
|
|
146
|
+
readonly retryable: false;
|
|
147
|
+
}, {
|
|
148
|
+
readonly code: "webhook_replay_window_exceeded";
|
|
149
|
+
readonly type: "authentication_error";
|
|
150
|
+
readonly description: "The webhook timestamp is older than the replay-tolerance window (default 5 minutes).";
|
|
151
|
+
readonly resolution: "The webhook is either replayed or your receiving clock is wildly skewed. Verify NTP on the receiving host. Increase replayToleranceMs only if you accept the replay-attack risk.";
|
|
152
|
+
readonly retryable: false;
|
|
153
|
+
}, {
|
|
154
|
+
readonly code: "webhook_missing_secret";
|
|
155
|
+
readonly type: "configuration_error";
|
|
156
|
+
readonly description: "verifyWebhookSignature() was called without a signing secret.";
|
|
157
|
+
readonly resolution: "Pass the secret from your Crossdeck dashboard → Webhooks page. Never hardcode in source — read from an env var.";
|
|
158
|
+
readonly retryable: false;
|
|
159
|
+
}];
|
|
160
|
+
/**
|
|
161
|
+
* Literal union of every code documented in `CROSSDECK_ERROR_CODES`.
|
|
162
|
+
* Exported so callers can do type-safe code comparisons:
|
|
163
|
+
*
|
|
164
|
+
* if (err.code === "webhook_invalid_signature") { ... } // typed
|
|
165
|
+
* if (err.code === "webook_invalid_signature") { ... } // TS error
|
|
166
|
+
*
|
|
167
|
+
* Mis-spelling a code at compile time fails to compile — the gap that
|
|
168
|
+
* silently broke v0.1.0 callers checking for non-existent codes.
|
|
169
|
+
*/
|
|
170
|
+
type CrossdeckErrorCode = (typeof _CROSSDECK_ERROR_CODES)[number]["code"];
|
|
171
|
+
/** Type guard: narrows a string to the documented literal union. */
|
|
172
|
+
declare function isCrossdeckErrorCode(code: string): code is CrossdeckErrorCode;
|
|
173
|
+
/**
|
|
174
|
+
* Public catalogue, widened to `readonly ErrorCodeEntry[]` for iteration
|
|
175
|
+
* stability. Callers needing literal types should reach for the
|
|
176
|
+
* `CrossdeckErrorCode` union above instead of indexing into this array.
|
|
177
|
+
*/
|
|
178
|
+
declare const CROSSDECK_ERROR_CODES: readonly ErrorCodeEntry[];
|
|
179
|
+
/** Lookup helper — returns the entry matching a CrossdeckError.code, or undefined. */
|
|
180
|
+
declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Webhook signature verification — Stripe pattern.
|
|
184
|
+
*
|
|
185
|
+
* Lets customers verify the events Crossdeck sends to THEM. Table-stakes
|
|
186
|
+
* for any backend SDK (Stripe ships `Stripe.webhooks.constructEvent()`
|
|
187
|
+
* from day one, Svix ships `Webhook.verify()` from day one).
|
|
188
|
+
*
|
|
189
|
+
* Wire format:
|
|
190
|
+
* Header `Crossdeck-Signature: t=<unix-seconds>,v1=<hex>`
|
|
191
|
+
* Where `v1` is HMAC-SHA256(secret, `${t}.${payload}`) — Stripe-compatible.
|
|
192
|
+
*
|
|
193
|
+
* Customers receive a signing secret from the Crossdeck dashboard
|
|
194
|
+
* (one-time reveal at mint time; rotated as needed). Each webhook
|
|
195
|
+
* carries the signature header above. The customer's handler:
|
|
196
|
+
*
|
|
197
|
+
* import { verifyWebhookSignature } from "@cross-deck/node";
|
|
198
|
+
*
|
|
199
|
+
* app.post("/crossdeck-webhook", express.raw({ type: "application/json" }), (req, res) => {
|
|
200
|
+
* try {
|
|
201
|
+
* const event = verifyWebhookSignature(
|
|
202
|
+
* req.body.toString("utf8"),
|
|
203
|
+
* req.headers["crossdeck-signature"],
|
|
204
|
+
* process.env.CROSSDECK_WEBHOOK_SECRET,
|
|
205
|
+
* );
|
|
206
|
+
* // event is the parsed JSON payload
|
|
207
|
+
* handleCrossdeckEvent(event);
|
|
208
|
+
* res.sendStatus(200);
|
|
209
|
+
* } catch (err) {
|
|
210
|
+
* res.sendStatus(401);
|
|
211
|
+
* }
|
|
212
|
+
* });
|
|
213
|
+
*
|
|
214
|
+
* The signing scheme is constant-time via `crypto.timingSafeEqual` so
|
|
215
|
+
* a malicious caller can't extract the signature by measuring response
|
|
216
|
+
* timing. Replay defence: timestamps older than `replayToleranceMs`
|
|
217
|
+
* (default 5 min) are rejected — required because HMAC-SHA256 is
|
|
218
|
+
* stateless and would otherwise allow an attacker to replay an old
|
|
219
|
+
* webhook indefinitely.
|
|
220
|
+
*
|
|
221
|
+
* Supports multiple secrets for rotation: pass an array; the helper
|
|
222
|
+
* tries each, accepts on the first match. Lets customers rotate the
|
|
223
|
+
* dashboard secret without dropping in-flight webhooks.
|
|
224
|
+
*/
|
|
225
|
+
interface VerifyWebhookOptions {
|
|
61
226
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
227
|
+
* Maximum age of the webhook timestamp in milliseconds. Default
|
|
228
|
+
* 5 minutes (5 * 60 * 1000). Anything older than this is rejected
|
|
229
|
+
* as a replay. Pass 0 to disable the replay window (NOT recommended
|
|
230
|
+
* — accept the trade-off only if you have a separate replay defence).
|
|
65
231
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
interface IdentifyOptions {
|
|
74
|
-
email?: string;
|
|
75
|
-
traits?: Record<string, unknown>;
|
|
76
|
-
}
|
|
77
|
-
interface AliasIdentityInput extends IdentifyOptions {
|
|
78
|
-
userId: string;
|
|
79
|
-
anonymousId: string;
|
|
80
|
-
}
|
|
81
|
-
type ErrorLevel = "error" | "warning" | "info";
|
|
82
|
-
interface ServerEvent {
|
|
83
|
-
eventId?: string;
|
|
84
|
-
name: string;
|
|
85
|
-
timestamp?: number;
|
|
86
|
-
properties?: Record<string, unknown>;
|
|
87
|
-
developerUserId?: string;
|
|
88
|
-
anonymousId?: string;
|
|
89
|
-
crossdeckCustomerId?: string;
|
|
90
|
-
level?: ErrorLevel;
|
|
91
|
-
tags?: Record<string, string>;
|
|
92
|
-
categoryTags?: string[];
|
|
93
|
-
}
|
|
94
|
-
interface IngestOptions {
|
|
95
|
-
idempotencyKey?: string;
|
|
96
|
-
}
|
|
97
|
-
interface SyncPurchaseInput {
|
|
98
|
-
rail?: "apple";
|
|
99
|
-
signedTransactionInfo: string;
|
|
100
|
-
signedRenewalInfo?: string;
|
|
101
|
-
appAccountToken?: string;
|
|
102
|
-
}
|
|
103
|
-
type GrantDuration = "P30D" | "P90D" | "P1Y" | "lifetime";
|
|
104
|
-
interface GrantEntitlementInput {
|
|
105
|
-
customerId: string;
|
|
106
|
-
entitlementKey: string;
|
|
107
|
-
duration: GrantDuration;
|
|
108
|
-
reason: string;
|
|
109
|
-
}
|
|
110
|
-
interface RevokeEntitlementInput {
|
|
111
|
-
customerId: string;
|
|
112
|
-
entitlementKey: string;
|
|
113
|
-
reason: string;
|
|
114
|
-
}
|
|
115
|
-
interface EntitlementMutationResult {
|
|
116
|
-
object: "entitlement_mutation";
|
|
117
|
-
action: "grant" | "revoke";
|
|
118
|
-
crossdeckCustomerId: string;
|
|
119
|
-
entitlement: PublicEntitlement;
|
|
120
|
-
env: Environment;
|
|
121
|
-
}
|
|
122
|
-
type AuditDecision = "applied" | "no_op" | "rejected";
|
|
123
|
-
interface AuditEntry {
|
|
124
|
-
eventId: string;
|
|
125
|
-
rail: AuditRail;
|
|
126
|
-
env: Environment;
|
|
127
|
-
eventType: string;
|
|
128
|
-
projectId: string;
|
|
129
|
-
subscriptionId?: string;
|
|
130
|
-
customerId?: string;
|
|
131
|
-
fromState?: string;
|
|
132
|
-
toState?: string;
|
|
133
|
-
decision: AuditDecision;
|
|
134
|
-
reason?: string;
|
|
135
|
-
derivedSignal?: string;
|
|
136
|
-
signatureVerified: boolean;
|
|
137
|
-
reconciledWithProvider: boolean;
|
|
138
|
-
rawEventReceivedAt: number;
|
|
139
|
-
processedAt: number;
|
|
232
|
+
replayToleranceMs?: number;
|
|
233
|
+
/**
|
|
234
|
+
* Override the current time. Tests use this to verify timestamp
|
|
235
|
+
* handling deterministically. Defaults to `Date.now()`.
|
|
236
|
+
*/
|
|
237
|
+
now?: () => number;
|
|
140
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Verify a Crossdeck-signed webhook. Returns the parsed JSON payload
|
|
241
|
+
* on success. Throws `CrossdeckError` on:
|
|
242
|
+
* - missing / malformed signature header (`webhook_invalid_signature`)
|
|
243
|
+
* - missing secret (`webhook_missing_secret`)
|
|
244
|
+
* - timestamp outside replay window (`webhook_replay_window_exceeded`)
|
|
245
|
+
* - HMAC mismatch (`webhook_invalid_signature`)
|
|
246
|
+
* - non-JSON payload (`webhook_invalid_signature` — same code because
|
|
247
|
+
* a tampered payload that breaks JSON parses as invalid)
|
|
248
|
+
*
|
|
249
|
+
* `secret` accepts a single string or an array of strings (for
|
|
250
|
+
* rotation). Any one match is sufficient.
|
|
251
|
+
*/
|
|
252
|
+
declare function verifyWebhookSignature(payload: string, signatureHeader: string | string[] | undefined, secret: string | string[] | undefined, options?: VerifyWebhookOptions): unknown;
|
|
253
|
+
/**
|
|
254
|
+
* Pure-function signing — mirror of what the Crossdeck backend does
|
|
255
|
+
* when sending a webhook. Exported so customers building their own
|
|
256
|
+
* test fixtures (a service that sends Crossdeck-signed webhooks to
|
|
257
|
+
* their own test harness) can re-use the canonical signing scheme
|
|
258
|
+
* instead of re-implementing it.
|
|
259
|
+
*
|
|
260
|
+
* const ts = Math.floor(Date.now() / 1000);
|
|
261
|
+
* const sig = signWebhookPayload(payload, secret, ts);
|
|
262
|
+
* const header = `t=${ts},v1=${sig}`;
|
|
263
|
+
*
|
|
264
|
+
* NOT marked as a security primitive for general HMAC — use
|
|
265
|
+
* `node:crypto` directly for that. This is only the
|
|
266
|
+
* Crossdeck-signature shape.
|
|
267
|
+
*/
|
|
268
|
+
declare function signWebhookPayload(payload: string, secret: string, timestampSec: number): string;
|
|
141
269
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
270
|
+
/**
|
|
271
|
+
* PII scrub utilities — Node port of `@cross-deck/web/src/consent.ts`'s
|
|
272
|
+
* regex-based defence layer.
|
|
273
|
+
*
|
|
274
|
+
* **What's NOT here (intentionally)**: the `ConsentManager` class. That's
|
|
275
|
+
* a browser/end-user UX surface (DNT detection, per-dimension consent
|
|
276
|
+
* gating). On the server side, the customer's user already accepted
|
|
277
|
+
* or declined consent in their client; the API caller decides what
|
|
278
|
+
* to forward. Shipping `ConsentManager` here would imply server-side
|
|
279
|
+
* gating that doesn't match the trust model.
|
|
280
|
+
*
|
|
281
|
+
* **What IS here**: opt-in utilities customers can use to scrub
|
|
282
|
+
* email-shaped and card-number-shaped substrings out of event
|
|
283
|
+
* properties before forwarding to Crossdeck. Stripe-grade defence in
|
|
284
|
+
* depth, applied at the caller's discretion:
|
|
285
|
+
*
|
|
286
|
+
* import { scrubPiiFromProperties } from "@cross-deck/node";
|
|
287
|
+
*
|
|
288
|
+
* server.track({
|
|
289
|
+
* name: "checkout.started",
|
|
290
|
+
* developerUserId: userId,
|
|
291
|
+
* properties: scrubPiiFromProperties({
|
|
292
|
+
* url: req.url, // might contain "/users/wes@…/" — gets [email]
|
|
293
|
+
* lastError: e.message, // might contain card numbers
|
|
294
|
+
* }),
|
|
295
|
+
* });
|
|
296
|
+
*/
|
|
297
|
+
/**
|
|
298
|
+
* Scrub a single string value: replace email-shaped substrings with
|
|
299
|
+
* `[email]` and card-number-shaped substrings with `[card]`. Returns
|
|
300
|
+
* the original string (===) when nothing matched, so callers can do
|
|
301
|
+
* an identity-check to skip allocating a new event copy.
|
|
302
|
+
*/
|
|
303
|
+
declare function scrubPii(value: string): string;
|
|
304
|
+
/**
|
|
305
|
+
* Walk a property bag and replace PII-shaped strings in place. Returns
|
|
306
|
+
* a new object with strings scrubbed; non-string values pass through
|
|
307
|
+
* unchanged.
|
|
308
|
+
*
|
|
309
|
+
* Defensive copy — the input is never altered. Caller can pass the
|
|
310
|
+
* result straight to `server.track()`.
|
|
311
|
+
*
|
|
312
|
+
* Recursive: nested objects + arrays are walked. Functions, symbols,
|
|
313
|
+
* Dates, etc. pass through untouched (those are the
|
|
314
|
+
* `validateEventProperties` sanitiser's job — this is just the
|
|
315
|
+
* PII regex pass).
|
|
316
|
+
*/
|
|
317
|
+
declare function scrubPiiFromProperties(properties: Record<string, unknown>): Record<string, unknown>;
|
|
163
318
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Debug signal vocabulary — NorthStar §16, Node port of
|
|
321
|
+
* `@cross-deck/web/src/debug.ts`.
|
|
322
|
+
*
|
|
323
|
+
* The SDK speaks a small fixed vocabulary of signals so the dashboard's
|
|
324
|
+
* onboarding checklist and the developer's console output both speak
|
|
325
|
+
* the same words. When `debug: true` is set in `CrossdeckServerOptions`,
|
|
326
|
+
* the signals are also logged to `console.info` so a developer doing
|
|
327
|
+
* copy-paste integration sees actionable feedback live.
|
|
328
|
+
*
|
|
329
|
+
* Signal names are STABLE — adding new ones is fine, renaming is a
|
|
330
|
+
* breaking change because the dashboard onboarding step keys off them.
|
|
331
|
+
*
|
|
332
|
+
* Node-specific additions beyond web's vocabulary:
|
|
333
|
+
* - `sdk.flush_on_exit_started` / `sdk.flush_on_exit_completed`
|
|
334
|
+
* - `sdk.webhook_verified`
|
|
335
|
+
* - `sdk.runtime_detected`
|
|
336
|
+
* - `sdk.entitlement_cache_warm`
|
|
337
|
+
* - `sdk.super_property_registered`
|
|
338
|
+
*/
|
|
339
|
+
type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "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";
|
|
340
|
+
interface DebugContext {
|
|
341
|
+
[key: string]: unknown;
|
|
172
342
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
readonly requestId?: string;
|
|
177
|
-
readonly status?: number;
|
|
178
|
-
readonly retryAfterMs?: number;
|
|
179
|
-
constructor(payload: CrossdeckErrorPayload);
|
|
343
|
+
interface DebugLogger {
|
|
344
|
+
enabled: boolean;
|
|
345
|
+
emit(signal: DebugSignal, message: string, context?: DebugContext): void;
|
|
180
346
|
}
|
|
181
347
|
|
|
182
|
-
|
|
183
|
-
declare const SDK_VERSION = "0.1.0";
|
|
184
|
-
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
185
|
-
declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
186
|
-
|
|
187
|
-
export { type AliasIdentityInput, type AliasResult, type AuditDecision, type AuditEntry, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, CrossdeckServer, type CrossdeckServerOptions, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, type EntitlementMutationResult, type EntitlementsListResponse, type Environment, type ErrorLevel, type ForgetResult, type GrantDuration, type GrantEntitlementInput, type IdentifyOptions, type IdentityHints, type IngestOptions, type IngestResponse, type PublicEntitlement, type RevokeEntitlementInput, SDK_NAME, SDK_VERSION, type ServerEvent, type SyncPurchaseInput };
|
|
348
|
+
export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
|