@klarna/add-to-klarna 1.0.0 → 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/README.md +55 -27
- package/dist/index.cjs +94 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -11
- package/dist/index.d.ts +38 -11
- package/dist/index.js +94 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @klarna/add-to-klarna
|
|
2
2
|
|
|
3
|
-
> Merchant integration library for generating Klarna "Add to Klarna"
|
|
3
|
+
> Merchant integration library for generating Klarna "Add to Klarna" AppsFlyer OneLinks.
|
|
4
4
|
|
|
5
5
|
[![Build Status][ci-image]][ci-url]
|
|
6
6
|
[![License][license-image]][license-url]
|
|
7
7
|
[![Developed at Klarna][klarna-image]][klarna-url]
|
|
8
8
|
|
|
9
|
-
Lets a merchant page turn a `brandNickname` + `inputId` into a fully encrypted Klarna
|
|
9
|
+
Lets a merchant page turn a `brandNickname` + `inputId` into a fully encrypted Klarna AppsFlyer OneLink in one call, without the merchant having to touch JWE, JWKS, key rotation, or URL encoding.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -79,7 +79,7 @@ Drop the library straight into an HTML page via an ESM-aware CDN — no
|
|
|
79
79
|
|
|
80
80
|
```html
|
|
81
81
|
<script type="module">
|
|
82
|
-
import { createAddToKlarnaClient } from "https://esm.sh/@klarna/add-to-klarna@
|
|
82
|
+
import { createAddToKlarnaClient } from "https://esm.sh/@klarna/add-to-klarna@2.0.1";
|
|
83
83
|
|
|
84
84
|
const klarna = createAddToKlarnaClient({ region: "eu" });
|
|
85
85
|
|
|
@@ -92,45 +92,73 @@ Drop the library straight into an HTML page via an ESM-aware CDN — no
|
|
|
92
92
|
</script>
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
`esm.sh` (above), `https://cdn.jsdelivr.net/npm/@klarna/add-to-klarna@
|
|
96
|
-
and `https://unpkg.com/@klarna/add-to-klarna@
|
|
95
|
+
`esm.sh` (above), `https://cdn.jsdelivr.net/npm/@klarna/add-to-klarna@2.0.1/+esm`,
|
|
96
|
+
and `https://unpkg.com/@klarna/add-to-klarna@2.0.1?module` all serve the
|
|
97
97
|
ESM build and resolve the `jose` dependency transparently. Always pin a
|
|
98
98
|
version in production so a future release can't change behaviour under
|
|
99
99
|
your page.
|
|
100
100
|
|
|
101
101
|
## Configuration
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
`
|
|
103
|
+
Only `region` is required. `environment` defaults to `"production"` and
|
|
104
|
+
`clientTarget` defaults to `"pink"` — the public production Klarna app —
|
|
105
|
+
so a merchant integration can usually pass just the region.
|
|
105
106
|
|
|
106
|
-
| Option
|
|
107
|
-
|
|
|
108
|
-
| `environment`
|
|
109
|
-
| `
|
|
107
|
+
| Option | Type | Default | Description |
|
|
108
|
+
| -------------- | -------------------------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `environment` | `"production" \| "staging"` | `"production"` | Which Klarna backend to target. Picks the JWKS endpoint. Desktop fallback is the same production page for every environment. |
|
|
110
|
+
| `clientTarget` | `"pink" \| "internalpink" \| "yellow" \| "staging" \| "oneoff" \| "local"` | `"pink"` | Which Klarna client application to target. Picks the AppsFlyer OneLink base URL. Merchants should leave this at `"pink"`; the other values are Klarna-internal builds. |
|
|
111
|
+
| `region` | `"eu" \| "us" \| "ap"` | _(required)_ | Which deployment region to target. Picks the key inside the JWKS via a `kid-{region}-` prefix. |
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
`environment` and `clientTarget` are independent by design: internal builds
|
|
114
|
+
of the Klarna app (`yellow`, `oneoff`, `local`, …) can point at either the
|
|
115
|
+
production or the staging backend, so the JWKS host and the AppsFlyer host
|
|
116
|
+
are configured separately.
|
|
117
|
+
|
|
118
|
+
There are no other knobs — by design. The clock, the UUID generator, and
|
|
119
|
+
the `fetch` implementation are all fixed (the library uses
|
|
120
|
+
`globalThis.fetch`, `Date.now`, and `globalThis.crypto.randomUUID`).
|
|
115
121
|
|
|
116
122
|
## URL shape
|
|
117
123
|
|
|
118
|
-
The library
|
|
124
|
+
The library emits an [AppsFlyer OneLink](https://support.appsflyer.com/hc/en-us/articles/207032246-OneLink-link-management) URL that routes based on the device:
|
|
125
|
+
|
|
126
|
+
- **Mobile with the Klarna app installed** — opens the app to the add-to-klarna deep link.
|
|
127
|
+
- **Mobile without the app** — AppsFlyer routes to the App Store / Play Store per the OneLink template configuration and replays the deep link after install (deferred deep linking).
|
|
128
|
+
- **Desktop** — falls back to `https://klarna.com/add-to-klarna` (`af_web_dp`).
|
|
129
|
+
|
|
130
|
+
The composed URL has the shape:
|
|
119
131
|
|
|
120
132
|
```
|
|
121
|
-
<
|
|
133
|
+
<oneLinkBase>?pid=WebApp
|
|
134
|
+
&c=add-to-klarna
|
|
135
|
+
&af_dp=<url-encoded scheme + /loyalty-cards-v2/add-to-klarna/<brandNickname>/<encryptedPayload>>
|
|
136
|
+
&deep_link_value=<url-encoded /loyalty-cards-v2/add-to-klarna/<brandNickname>/<encryptedPayload>>
|
|
137
|
+
&af_web_dp=<desktop fallback URL>
|
|
122
138
|
```
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
`deep_link_value` is the in-app path AppsFlyer reads after the app is open. `af_dp` is the address Android opens when the https OneLink is not claimed as an App Link. It is the same path on the build's custom scheme (`klarnaoneoff://loyalty-cards-v2/add-to-klarna/...`), because a bare `klarnaoneoff://` launches the app and leaves it on the home screen. The app inserts a third slash before it matches the route.
|
|
141
|
+
|
|
142
|
+
The `<oneLinkBase>` and `af_dp` scheme are fixed per `clientTarget`:
|
|
143
|
+
|
|
144
|
+
| `clientTarget` | OneLink base URL | `af_dp` scheme |
|
|
145
|
+
| -------------- | -------------------------------------------- | ----------------------- |
|
|
146
|
+
| `pink` | `https://l.klarna.com/22XC` | `klarna://` |
|
|
147
|
+
| `internalpink` | `https://klarnainternalpink.onelink.me/lXgD` | `klarnainternalpink://` |
|
|
148
|
+
| `yellow` | `https://klarnayellow.onelink.me/JQ8X` | `klarnayellow://` |
|
|
149
|
+
| `staging` | `https://klarnastaging.onelink.me/hV1K` | `klarnastaging://` |
|
|
150
|
+
| `oneoff` | `https://klarnaoneoff.onelink.me/FaEr` | `klarnaoneoff://` |
|
|
151
|
+
| `local` | `https://klarnalocal.onelink.me/dxUs` | `klarnadev://` |
|
|
126
152
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
The library sets `af_dp` (the screen on the app's custom scheme) and `af_web_dp` (the desktop fallback) on the URL itself.
|
|
154
|
+
The App Store / Play Store fallbacks for mobile are configured on the
|
|
155
|
+
AppsFlyer OneLink template, not on the URL. Desktop fallback is always
|
|
156
|
+
`https://klarna.com/add-to-klarna` — the same production page for every
|
|
157
|
+
`environment` and `clientTarget`. Merchants don't (and can't) configure it.
|
|
158
|
+
This keeps the merchant API surface identical to v1: just `brandNickname`
|
|
159
|
+
and `inputId`.
|
|
131
160
|
|
|
132
|
-
The `<
|
|
133
|
-
extra base64url so the entire ciphertext fits in a single URL path component.
|
|
161
|
+
The `<encryptedPayload>` inside `deep_link_value` is the JWE compact serialization wrapped in an extra base64url so the entire ciphertext fits in a single deep-link path component (the dots in the JWE compact form would otherwise break the Klarna app's in-app router).
|
|
134
162
|
|
|
135
163
|
## API
|
|
136
164
|
|
|
@@ -141,7 +169,7 @@ first network call happens on the first `buildLink` / `redirect`.
|
|
|
141
169
|
|
|
142
170
|
### `client.buildLink({ brandNickname, inputId })`
|
|
143
171
|
|
|
144
|
-
Returns `Promise<string>` — the fully-formed
|
|
172
|
+
Returns `Promise<string>` — the fully-formed AppsFlyer OneLink URL.
|
|
145
173
|
|
|
146
174
|
A fresh `linkId` is minted on every call (and embedded inside the encrypted
|
|
147
175
|
payload, not exposed on the surface). The Klarna backend enforces single-use
|
|
@@ -211,7 +239,7 @@ on the code, not on the message.
|
|
|
211
239
|
|
|
212
240
|
| Code | Meaning |
|
|
213
241
|
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
214
|
-
| `INVALID_CONFIG` | A client option was invalid (e.g. unknown `environment` or `region`).
|
|
242
|
+
| `INVALID_CONFIG` | A client option was invalid (e.g. unknown `environment`, `clientTarget` or `region`). |
|
|
215
243
|
| `INVALID_INPUT` | Missing / malformed `brandNickname` or `inputId`. |
|
|
216
244
|
| `JWKS_FETCH_FAILED` | Could not reach the JWKS endpoint, or the endpoint returned a non-2xx HTTP status. |
|
|
217
245
|
| `JWKS_INVALID` | The JWKS body was not valid JSON, or did not contain a `keys` array. |
|
package/dist/index.cjs
CHANGED
|
@@ -29,8 +29,8 @@ function toBase64Url(input) {
|
|
|
29
29
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// src/
|
|
33
|
-
function
|
|
32
|
+
// src/buildLinkPath.ts
|
|
33
|
+
function buildLinkPath(linkPath, brandNickname, jweCompact) {
|
|
34
34
|
if (typeof brandNickname !== "string" || brandNickname.length === 0) {
|
|
35
35
|
throw new AddToKlarnaError("INVALID_INPUT", "`brandNickname` must be a non-empty string.");
|
|
36
36
|
}
|
|
@@ -44,7 +44,7 @@ function buildLinkUrl(baseUrl, brandNickname, jweCompact) {
|
|
|
44
44
|
throw new AddToKlarnaError("ENCRYPTION_FAILED", "Empty JWE returned from encryptor.");
|
|
45
45
|
}
|
|
46
46
|
const encryptedPayload = toBase64Url(jweCompact);
|
|
47
|
-
return `${
|
|
47
|
+
return `${linkPath}/${encodeURIComponent(brandNickname)}/${encryptedPayload}`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// src/buildPayload.ts
|
|
@@ -64,12 +64,35 @@ var JWKS_URLS = {
|
|
|
64
64
|
production: "https://app.klarna.com/.well-known/jwks.json",
|
|
65
65
|
staging: "https://eu-non-production-klarna-static-assets.s3.eu-west-1.amazonaws.com/klapp/.well-known/jwks.json"
|
|
66
66
|
};
|
|
67
|
-
var
|
|
68
|
-
production: "
|
|
69
|
-
staging: "
|
|
67
|
+
var LINK_PATHS = {
|
|
68
|
+
production: "/loyalty-cards-v2/add-to-klarna",
|
|
69
|
+
staging: "/loyalty-cards-v2/add-to-klarna"
|
|
70
70
|
};
|
|
71
|
+
var ONELINK_BASE_URLS = {
|
|
72
|
+
pink: "https://l.klarna.com/22XC",
|
|
73
|
+
internalpink: "https://klarnainternalpink.onelink.me/lXgD",
|
|
74
|
+
yellow: "https://klarnayellow.onelink.me/JQ8X",
|
|
75
|
+
staging: "https://klarnastaging.onelink.me/hV1K",
|
|
76
|
+
oneoff: "https://klarnaoneoff.onelink.me/FaEr",
|
|
77
|
+
local: "https://klarnalocal.onelink.me/dxUs"
|
|
78
|
+
};
|
|
79
|
+
var APP_URI_SCHEMES = {
|
|
80
|
+
pink: "klarna://",
|
|
81
|
+
internalpink: "klarnainternalpink://",
|
|
82
|
+
yellow: "klarnayellow://",
|
|
83
|
+
staging: "klarnastaging://",
|
|
84
|
+
oneoff: "klarnaoneoff://",
|
|
85
|
+
local: "klarnadev://"
|
|
86
|
+
};
|
|
87
|
+
var DESKTOP_FALLBACK_URL = "https://klarna.com/add-to-klarna";
|
|
88
|
+
var MEDIA_SOURCE = "WebApp";
|
|
89
|
+
var CAMPAIGN_NAME = "add-to-klarna";
|
|
71
90
|
function resolveConfig(options) {
|
|
72
|
-
const {
|
|
91
|
+
const {
|
|
92
|
+
environment = "production",
|
|
93
|
+
clientTarget = "pink",
|
|
94
|
+
region
|
|
95
|
+
} = options ?? {};
|
|
73
96
|
if (environment !== "production" && environment !== "staging") {
|
|
74
97
|
throw new AddToKlarnaError(
|
|
75
98
|
"INVALID_CONFIG",
|
|
@@ -82,9 +105,20 @@ function resolveConfig(options) {
|
|
|
82
105
|
`Unknown region: ${String(region)}. Expected "eu", "us" or "ap".`
|
|
83
106
|
);
|
|
84
107
|
}
|
|
108
|
+
if (clientTarget !== "pink" && clientTarget !== "internalpink" && clientTarget !== "yellow" && clientTarget !== "staging" && clientTarget !== "oneoff" && clientTarget !== "local") {
|
|
109
|
+
throw new AddToKlarnaError(
|
|
110
|
+
"INVALID_CONFIG",
|
|
111
|
+
`Unknown clientTarget: ${String(clientTarget)}. Expected "pink", "internalpink", "yellow", "staging", "oneoff" or "local"`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
85
114
|
return {
|
|
86
115
|
jwksUrl: JWKS_URLS[environment],
|
|
87
|
-
|
|
116
|
+
linkPath: LINK_PATHS[environment],
|
|
117
|
+
oneLinkBaseUrl: ONELINK_BASE_URLS[clientTarget],
|
|
118
|
+
appUriScheme: APP_URI_SCHEMES[clientTarget],
|
|
119
|
+
desktopFallbackUrl: DESKTOP_FALLBACK_URL,
|
|
120
|
+
mediaSource: MEDIA_SOURCE,
|
|
121
|
+
campaignName: CAMPAIGN_NAME,
|
|
88
122
|
region
|
|
89
123
|
};
|
|
90
124
|
}
|
|
@@ -170,6 +204,49 @@ function assertJwks(body) {
|
|
|
170
204
|
return { keys };
|
|
171
205
|
}
|
|
172
206
|
|
|
207
|
+
// src/oneLink.ts
|
|
208
|
+
function buildOneLinkUrl(input) {
|
|
209
|
+
requireNonEmptyString("oneLinkBaseUrl", input.oneLinkBaseUrl, "INVALID_CONFIG");
|
|
210
|
+
requireNonEmptyString("pid", input.pid, "INVALID_CONFIG");
|
|
211
|
+
requireNonEmptyString("c", input.c, "INVALID_CONFIG");
|
|
212
|
+
requireNonEmptyString("appUriScheme", input.appUriScheme, "INVALID_CONFIG");
|
|
213
|
+
requireNonEmptyString("deepLinkValue", input.deepLinkValue, "INVALID_INPUT");
|
|
214
|
+
requireHttpUrl("webFallbackUrl", input.webFallbackUrl);
|
|
215
|
+
const params = [
|
|
216
|
+
["pid", input.pid],
|
|
217
|
+
["c", input.c],
|
|
218
|
+
["af_dp", buildAppDeepLink(input.appUriScheme, input.deepLinkValue)],
|
|
219
|
+
["deep_link_value", input.deepLinkValue],
|
|
220
|
+
["af_web_dp", input.webFallbackUrl]
|
|
221
|
+
];
|
|
222
|
+
const query = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
|
|
223
|
+
return `${input.oneLinkBaseUrl}?${query}`;
|
|
224
|
+
}
|
|
225
|
+
function buildAppDeepLink(appUriScheme, deepLinkValue) {
|
|
226
|
+
const path = deepLinkValue.startsWith("/") ? deepLinkValue.slice(1) : deepLinkValue;
|
|
227
|
+
return `${appUriScheme}${path}`;
|
|
228
|
+
}
|
|
229
|
+
function requireNonEmptyString(name, value, code) {
|
|
230
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
231
|
+
throw new AddToKlarnaError(code, `\`${name}\` must be a non-empty string.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function requireHttpUrl(name, value) {
|
|
235
|
+
requireNonEmptyString(name, value, "INVALID_CONFIG");
|
|
236
|
+
let url;
|
|
237
|
+
try {
|
|
238
|
+
url = new URL(value);
|
|
239
|
+
} catch {
|
|
240
|
+
throw new AddToKlarnaError("INVALID_CONFIG", `\`${name}\` is not a valid URL: ${value}`);
|
|
241
|
+
}
|
|
242
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
243
|
+
throw new AddToKlarnaError(
|
|
244
|
+
"INVALID_CONFIG",
|
|
245
|
+
`\`${name}\` must use http or https. Got: ${value}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
173
250
|
// src/client.ts
|
|
174
251
|
function createAddToKlarnaClient(options) {
|
|
175
252
|
const config = resolveConfig(options);
|
|
@@ -184,7 +261,15 @@ function createAddToKlarnaClient(options) {
|
|
|
184
261
|
const jwks = await fetchJwks(config.jwksUrl);
|
|
185
262
|
const key = pickEncryptionKey(jwks, config.region);
|
|
186
263
|
const jwe = await encryptJwe(payload, key);
|
|
187
|
-
|
|
264
|
+
const deepLinkValue = buildLinkPath(config.linkPath, input.brandNickname, jwe);
|
|
265
|
+
return buildOneLinkUrl({
|
|
266
|
+
oneLinkBaseUrl: config.oneLinkBaseUrl,
|
|
267
|
+
pid: config.mediaSource,
|
|
268
|
+
c: config.campaignName,
|
|
269
|
+
appUriScheme: config.appUriScheme,
|
|
270
|
+
deepLinkValue,
|
|
271
|
+
webFallbackUrl: config.desktopFallbackUrl
|
|
272
|
+
});
|
|
188
273
|
}
|
|
189
274
|
async function redirect(input) {
|
|
190
275
|
const url = await buildLink(input);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/utils.ts","../src/buildLinkUrl.ts","../src/buildPayload.ts","../src/config.ts","../src/encryptJwe.ts","../src/jwksClient.ts","../src/client.ts"],"names":["importJWK","EncryptJWT"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Stable error codes. Merchant integrations should branch on these, not on\n * the human-readable message.\n */\nexport type ErrorCode =\n | \"INVALID_INPUT\"\n | \"INVALID_CONFIG\"\n | \"JWKS_FETCH_FAILED\"\n | \"JWKS_INVALID\"\n | \"NO_MATCHING_KEY\"\n | \"ENCRYPTION_FAILED\"\n | \"NAVIGATION_UNAVAILABLE\";\n\n/**\n * The single error type thrown by every public API in this package. Always\n * carries a stable {@link ErrorCode} on `code` and, where relevant, the\n * underlying error on `cause`.\n */\nexport class AddToKlarnaError extends Error {\n public readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"AddToKlarnaError\";\n this.code = code;\n // Restore prototype chain for ES5-targeted bundles. No-op on modern targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport function isAddToKlarnaError(err: unknown): err is AddToKlarnaError {\n return err instanceof AddToKlarnaError;\n}\n","/**\n * Encode a UTF-8 string as base64url, without padding. Works in both Node\n * and browsers.\n *\n * Used to wrap the JWE compact serialization into a single URL path segment\n * (without the wrap, the dots inside the JWE break path matching on Klarna's\n * web router).\n */\nexport function toBase64Url(input: string): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(input, \"utf8\").toString(\"base64url\");\n }\n const bytes = new TextEncoder().encode(input);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Decode a base64url string back to UTF-8. Inverse of {@link toBase64Url}.\n * Exported mostly so tests can assert on the round-trip.\n */\nexport function fromBase64Url(input: string): string {\n const padded =\n input.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"==\".slice(0, (4 - (input.length % 4)) % 4);\n\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(padded, \"base64\").toString(\"utf8\");\n }\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return new TextDecoder().decode(bytes);\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport { toBase64Url } from \"./utils.js\";\n\n/**\n * Compose the final universal-link URL:\n *\n * <baseUrl>/<brandNickname>/<encryptedSegment>\n *\n * `baseUrl` already includes the `/add-to-klarna` route segment (resolved in\n * {@link resolveConfig}). `encryptedSegment` is the JWE compact serialization\n * wrapped in a single base64url — the wrap is required by the Klarna web\n * router because the dots in the JWE compact form would otherwise break\n * path matching.\n */\nexport function buildLinkUrl(baseUrl: string, brandNickname: string, jweCompact: string): string {\n if (typeof brandNickname !== \"string\" || brandNickname.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`brandNickname` must be a non-empty string.\");\n }\n if (/[\\s/?#]/.test(brandNickname)) {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n `\\`brandNickname\\` contains invalid characters (whitespace, \"/\", \"?\" or \"#\"): ${brandNickname}`,\n );\n }\n if (typeof jweCompact !== \"string\" || jweCompact.length === 0) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", \"Empty JWE returned from encryptor.\");\n }\n const encryptedPayload = toBase64Url(jweCompact);\n return `${baseUrl}/${encodeURIComponent(brandNickname)}/${encryptedPayload}`;\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * Build the plaintext payload that goes inside the JWE.\n *\n * The schema is `{ inputId, linkId, iat }`. Klarna's backend validates this\n * shape on decryption, so the field names and types must remain stable.\n */\nexport function buildPayload(inputId: string): DecryptedPayload {\n if (typeof inputId !== \"string\" || inputId.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`inputId` must be a non-empty string.\");\n }\n return {\n inputId,\n linkId: globalThis.crypto.randomUUID(),\n iat: Math.floor(Date.now() / 1000),\n };\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { ClientOptions, Environment, Region } from \"./types.js\";\n\n/**\n * Resolved configuration used internally. Everything has been collapsed down\n * to concrete strings by the time we construct one.\n */\nexport interface ResolvedConfig {\n jwksUrl: string;\n linkBaseUrl: string;\n region: Region;\n}\n\n/**\n * JWKS endpoints per environment. **Independent of region.** Every published\n * JWKS file contains keys for every region, namespaced by a `kid-{region}-`\n * prefix; the picker selects the right one.\n */\nconst JWKS_URLS: Record<Environment, string> = {\n production: \"https://app.klarna.com/.well-known/jwks.json\",\n staging:\n \"https://eu-non-production-klarna-static-assets.s3.eu-west-1.amazonaws.com/klapp/.well-known/jwks.json\",\n};\n\n/**\n * Base URLs for the universal link. The final URL appended is:\n *\n * <base>/<brandNickname>/<encryptedPayload>\n *\n * Production uses an HTTPS Universal Link. Staging uses the dev custom scheme\n * so taps open the staging app build directly.\n */\nconst LINK_BASE_URLS: Record<Environment, string> = {\n production: \"https://app.klarna.com/loyalty-cards-v2/add-to-klarna\",\n staging: \"klarnadev://loyalty-cards-v2/add-to-klarna\",\n};\n\n/**\n * Resolve user-facing client options into a concrete config.\n *\n * Validates inputs up-front so any misconfiguration surfaces as an\n * `INVALID_CONFIG` `AddToKlarnaError` *before* the merchant page issues any\n * network calls.\n */\nexport function resolveConfig(options: ClientOptions): ResolvedConfig {\n const { environment = \"production\", region } = options ?? ({} as ClientOptions);\n\n if (environment !== \"production\" && environment !== \"staging\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown environment: ${String(environment)}. Expected \"production\" or \"staging\".`,\n );\n }\n if (region !== \"eu\" && region !== \"us\" && region !== \"ap\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown region: ${String(region)}. Expected \"eu\", \"us\" or \"ap\".`,\n );\n }\n\n return {\n jwksUrl: JWKS_URLS[environment],\n linkBaseUrl: LINK_BASE_URLS[environment],\n region,\n };\n}\n","import { EncryptJWT, importJWK, type JWK } from \"jose\";\n\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * The JWE profile this library produces. Pinned end-to-end:\n *\n * - `ALG` (key-encryption algorithm)\n * - `ENC` (content-encryption algorithm)\n *\n * `pickEncryptionKey` enforces the matching `kty`/`crv` on the JWKS side,\n * so a JWKS that drifts off this profile fails closed with `NO_MATCHING_KEY`\n * instead of silently producing a non-decryptable link. Changing either\n * constant is an SDK-coordinated event with Klarna's backend.\n */\nexport const ALG = \"ECDH-ES+A256KW\";\nexport const ENC = \"A256GCM\";\n\n/**\n * Encrypt the plaintext payload as a JWE using the given JWK. Produces the\n * compact serialization (five dot-separated base64url segments).\n */\nexport async function encryptJwe(\n payload: DecryptedPayload,\n key: JWK & { kid: string },\n): Promise<string> {\n let imported: Awaited<ReturnType<typeof importJWK>>;\n try {\n imported = await importJWK(key, ALG);\n } catch (cause) {\n throw new AddToKlarnaError(\n \"ENCRYPTION_FAILED\",\n `Could not import JWK with kid=\"${key.kid}\" for alg=\"${ALG}\".`,\n { cause },\n );\n }\n\n try {\n return await new EncryptJWT({\n inputId: payload.inputId,\n linkId: payload.linkId,\n })\n .setProtectedHeader({\n alg: ALG,\n enc: ENC,\n kid: key.kid,\n typ: \"JWT\",\n cty: \"application/json\",\n })\n .setIssuedAt(payload.iat)\n .encrypt(imported);\n } catch (cause) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", `JWE encryption failed for kid=\"${key.kid}\".`, {\n cause,\n });\n }\n}\n","import type { JWK } from \"jose\";\n\nimport { ALG } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { Region } from \"./types.js\";\n\n/**\n * The minimal JWKS shape we depend on. The actual file may contain extra\n * fields per RFC 7517; we ignore everything we do not need.\n */\nexport interface Jwks {\n keys: Array<JWK & { kid?: string; alg?: string; use?: string }>;\n}\n\n/**\n * The single key shape this library accepts off the JWKS. Pinned to the\n * tuple Klarna's backend publishes today:\n *\n * alg = ECDH-ES+A256KW\n * kty = EC\n * crv = P-256\n *\n * Anything else is rejected with `NO_MATCHING_KEY`, so a JWKS that ever\n * drifts off this profile — whether by mistake or because someone\n * managed to substitute it past TLS — fails closed instead of\n * silently using a weaker or symmetric algorithm. Any change to this\n * tuple is an SDK-coordinated event with the backend.\n */\nconst REQUIRED_KTY = \"EC\";\nconst REQUIRED_CRV = \"P-256\";\n\n/**\n * Fetch and parse the JWKS at `url`. There is no in-memory cache — caching is\n * delegated to the HTTP layer and Klarna's CDN. Wraps every failure mode in\n * a typed `AddToKlarnaError`.\n */\nexport async function fetchJwks(url: string): Promise<Jwks> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_FETCH_FAILED\", `Failed to fetch JWKS from ${url}`, { cause });\n }\n\n if (!response.ok) {\n throw new AddToKlarnaError(\n \"JWKS_FETCH_FAILED\",\n `JWKS fetch returned HTTP ${response.status} ${response.statusText} for ${url}`,\n );\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response body was not valid JSON.\", { cause });\n }\n\n return assertJwks(body);\n}\n\n/**\n * Pick the encryption key for `region` from a JWKS document.\n *\n * A key is usable iff it declares all of:\n *\n * - `use === \"enc\"`\n * - `kid` starting with `kid-{region}-`\n * - `alg === ALG`, `kty === \"EC\"`, `crv === \"P-256\"` (see file header)\n *\n * If multiple keys match, the first one in document order wins — the JWKS\n * is published with the most recent key first. Throws `NO_MATCHING_KEY`\n * when nothing matches.\n */\nexport function pickEncryptionKey(jwks: Jwks, region: Region): JWK & { kid: string } {\n const prefix = `kid-${region}-`;\n const chosen = jwks.keys.find(\n (k) =>\n k.use === \"enc\" &&\n typeof k.kid === \"string\" &&\n k.kid.startsWith(prefix) &&\n k.alg === ALG &&\n k.kty === REQUIRED_KTY &&\n k.crv === REQUIRED_CRV,\n );\n\n if (!chosen || typeof chosen.kid !== \"string\") {\n throw new AddToKlarnaError(\n \"NO_MATCHING_KEY\",\n `JWKS contains no usable encryption key with kid prefix \"${prefix}\" ` +\n `(need use=\"enc\", alg=\"${ALG}\", kty=\"${REQUIRED_KTY}\", crv=\"${REQUIRED_CRV}\").`,\n );\n }\n return chosen as JWK & { kid: string };\n}\n\nfunction assertJwks(body: unknown): Jwks {\n if (!body || typeof body !== \"object\" || !(\"keys\" in body)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response is missing a `keys` array.\");\n }\n const keys = (body as { keys: unknown }).keys;\n if (!Array.isArray(keys)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS `keys` is not an array.\");\n }\n return { keys: keys as Jwks[\"keys\"] };\n}\n","import { buildLinkUrl } from \"./buildLinkUrl.js\";\nimport { buildPayload } from \"./buildPayload.js\";\nimport { resolveConfig } from \"./config.js\";\nimport { encryptJwe } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport { fetchJwks, pickEncryptionKey } from \"./jwksClient.js\";\nimport type { AddToKlarnaClient, BuildLinkInput, ClientOptions } from \"./types.js\";\n\n/**\n * Create an Add-to-Klarna client.\n *\n * Construction is cheap and synchronous: it validates options and wires up\n * the pipeline. The first network call happens on the first\n * {@link AddToKlarnaClient.buildLink} / {@link AddToKlarnaClient.redirect}.\n * Every `buildLink` re-fetches the JWKS — caching is handled by the HTTP\n * layer and Klarna's CDN, not in memory.\n */\nexport function createAddToKlarnaClient(options: ClientOptions): AddToKlarnaClient {\n const config = resolveConfig(options);\n\n async function buildLink(input: BuildLinkInput): Promise<string> {\n if (!input || typeof input !== \"object\") {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n \"buildLink() requires an object with `brandNickname` and `inputId`.\",\n );\n }\n const payload = buildPayload(input.inputId);\n const jwks = await fetchJwks(config.jwksUrl);\n const key = pickEncryptionKey(jwks, config.region);\n const jwe = await encryptJwe(payload, key);\n return buildLinkUrl(config.linkBaseUrl, input.brandNickname, jwe);\n }\n\n async function redirect(input: BuildLinkInput): Promise<void> {\n const url = await buildLink(input);\n const w = (globalThis as { window?: { location?: { assign?: (u: string) => void } } }).window;\n const assign = w?.location?.assign;\n if (typeof assign !== \"function\") {\n throw new AddToKlarnaError(\n \"NAVIGATION_UNAVAILABLE\",\n \"redirect() requires a browser context with `window.location.assign`. Use buildLink() and navigate yourself in non-browser environments.\",\n );\n }\n assign.call(w!.location, url);\n }\n\n return { buildLink, redirect };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/utils.ts","../src/buildLinkPath.ts","../src/buildPayload.ts","../src/config.ts","../src/encryptJwe.ts","../src/jwksClient.ts","../src/oneLink.ts","../src/client.ts"],"names":["importJWK","EncryptJWT"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Stable error codes. Merchant integrations should branch on these, not on\n * the human-readable message.\n */\nexport type ErrorCode =\n | \"INVALID_INPUT\"\n | \"INVALID_CONFIG\"\n | \"JWKS_FETCH_FAILED\"\n | \"JWKS_INVALID\"\n | \"NO_MATCHING_KEY\"\n | \"ENCRYPTION_FAILED\"\n | \"NAVIGATION_UNAVAILABLE\";\n\n/**\n * The single error type thrown by every public API in this package. Always\n * carries a stable {@link ErrorCode} on `code` and, where relevant, the\n * underlying error on `cause`.\n */\nexport class AddToKlarnaError extends Error {\n public readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"AddToKlarnaError\";\n this.code = code;\n // Restore prototype chain for ES5-targeted bundles. No-op on modern targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport function isAddToKlarnaError(err: unknown): err is AddToKlarnaError {\n return err instanceof AddToKlarnaError;\n}\n","/**\n * Encode a UTF-8 string as base64url, without padding. Works in both Node\n * and browsers.\n *\n * Used to wrap the JWE compact serialization into a single URL path segment\n * (without the wrap, the dots inside the JWE break path matching on Klarna's\n * web router).\n */\nexport function toBase64Url(input: string): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(input, \"utf8\").toString(\"base64url\");\n }\n const bytes = new TextEncoder().encode(input);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Decode a base64url string back to UTF-8. Inverse of {@link toBase64Url}.\n * Exported mostly so tests can assert on the round-trip.\n */\nexport function fromBase64Url(input: string): string {\n const padded =\n input.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"==\".slice(0, (4 - (input.length % 4)) % 4);\n\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(padded, \"base64\").toString(\"utf8\");\n }\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return new TextDecoder().decode(bytes);\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport { toBase64Url } from \"./utils.js\";\n\n/**\n * Compose the deep-link *path* that goes inside the outer AppsFlyer OneLink's\n * `deep_link_value` parameter:\n *\n * <linkPath>/<brandNickname>/<encryptedSegment>\n *\n * `linkPath` is a rooted path like `/loyalty-cards-v2/add-to-klarna` (see\n * {@link resolveConfig}). `encryptedSegment` is the JWE compact serialization\n * wrapped in a single base64url — the wrap is required because the dots in the\n * JWE compact form would otherwise break Klarna's in-app router path matching.\n *\n * Returns a path (not a full URL): the composed value is meant to be passed\n * verbatim to {@link buildOneLinkUrl} as `deepLinkValue`, which handles the\n * outer URL encoding.\n */\nexport function buildLinkPath(linkPath: string, brandNickname: string, jweCompact: string): string {\n if (typeof brandNickname !== \"string\" || brandNickname.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`brandNickname` must be a non-empty string.\");\n }\n if (/[\\s/?#]/.test(brandNickname)) {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n `\\`brandNickname\\` contains invalid characters (whitespace, \"/\", \"?\" or \"#\"): ${brandNickname}`,\n );\n }\n if (typeof jweCompact !== \"string\" || jweCompact.length === 0) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", \"Empty JWE returned from encryptor.\");\n }\n const encryptedPayload = toBase64Url(jweCompact);\n return `${linkPath}/${encodeURIComponent(brandNickname)}/${encryptedPayload}`;\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * Build the plaintext payload that goes inside the JWE.\n *\n * The schema is `{ inputId, linkId, iat }`. Klarna's backend validates this\n * shape on decryption, so the field names and types must remain stable.\n */\nexport function buildPayload(inputId: string): DecryptedPayload {\n if (typeof inputId !== \"string\" || inputId.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`inputId` must be a non-empty string.\");\n }\n return {\n inputId,\n linkId: globalThis.crypto.randomUUID(),\n iat: Math.floor(Date.now() / 1000),\n };\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { ClientOptions, ClientTarget, Environment, Region } from \"./types.js\";\n\n/**\n * Resolved configuration used internally. Everything has been collapsed down\n * to concrete strings by the time we construct one.\n */\nexport interface ResolvedConfig {\n jwksUrl: string;\n /**\n * The in-app deep-link path prefix. Combined with the brand nickname and\n * encrypted payload by {@link buildLinkPath}, then passed as\n * `deep_link_value` on the outer AppsFlyer OneLink.\n */\n linkPath: string;\n /**\n * AppsFlyer OneLink prefix that wraps the encrypted deep link. See\n * {@link buildOneLinkUrl}.\n */\n oneLinkBaseUrl: string;\n /**\n * Custom URI scheme for the targeted Klarna app, including `://`.\n * {@link buildOneLinkUrl} appends the in-app path and sends the result as\n * `af_dp`. Selected by `clientTarget`, not by `environment`.\n */\n appUriScheme: string;\n /**\n * Desktop fallback URL (`af_web_dp`). Always the production add-to-klarna\n * page — not selected by `environment` or `clientTarget`.\n */\n desktopFallbackUrl: string;\n /** AppsFlyer media source (`pid`). */\n mediaSource: string;\n /** AppsFlyer campaign name (`c`). */\n campaignName: string;\n region: Region;\n}\n\n/**\n * JWKS endpoints per environment. **Independent of region.** Every published\n * JWKS file contains keys for every region, namespaced by a `kid-{region}-`\n * prefix; the picker selects the right one.\n */\nconst JWKS_URLS: Record<Environment, string> = {\n production: \"https://app.klarna.com/.well-known/jwks.json\",\n staging:\n \"https://eu-non-production-klarna-static-assets.s3.eu-west-1.amazonaws.com/klapp/.well-known/jwks.json\",\n};\n\n/**\n * In-app deep-link path prefixes. Combined with `<brandNickname>/<encrypted>`\n * to form the value of `deep_link_value` on the outer AppsFlyer OneLink. The\n * Klarna app parses this path and routes to the add-to-klarna screen.\n */\nconst LINK_PATHS: Record<Environment, string> = {\n production: \"/loyalty-cards-v2/add-to-klarna\",\n staging: \"/loyalty-cards-v2/add-to-klarna\",\n};\n\n/**\n * AppsFlyer OneLink prefixes. Production uses Klarna's custom `l.klarna.com`\n * short domain; staging uses the standard AppsFlyer `*.onelink.me` host that\n * targets the staging app build.\n */\nconst ONELINK_BASE_URLS: Record<ClientTarget, string> = {\n pink: \"https://l.klarna.com/22XC\",\n internalpink: \"https://klarnainternalpink.onelink.me/lXgD\",\n yellow: \"https://klarnayellow.onelink.me/JQ8X\",\n staging: \"https://klarnastaging.onelink.me/hV1K\",\n oneoff: \"https://klarnaoneoff.onelink.me/FaEr\",\n local: \"https://klarnalocal.onelink.me/dxUs\",\n};\n\n/**\n * Custom URI schemes, one per `clientTarget`. These match the schemes the\n * Klarna app registers (`klarna` plus the build suffix). {@link buildOneLinkUrl}\n * appends the in-app path before sending the result as `af_dp`.\n */\nconst APP_URI_SCHEMES: Record<ClientTarget, string> = {\n pink: \"klarna://\",\n internalpink: \"klarnainternalpink://\",\n yellow: \"klarnayellow://\",\n staging: \"klarnastaging://\",\n oneoff: \"klarnaoneoff://\",\n local: \"klarnadev://\",\n};\n\n/**\n * Desktop fallback (`af_web_dp`). One production page for every environment\n * and client target — there is no staging-specific fallback.\n */\nconst DESKTOP_FALLBACK_URL = \"https://klarna.com/add-to-klarna\";\n\n/**\n * AppsFlyer `pid` (media source).\n */\nconst MEDIA_SOURCE = \"WebApp\";\n\n/**\n * AppsFlyer `c` (campaign name).\n */\nconst CAMPAIGN_NAME = \"add-to-klarna\";\n\n/**\n * Resolve user-facing client options into a concrete config.\n *\n * Validates inputs up-front so any misconfiguration surfaces as an\n * `INVALID_CONFIG` `AddToKlarnaError` *before* the merchant page issues any\n * network calls.\n */\nexport function resolveConfig(options: ClientOptions): ResolvedConfig {\n const {\n environment = \"production\",\n clientTarget = \"pink\",\n region,\n } = options ?? ({} as ClientOptions);\n\n if (environment !== \"production\" && environment !== \"staging\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown environment: ${String(environment)}. Expected \"production\" or \"staging\".`,\n );\n }\n if (region !== \"eu\" && region !== \"us\" && region !== \"ap\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown region: ${String(region)}. Expected \"eu\", \"us\" or \"ap\".`,\n );\n }\n if (\n clientTarget !== \"pink\" &&\n clientTarget !== \"internalpink\" &&\n clientTarget !== \"yellow\" &&\n clientTarget !== \"staging\" &&\n clientTarget !== \"oneoff\" &&\n clientTarget !== \"local\"\n ) {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown clientTarget: ${String(clientTarget)}. Expected \"pink\", \"internalpink\", \"yellow\", \"staging\", \"oneoff\" or \"local\"`,\n );\n }\n\n return {\n jwksUrl: JWKS_URLS[environment],\n linkPath: LINK_PATHS[environment],\n oneLinkBaseUrl: ONELINK_BASE_URLS[clientTarget],\n appUriScheme: APP_URI_SCHEMES[clientTarget],\n desktopFallbackUrl: DESKTOP_FALLBACK_URL,\n mediaSource: MEDIA_SOURCE,\n campaignName: CAMPAIGN_NAME,\n region,\n };\n}\n","import { EncryptJWT, importJWK, type JWK } from \"jose\";\n\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * The JWE profile this library produces. Pinned end-to-end:\n *\n * - `ALG` (key-encryption algorithm)\n * - `ENC` (content-encryption algorithm)\n *\n * `pickEncryptionKey` enforces the matching `kty`/`crv` on the JWKS side,\n * so a JWKS that drifts off this profile fails closed with `NO_MATCHING_KEY`\n * instead of silently producing a non-decryptable link. Changing either\n * constant is an SDK-coordinated event with Klarna's backend.\n */\nexport const ALG = \"ECDH-ES+A256KW\";\nexport const ENC = \"A256GCM\";\n\n/**\n * Encrypt the plaintext payload as a JWE using the given JWK. Produces the\n * compact serialization (five dot-separated base64url segments).\n */\nexport async function encryptJwe(\n payload: DecryptedPayload,\n key: JWK & { kid: string },\n): Promise<string> {\n let imported: Awaited<ReturnType<typeof importJWK>>;\n try {\n imported = await importJWK(key, ALG);\n } catch (cause) {\n throw new AddToKlarnaError(\n \"ENCRYPTION_FAILED\",\n `Could not import JWK with kid=\"${key.kid}\" for alg=\"${ALG}\".`,\n { cause },\n );\n }\n\n try {\n return await new EncryptJWT({\n inputId: payload.inputId,\n linkId: payload.linkId,\n })\n .setProtectedHeader({\n alg: ALG,\n enc: ENC,\n kid: key.kid,\n typ: \"JWT\",\n cty: \"application/json\",\n })\n .setIssuedAt(payload.iat)\n .encrypt(imported);\n } catch (cause) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", `JWE encryption failed for kid=\"${key.kid}\".`, {\n cause,\n });\n }\n}\n","import type { JWK } from \"jose\";\n\nimport { ALG } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { Region } from \"./types.js\";\n\n/**\n * The minimal JWKS shape we depend on. The actual file may contain extra\n * fields per RFC 7517; we ignore everything we do not need.\n */\nexport interface Jwks {\n keys: Array<JWK & { kid?: string; alg?: string; use?: string }>;\n}\n\n/**\n * The single key shape this library accepts off the JWKS. Pinned to the\n * tuple Klarna's backend publishes today:\n *\n * alg = ECDH-ES+A256KW\n * kty = EC\n * crv = P-256\n *\n * Anything else is rejected with `NO_MATCHING_KEY`, so a JWKS that ever\n * drifts off this profile — whether by mistake or because someone\n * managed to substitute it past TLS — fails closed instead of\n * silently using a weaker or symmetric algorithm. Any change to this\n * tuple is an SDK-coordinated event with the backend.\n */\nconst REQUIRED_KTY = \"EC\";\nconst REQUIRED_CRV = \"P-256\";\n\n/**\n * Fetch and parse the JWKS at `url`. There is no in-memory cache — caching is\n * delegated to the HTTP layer and Klarna's CDN. Wraps every failure mode in\n * a typed `AddToKlarnaError`.\n */\nexport async function fetchJwks(url: string): Promise<Jwks> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_FETCH_FAILED\", `Failed to fetch JWKS from ${url}`, { cause });\n }\n\n if (!response.ok) {\n throw new AddToKlarnaError(\n \"JWKS_FETCH_FAILED\",\n `JWKS fetch returned HTTP ${response.status} ${response.statusText} for ${url}`,\n );\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response body was not valid JSON.\", { cause });\n }\n\n return assertJwks(body);\n}\n\n/**\n * Pick the encryption key for `region` from a JWKS document.\n *\n * A key is usable iff it declares all of:\n *\n * - `use === \"enc\"`\n * - `kid` starting with `kid-{region}-`\n * - `alg === ALG`, `kty === \"EC\"`, `crv === \"P-256\"` (see file header)\n *\n * If multiple keys match, the first one in document order wins — the JWKS\n * is published with the most recent key first. Throws `NO_MATCHING_KEY`\n * when nothing matches.\n */\nexport function pickEncryptionKey(jwks: Jwks, region: Region): JWK & { kid: string } {\n const prefix = `kid-${region}-`;\n const chosen = jwks.keys.find(\n (k) =>\n k.use === \"enc\" &&\n typeof k.kid === \"string\" &&\n k.kid.startsWith(prefix) &&\n k.alg === ALG &&\n k.kty === REQUIRED_KTY &&\n k.crv === REQUIRED_CRV,\n );\n\n if (!chosen || typeof chosen.kid !== \"string\") {\n throw new AddToKlarnaError(\n \"NO_MATCHING_KEY\",\n `JWKS contains no usable encryption key with kid prefix \"${prefix}\" ` +\n `(need use=\"enc\", alg=\"${ALG}\", kty=\"${REQUIRED_KTY}\", crv=\"${REQUIRED_CRV}\").`,\n );\n }\n return chosen as JWK & { kid: string };\n}\n\nfunction assertJwks(body: unknown): Jwks {\n if (!body || typeof body !== \"object\" || !(\"keys\" in body)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response is missing a `keys` array.\");\n }\n const keys = (body as { keys: unknown }).keys;\n if (!Array.isArray(keys)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS `keys` is not an array.\");\n }\n return { keys: keys as Jwks[\"keys\"] };\n}\n","import { AddToKlarnaError } from \"./errors.js\";\n\n/**\n * Inputs for {@link buildOneLinkUrl}.\n *\n * All fields are required — this function is deliberately dumb: it takes\n * concrete URLs and composes them into an AppsFlyer OneLink. Environment\n * defaults live in {@link resolveConfig}.\n */\nexport interface BuildOneLinkInput {\n /** The AppsFlyer OneLink prefix (e.g. `https://l.klarna.com/22XC`). */\n oneLinkBaseUrl: string;\n\n /** AppsFlyer media source. Sent as `pid`. */\n pid: string;\n\n /** AppsFlyer campaign name. Sent as `c`. */\n c: string;\n\n /**\n * The in-app deep link path. Sent as `deep_link_value`. AppsFlyer hands\n * this string to the app for in-app routing once the app is open, and\n * replays it after install when the app was not installed (deferred deep\n * linking). It does not launch the app.\n */\n deepLinkValue: string;\n\n /**\n * Custom URI scheme for the targeted Klarna build, including `://`\n * (for example `klarna://` or `klarnaoneoff://`). Combined with\n * `deepLinkValue` and sent as `af_dp`, so the address that launches the\n * app is also the screen.\n */\n appUriScheme: string;\n\n /** Desktop fallback URL. Sent as `af_web_dp`. */\n webFallbackUrl: string;\n}\n\n/**\n * Compose an AppsFlyer OneLink URL that:\n *\n * - launches the installed Klarna app directly to `deepLinkValue` via `af_dp`,\n * - falls back to the App Store / Play Store (configured on the OneLink\n * template, not on this URL) when the app is not installed on mobile,\n * - falls back to `webFallbackUrl` on desktop.\n *\n * The returned URL has the shape:\n *\n * <oneLinkBaseUrl>?pid=<pid>&c=<c>&af_dp=<url-encoded scheme + deepLinkValue>\n * &deep_link_value=<url-encoded deepLinkValue>\n * &af_web_dp=<url-encoded webFallbackUrl>\n *\n * `af_dp` uses two slashes (`klarnaoneoff://loyalty-cards-v2/...`). The Klarna\n * app inserts the third slash before matching the path. A bare scheme opens\n * the app and leaves it on the home screen, because that launch address has\n * no route.\n *\n * Throws `AddToKlarnaError` with `INVALID_CONFIG` if any URL or the app URI\n * scheme is missing or malformed, or `INVALID_INPUT` if `deepLinkValue` is\n * empty.\n */\nexport function buildOneLinkUrl(input: BuildOneLinkInput): string {\n requireNonEmptyString(\"oneLinkBaseUrl\", input.oneLinkBaseUrl, \"INVALID_CONFIG\");\n requireNonEmptyString(\"pid\", input.pid, \"INVALID_CONFIG\");\n requireNonEmptyString(\"c\", input.c, \"INVALID_CONFIG\");\n requireNonEmptyString(\"appUriScheme\", input.appUriScheme, \"INVALID_CONFIG\");\n requireNonEmptyString(\"deepLinkValue\", input.deepLinkValue, \"INVALID_INPUT\");\n requireHttpUrl(\"webFallbackUrl\", input.webFallbackUrl);\n\n // Insertion order is preserved by URLSearchParams / Object.entries, and it\n // matters here only for output stability (easier to snapshot in tests).\n const params: Array<[string, string]> = [\n [\"pid\", input.pid],\n [\"c\", input.c],\n [\"af_dp\", buildAppDeepLink(input.appUriScheme, input.deepLinkValue)],\n [\"deep_link_value\", input.deepLinkValue],\n [\"af_web_dp\", input.webFallbackUrl],\n ];\n\n const query = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join(\"&\");\n\n return `${input.oneLinkBaseUrl}?${query}`;\n}\n\n/**\n * `scheme://` + path, without a leading slash on the path, so the result is\n * `klarnaoneoff://loyalty-cards-v2/add-to-klarna/...` rather than three slashes.\n */\nfunction buildAppDeepLink(appUriScheme: string, deepLinkValue: string): string {\n const path = deepLinkValue.startsWith(\"/\") ? deepLinkValue.slice(1) : deepLinkValue;\n return `${appUriScheme}${path}`;\n}\n\nfunction requireNonEmptyString(\n name: string,\n value: unknown,\n code: \"INVALID_CONFIG\" | \"INVALID_INPUT\",\n): void {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new AddToKlarnaError(code, `\\`${name}\\` must be a non-empty string.`);\n }\n}\n\nfunction requireHttpUrl(name: string, value: unknown): void {\n requireNonEmptyString(name, value, \"INVALID_CONFIG\");\n let url: URL;\n try {\n url = new URL(value as string);\n } catch {\n throw new AddToKlarnaError(\"INVALID_CONFIG\", `\\`${name}\\` is not a valid URL: ${value}`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `\\`${name}\\` must use http or https. Got: ${value}`,\n );\n }\n}\n","import { buildLinkPath } from \"./buildLinkPath.js\";\nimport { buildPayload } from \"./buildPayload.js\";\nimport { resolveConfig } from \"./config.js\";\nimport { encryptJwe } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport { fetchJwks, pickEncryptionKey } from \"./jwksClient.js\";\nimport { buildOneLinkUrl } from \"./oneLink.js\";\nimport type { AddToKlarnaClient, BuildLinkInput, ClientOptions } from \"./types.js\";\n\n/**\n * Create an Add-to-Klarna client.\n *\n * Construction is cheap and synchronous: it validates options and wires up\n * the pipeline. The first network call happens on the first\n * {@link AddToKlarnaClient.buildLink} / {@link AddToKlarnaClient.redirect}.\n *\n * Each `buildLink` returns an AppsFlyer OneLink that:\n *\n * - opens the Klarna app to the encrypted add-to-klarna deep link when the\n * app is installed,\n * - falls back to the App Store / Play Store on mobile when the app is not\n * installed (deferred deep link replays after install),\n * - falls back to `https://klarna.com/add-to-klarna` on desktop.\n *\n * Every `buildLink` re-fetches the JWKS — caching is handled by the HTTP\n * layer and Klarna's CDN, not in memory.\n */\nexport function createAddToKlarnaClient(options: ClientOptions): AddToKlarnaClient {\n const config = resolveConfig(options);\n\n async function buildLink(input: BuildLinkInput): Promise<string> {\n if (!input || typeof input !== \"object\") {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n \"buildLink() requires an object with `brandNickname` and `inputId`.\",\n );\n }\n const payload = buildPayload(input.inputId);\n const jwks = await fetchJwks(config.jwksUrl);\n const key = pickEncryptionKey(jwks, config.region);\n const jwe = await encryptJwe(payload, key);\n const deepLinkValue = buildLinkPath(config.linkPath, input.brandNickname, jwe);\n return buildOneLinkUrl({\n oneLinkBaseUrl: config.oneLinkBaseUrl,\n pid: config.mediaSource,\n c: config.campaignName,\n appUriScheme: config.appUriScheme,\n deepLinkValue,\n webFallbackUrl: config.desktopFallbackUrl,\n });\n }\n\n async function redirect(input: BuildLinkInput): Promise<void> {\n const url = await buildLink(input);\n const w = (globalThis as { window?: { location?: { assign?: (u: string) => void } } }).window;\n const assign = w?.location?.assign;\n if (typeof assign !== \"function\") {\n throw new AddToKlarnaError(\n \"NAVIGATION_UNAVAILABLE\",\n \"redirect() requires a browser context with `window.location.assign`. Use buildLink() and navigate yourself in non-browser environments.\",\n );\n }\n assign.call(w!.location, url);\n }\n\n return { buildLink, redirect };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,27 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Klarna deployment region the link should target. Selects which key the
|
|
3
3
|
* library picks out of the JWKS (keys are namespaced via a `kid-{region}-`
|
|
4
|
-
* prefix). Does *not* change the JWKS URL
|
|
5
|
-
*
|
|
4
|
+
* prefix). Does *not* change the JWKS URL ({@link Environment}) or the
|
|
5
|
+
* AppsFlyer OneLink host ({@link ClientTarget}).
|
|
6
6
|
*/
|
|
7
7
|
type Region = "eu" | "us" | "ap";
|
|
8
8
|
/**
|
|
9
|
-
* The Klarna environment to target.
|
|
9
|
+
* The Klarna backend environment to target.
|
|
10
10
|
*
|
|
11
|
-
* - `production` — the production JWKS
|
|
12
|
-
* - `staging` — the non-production JWKS
|
|
13
|
-
*
|
|
11
|
+
* - `production` — the production JWKS.
|
|
12
|
+
* - `staging` — the non-production JWKS. Use this for development and
|
|
13
|
+
* testing only.
|
|
14
|
+
*
|
|
15
|
+
* Does *not* select the AppsFlyer OneLink host — that is {@link ClientTarget}.
|
|
16
|
+
* Desktop fallback (`af_web_dp`) is a single production page for every
|
|
17
|
+
* environment.
|
|
14
18
|
*/
|
|
15
19
|
type Environment = "production" | "staging";
|
|
20
|
+
/**
|
|
21
|
+
* The Klarna client application we are targeting.
|
|
22
|
+
*
|
|
23
|
+
* - `pink` — the production application that is available to all of our users
|
|
24
|
+
* - `internalpink` — the pre-release production application, Klarna internal
|
|
25
|
+
* - `yellow` — internal build targeting the production backend
|
|
26
|
+
* - `staging` — internal build targeting the staging backend
|
|
27
|
+
* - `oneoff` — internal build created from a specific commit, targeting either environment
|
|
28
|
+
* - `local` — targets the local simulator
|
|
29
|
+
*/
|
|
30
|
+
type ClientTarget = "pink" | "internalpink" | "yellow" | "staging" | "oneoff" | "local";
|
|
16
31
|
/**
|
|
17
32
|
* Options accepted by {@link createAddToKlarnaClient}.
|
|
18
33
|
*
|
|
19
|
-
* Deliberately tiny: just
|
|
20
|
-
* to use. The JWKS endpoint
|
|
21
|
-
* `environment
|
|
34
|
+
* Deliberately tiny: just which backend (`environment`), which app
|
|
35
|
+
* (`clientTarget`), and which region's key to use. The JWKS endpoint is
|
|
36
|
+
* derived from `environment`; the AppsFlyer OneLink host is derived from
|
|
37
|
+
* `clientTarget`. Desktop fallback is a single production page for every
|
|
38
|
+
* combination. None of these are otherwise configurable.
|
|
22
39
|
*/
|
|
23
40
|
interface ClientOptions {
|
|
24
41
|
environment?: Environment;
|
|
42
|
+
clientTarget?: ClientTarget;
|
|
25
43
|
region: Region;
|
|
26
44
|
}
|
|
27
45
|
/**
|
|
@@ -45,7 +63,7 @@ interface BuildLinkInput {
|
|
|
45
63
|
*/
|
|
46
64
|
interface AddToKlarnaClient {
|
|
47
65
|
/**
|
|
48
|
-
* Build
|
|
66
|
+
* Build an AppsFlyer OneLink URL without navigating to it.
|
|
49
67
|
*
|
|
50
68
|
* Each call mints a fresh `linkId` and re-fetches the JWKS — no in-memory
|
|
51
69
|
* cache. Caching is delegated to the HTTP layer + Klarna's CDN.
|
|
@@ -74,6 +92,15 @@ interface DecryptedPayload {
|
|
|
74
92
|
* Construction is cheap and synchronous: it validates options and wires up
|
|
75
93
|
* the pipeline. The first network call happens on the first
|
|
76
94
|
* {@link AddToKlarnaClient.buildLink} / {@link AddToKlarnaClient.redirect}.
|
|
95
|
+
*
|
|
96
|
+
* Each `buildLink` returns an AppsFlyer OneLink that:
|
|
97
|
+
*
|
|
98
|
+
* - opens the Klarna app to the encrypted add-to-klarna deep link when the
|
|
99
|
+
* app is installed,
|
|
100
|
+
* - falls back to the App Store / Play Store on mobile when the app is not
|
|
101
|
+
* installed (deferred deep link replays after install),
|
|
102
|
+
* - falls back to `https://klarna.com/add-to-klarna` on desktop.
|
|
103
|
+
*
|
|
77
104
|
* Every `buildLink` re-fetches the JWKS — caching is handled by the HTTP
|
|
78
105
|
* layer and Klarna's CDN, not in memory.
|
|
79
106
|
*/
|
|
@@ -97,4 +124,4 @@ declare class AddToKlarnaError extends Error {
|
|
|
97
124
|
}
|
|
98
125
|
declare function isAddToKlarnaError(err: unknown): err is AddToKlarnaError;
|
|
99
126
|
|
|
100
|
-
export { type AddToKlarnaClient, AddToKlarnaError, type BuildLinkInput, type ClientOptions, type DecryptedPayload, type Environment, type ErrorCode, type Region, createAddToKlarnaClient, isAddToKlarnaError };
|
|
127
|
+
export { type AddToKlarnaClient, AddToKlarnaError, type BuildLinkInput, type ClientOptions, type ClientTarget, type DecryptedPayload, type Environment, type ErrorCode, type Region, createAddToKlarnaClient, isAddToKlarnaError };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Klarna deployment region the link should target. Selects which key the
|
|
3
3
|
* library picks out of the JWKS (keys are namespaced via a `kid-{region}-`
|
|
4
|
-
* prefix). Does *not* change the JWKS URL
|
|
5
|
-
*
|
|
4
|
+
* prefix). Does *not* change the JWKS URL ({@link Environment}) or the
|
|
5
|
+
* AppsFlyer OneLink host ({@link ClientTarget}).
|
|
6
6
|
*/
|
|
7
7
|
type Region = "eu" | "us" | "ap";
|
|
8
8
|
/**
|
|
9
|
-
* The Klarna environment to target.
|
|
9
|
+
* The Klarna backend environment to target.
|
|
10
10
|
*
|
|
11
|
-
* - `production` — the production JWKS
|
|
12
|
-
* - `staging` — the non-production JWKS
|
|
13
|
-
*
|
|
11
|
+
* - `production` — the production JWKS.
|
|
12
|
+
* - `staging` — the non-production JWKS. Use this for development and
|
|
13
|
+
* testing only.
|
|
14
|
+
*
|
|
15
|
+
* Does *not* select the AppsFlyer OneLink host — that is {@link ClientTarget}.
|
|
16
|
+
* Desktop fallback (`af_web_dp`) is a single production page for every
|
|
17
|
+
* environment.
|
|
14
18
|
*/
|
|
15
19
|
type Environment = "production" | "staging";
|
|
20
|
+
/**
|
|
21
|
+
* The Klarna client application we are targeting.
|
|
22
|
+
*
|
|
23
|
+
* - `pink` — the production application that is available to all of our users
|
|
24
|
+
* - `internalpink` — the pre-release production application, Klarna internal
|
|
25
|
+
* - `yellow` — internal build targeting the production backend
|
|
26
|
+
* - `staging` — internal build targeting the staging backend
|
|
27
|
+
* - `oneoff` — internal build created from a specific commit, targeting either environment
|
|
28
|
+
* - `local` — targets the local simulator
|
|
29
|
+
*/
|
|
30
|
+
type ClientTarget = "pink" | "internalpink" | "yellow" | "staging" | "oneoff" | "local";
|
|
16
31
|
/**
|
|
17
32
|
* Options accepted by {@link createAddToKlarnaClient}.
|
|
18
33
|
*
|
|
19
|
-
* Deliberately tiny: just
|
|
20
|
-
* to use. The JWKS endpoint
|
|
21
|
-
* `environment
|
|
34
|
+
* Deliberately tiny: just which backend (`environment`), which app
|
|
35
|
+
* (`clientTarget`), and which region's key to use. The JWKS endpoint is
|
|
36
|
+
* derived from `environment`; the AppsFlyer OneLink host is derived from
|
|
37
|
+
* `clientTarget`. Desktop fallback is a single production page for every
|
|
38
|
+
* combination. None of these are otherwise configurable.
|
|
22
39
|
*/
|
|
23
40
|
interface ClientOptions {
|
|
24
41
|
environment?: Environment;
|
|
42
|
+
clientTarget?: ClientTarget;
|
|
25
43
|
region: Region;
|
|
26
44
|
}
|
|
27
45
|
/**
|
|
@@ -45,7 +63,7 @@ interface BuildLinkInput {
|
|
|
45
63
|
*/
|
|
46
64
|
interface AddToKlarnaClient {
|
|
47
65
|
/**
|
|
48
|
-
* Build
|
|
66
|
+
* Build an AppsFlyer OneLink URL without navigating to it.
|
|
49
67
|
*
|
|
50
68
|
* Each call mints a fresh `linkId` and re-fetches the JWKS — no in-memory
|
|
51
69
|
* cache. Caching is delegated to the HTTP layer + Klarna's CDN.
|
|
@@ -74,6 +92,15 @@ interface DecryptedPayload {
|
|
|
74
92
|
* Construction is cheap and synchronous: it validates options and wires up
|
|
75
93
|
* the pipeline. The first network call happens on the first
|
|
76
94
|
* {@link AddToKlarnaClient.buildLink} / {@link AddToKlarnaClient.redirect}.
|
|
95
|
+
*
|
|
96
|
+
* Each `buildLink` returns an AppsFlyer OneLink that:
|
|
97
|
+
*
|
|
98
|
+
* - opens the Klarna app to the encrypted add-to-klarna deep link when the
|
|
99
|
+
* app is installed,
|
|
100
|
+
* - falls back to the App Store / Play Store on mobile when the app is not
|
|
101
|
+
* installed (deferred deep link replays after install),
|
|
102
|
+
* - falls back to `https://klarna.com/add-to-klarna` on desktop.
|
|
103
|
+
*
|
|
77
104
|
* Every `buildLink` re-fetches the JWKS — caching is handled by the HTTP
|
|
78
105
|
* layer and Klarna's CDN, not in memory.
|
|
79
106
|
*/
|
|
@@ -97,4 +124,4 @@ declare class AddToKlarnaError extends Error {
|
|
|
97
124
|
}
|
|
98
125
|
declare function isAddToKlarnaError(err: unknown): err is AddToKlarnaError;
|
|
99
126
|
|
|
100
|
-
export { type AddToKlarnaClient, AddToKlarnaError, type BuildLinkInput, type ClientOptions, type DecryptedPayload, type Environment, type ErrorCode, type Region, createAddToKlarnaClient, isAddToKlarnaError };
|
|
127
|
+
export { type AddToKlarnaClient, AddToKlarnaError, type BuildLinkInput, type ClientOptions, type ClientTarget, type DecryptedPayload, type Environment, type ErrorCode, type Region, createAddToKlarnaClient, isAddToKlarnaError };
|
package/dist/index.js
CHANGED
|
@@ -27,8 +27,8 @@ function toBase64Url(input) {
|
|
|
27
27
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// src/
|
|
31
|
-
function
|
|
30
|
+
// src/buildLinkPath.ts
|
|
31
|
+
function buildLinkPath(linkPath, brandNickname, jweCompact) {
|
|
32
32
|
if (typeof brandNickname !== "string" || brandNickname.length === 0) {
|
|
33
33
|
throw new AddToKlarnaError("INVALID_INPUT", "`brandNickname` must be a non-empty string.");
|
|
34
34
|
}
|
|
@@ -42,7 +42,7 @@ function buildLinkUrl(baseUrl, brandNickname, jweCompact) {
|
|
|
42
42
|
throw new AddToKlarnaError("ENCRYPTION_FAILED", "Empty JWE returned from encryptor.");
|
|
43
43
|
}
|
|
44
44
|
const encryptedPayload = toBase64Url(jweCompact);
|
|
45
|
-
return `${
|
|
45
|
+
return `${linkPath}/${encodeURIComponent(brandNickname)}/${encryptedPayload}`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// src/buildPayload.ts
|
|
@@ -62,12 +62,35 @@ var JWKS_URLS = {
|
|
|
62
62
|
production: "https://app.klarna.com/.well-known/jwks.json",
|
|
63
63
|
staging: "https://eu-non-production-klarna-static-assets.s3.eu-west-1.amazonaws.com/klapp/.well-known/jwks.json"
|
|
64
64
|
};
|
|
65
|
-
var
|
|
66
|
-
production: "
|
|
67
|
-
staging: "
|
|
65
|
+
var LINK_PATHS = {
|
|
66
|
+
production: "/loyalty-cards-v2/add-to-klarna",
|
|
67
|
+
staging: "/loyalty-cards-v2/add-to-klarna"
|
|
68
68
|
};
|
|
69
|
+
var ONELINK_BASE_URLS = {
|
|
70
|
+
pink: "https://l.klarna.com/22XC",
|
|
71
|
+
internalpink: "https://klarnainternalpink.onelink.me/lXgD",
|
|
72
|
+
yellow: "https://klarnayellow.onelink.me/JQ8X",
|
|
73
|
+
staging: "https://klarnastaging.onelink.me/hV1K",
|
|
74
|
+
oneoff: "https://klarnaoneoff.onelink.me/FaEr",
|
|
75
|
+
local: "https://klarnalocal.onelink.me/dxUs"
|
|
76
|
+
};
|
|
77
|
+
var APP_URI_SCHEMES = {
|
|
78
|
+
pink: "klarna://",
|
|
79
|
+
internalpink: "klarnainternalpink://",
|
|
80
|
+
yellow: "klarnayellow://",
|
|
81
|
+
staging: "klarnastaging://",
|
|
82
|
+
oneoff: "klarnaoneoff://",
|
|
83
|
+
local: "klarnadev://"
|
|
84
|
+
};
|
|
85
|
+
var DESKTOP_FALLBACK_URL = "https://klarna.com/add-to-klarna";
|
|
86
|
+
var MEDIA_SOURCE = "WebApp";
|
|
87
|
+
var CAMPAIGN_NAME = "add-to-klarna";
|
|
69
88
|
function resolveConfig(options) {
|
|
70
|
-
const {
|
|
89
|
+
const {
|
|
90
|
+
environment = "production",
|
|
91
|
+
clientTarget = "pink",
|
|
92
|
+
region
|
|
93
|
+
} = options ?? {};
|
|
71
94
|
if (environment !== "production" && environment !== "staging") {
|
|
72
95
|
throw new AddToKlarnaError(
|
|
73
96
|
"INVALID_CONFIG",
|
|
@@ -80,9 +103,20 @@ function resolveConfig(options) {
|
|
|
80
103
|
`Unknown region: ${String(region)}. Expected "eu", "us" or "ap".`
|
|
81
104
|
);
|
|
82
105
|
}
|
|
106
|
+
if (clientTarget !== "pink" && clientTarget !== "internalpink" && clientTarget !== "yellow" && clientTarget !== "staging" && clientTarget !== "oneoff" && clientTarget !== "local") {
|
|
107
|
+
throw new AddToKlarnaError(
|
|
108
|
+
"INVALID_CONFIG",
|
|
109
|
+
`Unknown clientTarget: ${String(clientTarget)}. Expected "pink", "internalpink", "yellow", "staging", "oneoff" or "local"`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
83
112
|
return {
|
|
84
113
|
jwksUrl: JWKS_URLS[environment],
|
|
85
|
-
|
|
114
|
+
linkPath: LINK_PATHS[environment],
|
|
115
|
+
oneLinkBaseUrl: ONELINK_BASE_URLS[clientTarget],
|
|
116
|
+
appUriScheme: APP_URI_SCHEMES[clientTarget],
|
|
117
|
+
desktopFallbackUrl: DESKTOP_FALLBACK_URL,
|
|
118
|
+
mediaSource: MEDIA_SOURCE,
|
|
119
|
+
campaignName: CAMPAIGN_NAME,
|
|
86
120
|
region
|
|
87
121
|
};
|
|
88
122
|
}
|
|
@@ -168,6 +202,49 @@ function assertJwks(body) {
|
|
|
168
202
|
return { keys };
|
|
169
203
|
}
|
|
170
204
|
|
|
205
|
+
// src/oneLink.ts
|
|
206
|
+
function buildOneLinkUrl(input) {
|
|
207
|
+
requireNonEmptyString("oneLinkBaseUrl", input.oneLinkBaseUrl, "INVALID_CONFIG");
|
|
208
|
+
requireNonEmptyString("pid", input.pid, "INVALID_CONFIG");
|
|
209
|
+
requireNonEmptyString("c", input.c, "INVALID_CONFIG");
|
|
210
|
+
requireNonEmptyString("appUriScheme", input.appUriScheme, "INVALID_CONFIG");
|
|
211
|
+
requireNonEmptyString("deepLinkValue", input.deepLinkValue, "INVALID_INPUT");
|
|
212
|
+
requireHttpUrl("webFallbackUrl", input.webFallbackUrl);
|
|
213
|
+
const params = [
|
|
214
|
+
["pid", input.pid],
|
|
215
|
+
["c", input.c],
|
|
216
|
+
["af_dp", buildAppDeepLink(input.appUriScheme, input.deepLinkValue)],
|
|
217
|
+
["deep_link_value", input.deepLinkValue],
|
|
218
|
+
["af_web_dp", input.webFallbackUrl]
|
|
219
|
+
];
|
|
220
|
+
const query = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
|
|
221
|
+
return `${input.oneLinkBaseUrl}?${query}`;
|
|
222
|
+
}
|
|
223
|
+
function buildAppDeepLink(appUriScheme, deepLinkValue) {
|
|
224
|
+
const path = deepLinkValue.startsWith("/") ? deepLinkValue.slice(1) : deepLinkValue;
|
|
225
|
+
return `${appUriScheme}${path}`;
|
|
226
|
+
}
|
|
227
|
+
function requireNonEmptyString(name, value, code) {
|
|
228
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
229
|
+
throw new AddToKlarnaError(code, `\`${name}\` must be a non-empty string.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function requireHttpUrl(name, value) {
|
|
233
|
+
requireNonEmptyString(name, value, "INVALID_CONFIG");
|
|
234
|
+
let url;
|
|
235
|
+
try {
|
|
236
|
+
url = new URL(value);
|
|
237
|
+
} catch {
|
|
238
|
+
throw new AddToKlarnaError("INVALID_CONFIG", `\`${name}\` is not a valid URL: ${value}`);
|
|
239
|
+
}
|
|
240
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
241
|
+
throw new AddToKlarnaError(
|
|
242
|
+
"INVALID_CONFIG",
|
|
243
|
+
`\`${name}\` must use http or https. Got: ${value}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
171
248
|
// src/client.ts
|
|
172
249
|
function createAddToKlarnaClient(options) {
|
|
173
250
|
const config = resolveConfig(options);
|
|
@@ -182,7 +259,15 @@ function createAddToKlarnaClient(options) {
|
|
|
182
259
|
const jwks = await fetchJwks(config.jwksUrl);
|
|
183
260
|
const key = pickEncryptionKey(jwks, config.region);
|
|
184
261
|
const jwe = await encryptJwe(payload, key);
|
|
185
|
-
|
|
262
|
+
const deepLinkValue = buildLinkPath(config.linkPath, input.brandNickname, jwe);
|
|
263
|
+
return buildOneLinkUrl({
|
|
264
|
+
oneLinkBaseUrl: config.oneLinkBaseUrl,
|
|
265
|
+
pid: config.mediaSource,
|
|
266
|
+
c: config.campaignName,
|
|
267
|
+
appUriScheme: config.appUriScheme,
|
|
268
|
+
deepLinkValue,
|
|
269
|
+
webFallbackUrl: config.desktopFallbackUrl
|
|
270
|
+
});
|
|
186
271
|
}
|
|
187
272
|
async function redirect(input) {
|
|
188
273
|
const url = await buildLink(input);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/utils.ts","../src/buildLinkUrl.ts","../src/buildPayload.ts","../src/config.ts","../src/encryptJwe.ts","../src/jwksClient.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Stable error codes. Merchant integrations should branch on these, not on\n * the human-readable message.\n */\nexport type ErrorCode =\n | \"INVALID_INPUT\"\n | \"INVALID_CONFIG\"\n | \"JWKS_FETCH_FAILED\"\n | \"JWKS_INVALID\"\n | \"NO_MATCHING_KEY\"\n | \"ENCRYPTION_FAILED\"\n | \"NAVIGATION_UNAVAILABLE\";\n\n/**\n * The single error type thrown by every public API in this package. Always\n * carries a stable {@link ErrorCode} on `code` and, where relevant, the\n * underlying error on `cause`.\n */\nexport class AddToKlarnaError extends Error {\n public readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"AddToKlarnaError\";\n this.code = code;\n // Restore prototype chain for ES5-targeted bundles. No-op on modern targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport function isAddToKlarnaError(err: unknown): err is AddToKlarnaError {\n return err instanceof AddToKlarnaError;\n}\n","/**\n * Encode a UTF-8 string as base64url, without padding. Works in both Node\n * and browsers.\n *\n * Used to wrap the JWE compact serialization into a single URL path segment\n * (without the wrap, the dots inside the JWE break path matching on Klarna's\n * web router).\n */\nexport function toBase64Url(input: string): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(input, \"utf8\").toString(\"base64url\");\n }\n const bytes = new TextEncoder().encode(input);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Decode a base64url string back to UTF-8. Inverse of {@link toBase64Url}.\n * Exported mostly so tests can assert on the round-trip.\n */\nexport function fromBase64Url(input: string): string {\n const padded =\n input.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"==\".slice(0, (4 - (input.length % 4)) % 4);\n\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(padded, \"base64\").toString(\"utf8\");\n }\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return new TextDecoder().decode(bytes);\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport { toBase64Url } from \"./utils.js\";\n\n/**\n * Compose the final universal-link URL:\n *\n * <baseUrl>/<brandNickname>/<encryptedSegment>\n *\n * `baseUrl` already includes the `/add-to-klarna` route segment (resolved in\n * {@link resolveConfig}). `encryptedSegment` is the JWE compact serialization\n * wrapped in a single base64url — the wrap is required by the Klarna web\n * router because the dots in the JWE compact form would otherwise break\n * path matching.\n */\nexport function buildLinkUrl(baseUrl: string, brandNickname: string, jweCompact: string): string {\n if (typeof brandNickname !== \"string\" || brandNickname.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`brandNickname` must be a non-empty string.\");\n }\n if (/[\\s/?#]/.test(brandNickname)) {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n `\\`brandNickname\\` contains invalid characters (whitespace, \"/\", \"?\" or \"#\"): ${brandNickname}`,\n );\n }\n if (typeof jweCompact !== \"string\" || jweCompact.length === 0) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", \"Empty JWE returned from encryptor.\");\n }\n const encryptedPayload = toBase64Url(jweCompact);\n return `${baseUrl}/${encodeURIComponent(brandNickname)}/${encryptedPayload}`;\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * Build the plaintext payload that goes inside the JWE.\n *\n * The schema is `{ inputId, linkId, iat }`. Klarna's backend validates this\n * shape on decryption, so the field names and types must remain stable.\n */\nexport function buildPayload(inputId: string): DecryptedPayload {\n if (typeof inputId !== \"string\" || inputId.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`inputId` must be a non-empty string.\");\n }\n return {\n inputId,\n linkId: globalThis.crypto.randomUUID(),\n iat: Math.floor(Date.now() / 1000),\n };\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { ClientOptions, Environment, Region } from \"./types.js\";\n\n/**\n * Resolved configuration used internally. Everything has been collapsed down\n * to concrete strings by the time we construct one.\n */\nexport interface ResolvedConfig {\n jwksUrl: string;\n linkBaseUrl: string;\n region: Region;\n}\n\n/**\n * JWKS endpoints per environment. **Independent of region.** Every published\n * JWKS file contains keys for every region, namespaced by a `kid-{region}-`\n * prefix; the picker selects the right one.\n */\nconst JWKS_URLS: Record<Environment, string> = {\n production: \"https://app.klarna.com/.well-known/jwks.json\",\n staging:\n \"https://eu-non-production-klarna-static-assets.s3.eu-west-1.amazonaws.com/klapp/.well-known/jwks.json\",\n};\n\n/**\n * Base URLs for the universal link. The final URL appended is:\n *\n * <base>/<brandNickname>/<encryptedPayload>\n *\n * Production uses an HTTPS Universal Link. Staging uses the dev custom scheme\n * so taps open the staging app build directly.\n */\nconst LINK_BASE_URLS: Record<Environment, string> = {\n production: \"https://app.klarna.com/loyalty-cards-v2/add-to-klarna\",\n staging: \"klarnadev://loyalty-cards-v2/add-to-klarna\",\n};\n\n/**\n * Resolve user-facing client options into a concrete config.\n *\n * Validates inputs up-front so any misconfiguration surfaces as an\n * `INVALID_CONFIG` `AddToKlarnaError` *before* the merchant page issues any\n * network calls.\n */\nexport function resolveConfig(options: ClientOptions): ResolvedConfig {\n const { environment = \"production\", region } = options ?? ({} as ClientOptions);\n\n if (environment !== \"production\" && environment !== \"staging\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown environment: ${String(environment)}. Expected \"production\" or \"staging\".`,\n );\n }\n if (region !== \"eu\" && region !== \"us\" && region !== \"ap\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown region: ${String(region)}. Expected \"eu\", \"us\" or \"ap\".`,\n );\n }\n\n return {\n jwksUrl: JWKS_URLS[environment],\n linkBaseUrl: LINK_BASE_URLS[environment],\n region,\n };\n}\n","import { EncryptJWT, importJWK, type JWK } from \"jose\";\n\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * The JWE profile this library produces. Pinned end-to-end:\n *\n * - `ALG` (key-encryption algorithm)\n * - `ENC` (content-encryption algorithm)\n *\n * `pickEncryptionKey` enforces the matching `kty`/`crv` on the JWKS side,\n * so a JWKS that drifts off this profile fails closed with `NO_MATCHING_KEY`\n * instead of silently producing a non-decryptable link. Changing either\n * constant is an SDK-coordinated event with Klarna's backend.\n */\nexport const ALG = \"ECDH-ES+A256KW\";\nexport const ENC = \"A256GCM\";\n\n/**\n * Encrypt the plaintext payload as a JWE using the given JWK. Produces the\n * compact serialization (five dot-separated base64url segments).\n */\nexport async function encryptJwe(\n payload: DecryptedPayload,\n key: JWK & { kid: string },\n): Promise<string> {\n let imported: Awaited<ReturnType<typeof importJWK>>;\n try {\n imported = await importJWK(key, ALG);\n } catch (cause) {\n throw new AddToKlarnaError(\n \"ENCRYPTION_FAILED\",\n `Could not import JWK with kid=\"${key.kid}\" for alg=\"${ALG}\".`,\n { cause },\n );\n }\n\n try {\n return await new EncryptJWT({\n inputId: payload.inputId,\n linkId: payload.linkId,\n })\n .setProtectedHeader({\n alg: ALG,\n enc: ENC,\n kid: key.kid,\n typ: \"JWT\",\n cty: \"application/json\",\n })\n .setIssuedAt(payload.iat)\n .encrypt(imported);\n } catch (cause) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", `JWE encryption failed for kid=\"${key.kid}\".`, {\n cause,\n });\n }\n}\n","import type { JWK } from \"jose\";\n\nimport { ALG } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { Region } from \"./types.js\";\n\n/**\n * The minimal JWKS shape we depend on. The actual file may contain extra\n * fields per RFC 7517; we ignore everything we do not need.\n */\nexport interface Jwks {\n keys: Array<JWK & { kid?: string; alg?: string; use?: string }>;\n}\n\n/**\n * The single key shape this library accepts off the JWKS. Pinned to the\n * tuple Klarna's backend publishes today:\n *\n * alg = ECDH-ES+A256KW\n * kty = EC\n * crv = P-256\n *\n * Anything else is rejected with `NO_MATCHING_KEY`, so a JWKS that ever\n * drifts off this profile — whether by mistake or because someone\n * managed to substitute it past TLS — fails closed instead of\n * silently using a weaker or symmetric algorithm. Any change to this\n * tuple is an SDK-coordinated event with the backend.\n */\nconst REQUIRED_KTY = \"EC\";\nconst REQUIRED_CRV = \"P-256\";\n\n/**\n * Fetch and parse the JWKS at `url`. There is no in-memory cache — caching is\n * delegated to the HTTP layer and Klarna's CDN. Wraps every failure mode in\n * a typed `AddToKlarnaError`.\n */\nexport async function fetchJwks(url: string): Promise<Jwks> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_FETCH_FAILED\", `Failed to fetch JWKS from ${url}`, { cause });\n }\n\n if (!response.ok) {\n throw new AddToKlarnaError(\n \"JWKS_FETCH_FAILED\",\n `JWKS fetch returned HTTP ${response.status} ${response.statusText} for ${url}`,\n );\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response body was not valid JSON.\", { cause });\n }\n\n return assertJwks(body);\n}\n\n/**\n * Pick the encryption key for `region` from a JWKS document.\n *\n * A key is usable iff it declares all of:\n *\n * - `use === \"enc\"`\n * - `kid` starting with `kid-{region}-`\n * - `alg === ALG`, `kty === \"EC\"`, `crv === \"P-256\"` (see file header)\n *\n * If multiple keys match, the first one in document order wins — the JWKS\n * is published with the most recent key first. Throws `NO_MATCHING_KEY`\n * when nothing matches.\n */\nexport function pickEncryptionKey(jwks: Jwks, region: Region): JWK & { kid: string } {\n const prefix = `kid-${region}-`;\n const chosen = jwks.keys.find(\n (k) =>\n k.use === \"enc\" &&\n typeof k.kid === \"string\" &&\n k.kid.startsWith(prefix) &&\n k.alg === ALG &&\n k.kty === REQUIRED_KTY &&\n k.crv === REQUIRED_CRV,\n );\n\n if (!chosen || typeof chosen.kid !== \"string\") {\n throw new AddToKlarnaError(\n \"NO_MATCHING_KEY\",\n `JWKS contains no usable encryption key with kid prefix \"${prefix}\" ` +\n `(need use=\"enc\", alg=\"${ALG}\", kty=\"${REQUIRED_KTY}\", crv=\"${REQUIRED_CRV}\").`,\n );\n }\n return chosen as JWK & { kid: string };\n}\n\nfunction assertJwks(body: unknown): Jwks {\n if (!body || typeof body !== \"object\" || !(\"keys\" in body)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response is missing a `keys` array.\");\n }\n const keys = (body as { keys: unknown }).keys;\n if (!Array.isArray(keys)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS `keys` is not an array.\");\n }\n return { keys: keys as Jwks[\"keys\"] };\n}\n","import { buildLinkUrl } from \"./buildLinkUrl.js\";\nimport { buildPayload } from \"./buildPayload.js\";\nimport { resolveConfig } from \"./config.js\";\nimport { encryptJwe } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport { fetchJwks, pickEncryptionKey } from \"./jwksClient.js\";\nimport type { AddToKlarnaClient, BuildLinkInput, ClientOptions } from \"./types.js\";\n\n/**\n * Create an Add-to-Klarna client.\n *\n * Construction is cheap and synchronous: it validates options and wires up\n * the pipeline. The first network call happens on the first\n * {@link AddToKlarnaClient.buildLink} / {@link AddToKlarnaClient.redirect}.\n * Every `buildLink` re-fetches the JWKS — caching is handled by the HTTP\n * layer and Klarna's CDN, not in memory.\n */\nexport function createAddToKlarnaClient(options: ClientOptions): AddToKlarnaClient {\n const config = resolveConfig(options);\n\n async function buildLink(input: BuildLinkInput): Promise<string> {\n if (!input || typeof input !== \"object\") {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n \"buildLink() requires an object with `brandNickname` and `inputId`.\",\n );\n }\n const payload = buildPayload(input.inputId);\n const jwks = await fetchJwks(config.jwksUrl);\n const key = pickEncryptionKey(jwks, config.region);\n const jwe = await encryptJwe(payload, key);\n return buildLinkUrl(config.linkBaseUrl, input.brandNickname, jwe);\n }\n\n async function redirect(input: BuildLinkInput): Promise<void> {\n const url = await buildLink(input);\n const w = (globalThis as { window?: { location?: { assign?: (u: string) => void } } }).window;\n const assign = w?.location?.assign;\n if (typeof assign !== \"function\") {\n throw new AddToKlarnaError(\n \"NAVIGATION_UNAVAILABLE\",\n \"redirect() requires a browser context with `window.location.assign`. Use buildLink() and navigate yourself in non-browser environments.\",\n );\n }\n assign.call(w!.location, url);\n }\n\n return { buildLink, redirect };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/utils.ts","../src/buildLinkPath.ts","../src/buildPayload.ts","../src/config.ts","../src/encryptJwe.ts","../src/jwksClient.ts","../src/oneLink.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Stable error codes. Merchant integrations should branch on these, not on\n * the human-readable message.\n */\nexport type ErrorCode =\n | \"INVALID_INPUT\"\n | \"INVALID_CONFIG\"\n | \"JWKS_FETCH_FAILED\"\n | \"JWKS_INVALID\"\n | \"NO_MATCHING_KEY\"\n | \"ENCRYPTION_FAILED\"\n | \"NAVIGATION_UNAVAILABLE\";\n\n/**\n * The single error type thrown by every public API in this package. Always\n * carries a stable {@link ErrorCode} on `code` and, where relevant, the\n * underlying error on `cause`.\n */\nexport class AddToKlarnaError extends Error {\n public readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"AddToKlarnaError\";\n this.code = code;\n // Restore prototype chain for ES5-targeted bundles. No-op on modern targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport function isAddToKlarnaError(err: unknown): err is AddToKlarnaError {\n return err instanceof AddToKlarnaError;\n}\n","/**\n * Encode a UTF-8 string as base64url, without padding. Works in both Node\n * and browsers.\n *\n * Used to wrap the JWE compact serialization into a single URL path segment\n * (without the wrap, the dots inside the JWE break path matching on Klarna's\n * web router).\n */\nexport function toBase64Url(input: string): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(input, \"utf8\").toString(\"base64url\");\n }\n const bytes = new TextEncoder().encode(input);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Decode a base64url string back to UTF-8. Inverse of {@link toBase64Url}.\n * Exported mostly so tests can assert on the round-trip.\n */\nexport function fromBase64Url(input: string): string {\n const padded =\n input.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"==\".slice(0, (4 - (input.length % 4)) % 4);\n\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(padded, \"base64\").toString(\"utf8\");\n }\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return new TextDecoder().decode(bytes);\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport { toBase64Url } from \"./utils.js\";\n\n/**\n * Compose the deep-link *path* that goes inside the outer AppsFlyer OneLink's\n * `deep_link_value` parameter:\n *\n * <linkPath>/<brandNickname>/<encryptedSegment>\n *\n * `linkPath` is a rooted path like `/loyalty-cards-v2/add-to-klarna` (see\n * {@link resolveConfig}). `encryptedSegment` is the JWE compact serialization\n * wrapped in a single base64url — the wrap is required because the dots in the\n * JWE compact form would otherwise break Klarna's in-app router path matching.\n *\n * Returns a path (not a full URL): the composed value is meant to be passed\n * verbatim to {@link buildOneLinkUrl} as `deepLinkValue`, which handles the\n * outer URL encoding.\n */\nexport function buildLinkPath(linkPath: string, brandNickname: string, jweCompact: string): string {\n if (typeof brandNickname !== \"string\" || brandNickname.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`brandNickname` must be a non-empty string.\");\n }\n if (/[\\s/?#]/.test(brandNickname)) {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n `\\`brandNickname\\` contains invalid characters (whitespace, \"/\", \"?\" or \"#\"): ${brandNickname}`,\n );\n }\n if (typeof jweCompact !== \"string\" || jweCompact.length === 0) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", \"Empty JWE returned from encryptor.\");\n }\n const encryptedPayload = toBase64Url(jweCompact);\n return `${linkPath}/${encodeURIComponent(brandNickname)}/${encryptedPayload}`;\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * Build the plaintext payload that goes inside the JWE.\n *\n * The schema is `{ inputId, linkId, iat }`. Klarna's backend validates this\n * shape on decryption, so the field names and types must remain stable.\n */\nexport function buildPayload(inputId: string): DecryptedPayload {\n if (typeof inputId !== \"string\" || inputId.length === 0) {\n throw new AddToKlarnaError(\"INVALID_INPUT\", \"`inputId` must be a non-empty string.\");\n }\n return {\n inputId,\n linkId: globalThis.crypto.randomUUID(),\n iat: Math.floor(Date.now() / 1000),\n };\n}\n","import { AddToKlarnaError } from \"./errors.js\";\nimport type { ClientOptions, ClientTarget, Environment, Region } from \"./types.js\";\n\n/**\n * Resolved configuration used internally. Everything has been collapsed down\n * to concrete strings by the time we construct one.\n */\nexport interface ResolvedConfig {\n jwksUrl: string;\n /**\n * The in-app deep-link path prefix. Combined with the brand nickname and\n * encrypted payload by {@link buildLinkPath}, then passed as\n * `deep_link_value` on the outer AppsFlyer OneLink.\n */\n linkPath: string;\n /**\n * AppsFlyer OneLink prefix that wraps the encrypted deep link. See\n * {@link buildOneLinkUrl}.\n */\n oneLinkBaseUrl: string;\n /**\n * Custom URI scheme for the targeted Klarna app, including `://`.\n * {@link buildOneLinkUrl} appends the in-app path and sends the result as\n * `af_dp`. Selected by `clientTarget`, not by `environment`.\n */\n appUriScheme: string;\n /**\n * Desktop fallback URL (`af_web_dp`). Always the production add-to-klarna\n * page — not selected by `environment` or `clientTarget`.\n */\n desktopFallbackUrl: string;\n /** AppsFlyer media source (`pid`). */\n mediaSource: string;\n /** AppsFlyer campaign name (`c`). */\n campaignName: string;\n region: Region;\n}\n\n/**\n * JWKS endpoints per environment. **Independent of region.** Every published\n * JWKS file contains keys for every region, namespaced by a `kid-{region}-`\n * prefix; the picker selects the right one.\n */\nconst JWKS_URLS: Record<Environment, string> = {\n production: \"https://app.klarna.com/.well-known/jwks.json\",\n staging:\n \"https://eu-non-production-klarna-static-assets.s3.eu-west-1.amazonaws.com/klapp/.well-known/jwks.json\",\n};\n\n/**\n * In-app deep-link path prefixes. Combined with `<brandNickname>/<encrypted>`\n * to form the value of `deep_link_value` on the outer AppsFlyer OneLink. The\n * Klarna app parses this path and routes to the add-to-klarna screen.\n */\nconst LINK_PATHS: Record<Environment, string> = {\n production: \"/loyalty-cards-v2/add-to-klarna\",\n staging: \"/loyalty-cards-v2/add-to-klarna\",\n};\n\n/**\n * AppsFlyer OneLink prefixes. Production uses Klarna's custom `l.klarna.com`\n * short domain; staging uses the standard AppsFlyer `*.onelink.me` host that\n * targets the staging app build.\n */\nconst ONELINK_BASE_URLS: Record<ClientTarget, string> = {\n pink: \"https://l.klarna.com/22XC\",\n internalpink: \"https://klarnainternalpink.onelink.me/lXgD\",\n yellow: \"https://klarnayellow.onelink.me/JQ8X\",\n staging: \"https://klarnastaging.onelink.me/hV1K\",\n oneoff: \"https://klarnaoneoff.onelink.me/FaEr\",\n local: \"https://klarnalocal.onelink.me/dxUs\",\n};\n\n/**\n * Custom URI schemes, one per `clientTarget`. These match the schemes the\n * Klarna app registers (`klarna` plus the build suffix). {@link buildOneLinkUrl}\n * appends the in-app path before sending the result as `af_dp`.\n */\nconst APP_URI_SCHEMES: Record<ClientTarget, string> = {\n pink: \"klarna://\",\n internalpink: \"klarnainternalpink://\",\n yellow: \"klarnayellow://\",\n staging: \"klarnastaging://\",\n oneoff: \"klarnaoneoff://\",\n local: \"klarnadev://\",\n};\n\n/**\n * Desktop fallback (`af_web_dp`). One production page for every environment\n * and client target — there is no staging-specific fallback.\n */\nconst DESKTOP_FALLBACK_URL = \"https://klarna.com/add-to-klarna\";\n\n/**\n * AppsFlyer `pid` (media source).\n */\nconst MEDIA_SOURCE = \"WebApp\";\n\n/**\n * AppsFlyer `c` (campaign name).\n */\nconst CAMPAIGN_NAME = \"add-to-klarna\";\n\n/**\n * Resolve user-facing client options into a concrete config.\n *\n * Validates inputs up-front so any misconfiguration surfaces as an\n * `INVALID_CONFIG` `AddToKlarnaError` *before* the merchant page issues any\n * network calls.\n */\nexport function resolveConfig(options: ClientOptions): ResolvedConfig {\n const {\n environment = \"production\",\n clientTarget = \"pink\",\n region,\n } = options ?? ({} as ClientOptions);\n\n if (environment !== \"production\" && environment !== \"staging\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown environment: ${String(environment)}. Expected \"production\" or \"staging\".`,\n );\n }\n if (region !== \"eu\" && region !== \"us\" && region !== \"ap\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown region: ${String(region)}. Expected \"eu\", \"us\" or \"ap\".`,\n );\n }\n if (\n clientTarget !== \"pink\" &&\n clientTarget !== \"internalpink\" &&\n clientTarget !== \"yellow\" &&\n clientTarget !== \"staging\" &&\n clientTarget !== \"oneoff\" &&\n clientTarget !== \"local\"\n ) {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `Unknown clientTarget: ${String(clientTarget)}. Expected \"pink\", \"internalpink\", \"yellow\", \"staging\", \"oneoff\" or \"local\"`,\n );\n }\n\n return {\n jwksUrl: JWKS_URLS[environment],\n linkPath: LINK_PATHS[environment],\n oneLinkBaseUrl: ONELINK_BASE_URLS[clientTarget],\n appUriScheme: APP_URI_SCHEMES[clientTarget],\n desktopFallbackUrl: DESKTOP_FALLBACK_URL,\n mediaSource: MEDIA_SOURCE,\n campaignName: CAMPAIGN_NAME,\n region,\n };\n}\n","import { EncryptJWT, importJWK, type JWK } from \"jose\";\n\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { DecryptedPayload } from \"./types.js\";\n\n/**\n * The JWE profile this library produces. Pinned end-to-end:\n *\n * - `ALG` (key-encryption algorithm)\n * - `ENC` (content-encryption algorithm)\n *\n * `pickEncryptionKey` enforces the matching `kty`/`crv` on the JWKS side,\n * so a JWKS that drifts off this profile fails closed with `NO_MATCHING_KEY`\n * instead of silently producing a non-decryptable link. Changing either\n * constant is an SDK-coordinated event with Klarna's backend.\n */\nexport const ALG = \"ECDH-ES+A256KW\";\nexport const ENC = \"A256GCM\";\n\n/**\n * Encrypt the plaintext payload as a JWE using the given JWK. Produces the\n * compact serialization (five dot-separated base64url segments).\n */\nexport async function encryptJwe(\n payload: DecryptedPayload,\n key: JWK & { kid: string },\n): Promise<string> {\n let imported: Awaited<ReturnType<typeof importJWK>>;\n try {\n imported = await importJWK(key, ALG);\n } catch (cause) {\n throw new AddToKlarnaError(\n \"ENCRYPTION_FAILED\",\n `Could not import JWK with kid=\"${key.kid}\" for alg=\"${ALG}\".`,\n { cause },\n );\n }\n\n try {\n return await new EncryptJWT({\n inputId: payload.inputId,\n linkId: payload.linkId,\n })\n .setProtectedHeader({\n alg: ALG,\n enc: ENC,\n kid: key.kid,\n typ: \"JWT\",\n cty: \"application/json\",\n })\n .setIssuedAt(payload.iat)\n .encrypt(imported);\n } catch (cause) {\n throw new AddToKlarnaError(\"ENCRYPTION_FAILED\", `JWE encryption failed for kid=\"${key.kid}\".`, {\n cause,\n });\n }\n}\n","import type { JWK } from \"jose\";\n\nimport { ALG } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport type { Region } from \"./types.js\";\n\n/**\n * The minimal JWKS shape we depend on. The actual file may contain extra\n * fields per RFC 7517; we ignore everything we do not need.\n */\nexport interface Jwks {\n keys: Array<JWK & { kid?: string; alg?: string; use?: string }>;\n}\n\n/**\n * The single key shape this library accepts off the JWKS. Pinned to the\n * tuple Klarna's backend publishes today:\n *\n * alg = ECDH-ES+A256KW\n * kty = EC\n * crv = P-256\n *\n * Anything else is rejected with `NO_MATCHING_KEY`, so a JWKS that ever\n * drifts off this profile — whether by mistake or because someone\n * managed to substitute it past TLS — fails closed instead of\n * silently using a weaker or symmetric algorithm. Any change to this\n * tuple is an SDK-coordinated event with the backend.\n */\nconst REQUIRED_KTY = \"EC\";\nconst REQUIRED_CRV = \"P-256\";\n\n/**\n * Fetch and parse the JWKS at `url`. There is no in-memory cache — caching is\n * delegated to the HTTP layer and Klarna's CDN. Wraps every failure mode in\n * a typed `AddToKlarnaError`.\n */\nexport async function fetchJwks(url: string): Promise<Jwks> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_FETCH_FAILED\", `Failed to fetch JWKS from ${url}`, { cause });\n }\n\n if (!response.ok) {\n throw new AddToKlarnaError(\n \"JWKS_FETCH_FAILED\",\n `JWKS fetch returned HTTP ${response.status} ${response.statusText} for ${url}`,\n );\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch (cause) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response body was not valid JSON.\", { cause });\n }\n\n return assertJwks(body);\n}\n\n/**\n * Pick the encryption key for `region` from a JWKS document.\n *\n * A key is usable iff it declares all of:\n *\n * - `use === \"enc\"`\n * - `kid` starting with `kid-{region}-`\n * - `alg === ALG`, `kty === \"EC\"`, `crv === \"P-256\"` (see file header)\n *\n * If multiple keys match, the first one in document order wins — the JWKS\n * is published with the most recent key first. Throws `NO_MATCHING_KEY`\n * when nothing matches.\n */\nexport function pickEncryptionKey(jwks: Jwks, region: Region): JWK & { kid: string } {\n const prefix = `kid-${region}-`;\n const chosen = jwks.keys.find(\n (k) =>\n k.use === \"enc\" &&\n typeof k.kid === \"string\" &&\n k.kid.startsWith(prefix) &&\n k.alg === ALG &&\n k.kty === REQUIRED_KTY &&\n k.crv === REQUIRED_CRV,\n );\n\n if (!chosen || typeof chosen.kid !== \"string\") {\n throw new AddToKlarnaError(\n \"NO_MATCHING_KEY\",\n `JWKS contains no usable encryption key with kid prefix \"${prefix}\" ` +\n `(need use=\"enc\", alg=\"${ALG}\", kty=\"${REQUIRED_KTY}\", crv=\"${REQUIRED_CRV}\").`,\n );\n }\n return chosen as JWK & { kid: string };\n}\n\nfunction assertJwks(body: unknown): Jwks {\n if (!body || typeof body !== \"object\" || !(\"keys\" in body)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS response is missing a `keys` array.\");\n }\n const keys = (body as { keys: unknown }).keys;\n if (!Array.isArray(keys)) {\n throw new AddToKlarnaError(\"JWKS_INVALID\", \"JWKS `keys` is not an array.\");\n }\n return { keys: keys as Jwks[\"keys\"] };\n}\n","import { AddToKlarnaError } from \"./errors.js\";\n\n/**\n * Inputs for {@link buildOneLinkUrl}.\n *\n * All fields are required — this function is deliberately dumb: it takes\n * concrete URLs and composes them into an AppsFlyer OneLink. Environment\n * defaults live in {@link resolveConfig}.\n */\nexport interface BuildOneLinkInput {\n /** The AppsFlyer OneLink prefix (e.g. `https://l.klarna.com/22XC`). */\n oneLinkBaseUrl: string;\n\n /** AppsFlyer media source. Sent as `pid`. */\n pid: string;\n\n /** AppsFlyer campaign name. Sent as `c`. */\n c: string;\n\n /**\n * The in-app deep link path. Sent as `deep_link_value`. AppsFlyer hands\n * this string to the app for in-app routing once the app is open, and\n * replays it after install when the app was not installed (deferred deep\n * linking). It does not launch the app.\n */\n deepLinkValue: string;\n\n /**\n * Custom URI scheme for the targeted Klarna build, including `://`\n * (for example `klarna://` or `klarnaoneoff://`). Combined with\n * `deepLinkValue` and sent as `af_dp`, so the address that launches the\n * app is also the screen.\n */\n appUriScheme: string;\n\n /** Desktop fallback URL. Sent as `af_web_dp`. */\n webFallbackUrl: string;\n}\n\n/**\n * Compose an AppsFlyer OneLink URL that:\n *\n * - launches the installed Klarna app directly to `deepLinkValue` via `af_dp`,\n * - falls back to the App Store / Play Store (configured on the OneLink\n * template, not on this URL) when the app is not installed on mobile,\n * - falls back to `webFallbackUrl` on desktop.\n *\n * The returned URL has the shape:\n *\n * <oneLinkBaseUrl>?pid=<pid>&c=<c>&af_dp=<url-encoded scheme + deepLinkValue>\n * &deep_link_value=<url-encoded deepLinkValue>\n * &af_web_dp=<url-encoded webFallbackUrl>\n *\n * `af_dp` uses two slashes (`klarnaoneoff://loyalty-cards-v2/...`). The Klarna\n * app inserts the third slash before matching the path. A bare scheme opens\n * the app and leaves it on the home screen, because that launch address has\n * no route.\n *\n * Throws `AddToKlarnaError` with `INVALID_CONFIG` if any URL or the app URI\n * scheme is missing or malformed, or `INVALID_INPUT` if `deepLinkValue` is\n * empty.\n */\nexport function buildOneLinkUrl(input: BuildOneLinkInput): string {\n requireNonEmptyString(\"oneLinkBaseUrl\", input.oneLinkBaseUrl, \"INVALID_CONFIG\");\n requireNonEmptyString(\"pid\", input.pid, \"INVALID_CONFIG\");\n requireNonEmptyString(\"c\", input.c, \"INVALID_CONFIG\");\n requireNonEmptyString(\"appUriScheme\", input.appUriScheme, \"INVALID_CONFIG\");\n requireNonEmptyString(\"deepLinkValue\", input.deepLinkValue, \"INVALID_INPUT\");\n requireHttpUrl(\"webFallbackUrl\", input.webFallbackUrl);\n\n // Insertion order is preserved by URLSearchParams / Object.entries, and it\n // matters here only for output stability (easier to snapshot in tests).\n const params: Array<[string, string]> = [\n [\"pid\", input.pid],\n [\"c\", input.c],\n [\"af_dp\", buildAppDeepLink(input.appUriScheme, input.deepLinkValue)],\n [\"deep_link_value\", input.deepLinkValue],\n [\"af_web_dp\", input.webFallbackUrl],\n ];\n\n const query = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join(\"&\");\n\n return `${input.oneLinkBaseUrl}?${query}`;\n}\n\n/**\n * `scheme://` + path, without a leading slash on the path, so the result is\n * `klarnaoneoff://loyalty-cards-v2/add-to-klarna/...` rather than three slashes.\n */\nfunction buildAppDeepLink(appUriScheme: string, deepLinkValue: string): string {\n const path = deepLinkValue.startsWith(\"/\") ? deepLinkValue.slice(1) : deepLinkValue;\n return `${appUriScheme}${path}`;\n}\n\nfunction requireNonEmptyString(\n name: string,\n value: unknown,\n code: \"INVALID_CONFIG\" | \"INVALID_INPUT\",\n): void {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new AddToKlarnaError(code, `\\`${name}\\` must be a non-empty string.`);\n }\n}\n\nfunction requireHttpUrl(name: string, value: unknown): void {\n requireNonEmptyString(name, value, \"INVALID_CONFIG\");\n let url: URL;\n try {\n url = new URL(value as string);\n } catch {\n throw new AddToKlarnaError(\"INVALID_CONFIG\", `\\`${name}\\` is not a valid URL: ${value}`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new AddToKlarnaError(\n \"INVALID_CONFIG\",\n `\\`${name}\\` must use http or https. Got: ${value}`,\n );\n }\n}\n","import { buildLinkPath } from \"./buildLinkPath.js\";\nimport { buildPayload } from \"./buildPayload.js\";\nimport { resolveConfig } from \"./config.js\";\nimport { encryptJwe } from \"./encryptJwe.js\";\nimport { AddToKlarnaError } from \"./errors.js\";\nimport { fetchJwks, pickEncryptionKey } from \"./jwksClient.js\";\nimport { buildOneLinkUrl } from \"./oneLink.js\";\nimport type { AddToKlarnaClient, BuildLinkInput, ClientOptions } from \"./types.js\";\n\n/**\n * Create an Add-to-Klarna client.\n *\n * Construction is cheap and synchronous: it validates options and wires up\n * the pipeline. The first network call happens on the first\n * {@link AddToKlarnaClient.buildLink} / {@link AddToKlarnaClient.redirect}.\n *\n * Each `buildLink` returns an AppsFlyer OneLink that:\n *\n * - opens the Klarna app to the encrypted add-to-klarna deep link when the\n * app is installed,\n * - falls back to the App Store / Play Store on mobile when the app is not\n * installed (deferred deep link replays after install),\n * - falls back to `https://klarna.com/add-to-klarna` on desktop.\n *\n * Every `buildLink` re-fetches the JWKS — caching is handled by the HTTP\n * layer and Klarna's CDN, not in memory.\n */\nexport function createAddToKlarnaClient(options: ClientOptions): AddToKlarnaClient {\n const config = resolveConfig(options);\n\n async function buildLink(input: BuildLinkInput): Promise<string> {\n if (!input || typeof input !== \"object\") {\n throw new AddToKlarnaError(\n \"INVALID_INPUT\",\n \"buildLink() requires an object with `brandNickname` and `inputId`.\",\n );\n }\n const payload = buildPayload(input.inputId);\n const jwks = await fetchJwks(config.jwksUrl);\n const key = pickEncryptionKey(jwks, config.region);\n const jwe = await encryptJwe(payload, key);\n const deepLinkValue = buildLinkPath(config.linkPath, input.brandNickname, jwe);\n return buildOneLinkUrl({\n oneLinkBaseUrl: config.oneLinkBaseUrl,\n pid: config.mediaSource,\n c: config.campaignName,\n appUriScheme: config.appUriScheme,\n deepLinkValue,\n webFallbackUrl: config.desktopFallbackUrl,\n });\n }\n\n async function redirect(input: BuildLinkInput): Promise<void> {\n const url = await buildLink(input);\n const w = (globalThis as { window?: { location?: { assign?: (u: string) => void } } }).window;\n const assign = w?.location?.assign;\n if (typeof assign !== \"function\") {\n throw new AddToKlarnaError(\n \"NAVIGATION_UNAVAILABLE\",\n \"redirect() requires a browser context with `window.location.assign`. Use buildLink() and navigate yourself in non-browser environments.\",\n );\n }\n assign.call(w!.location, url);\n }\n\n return { buildLink, redirect };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klarna/add-to-klarna",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Merchant integration library for generating Klarna 'Add to Klarna'
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Merchant integration library for generating Klarna 'Add to Klarna' AppsFlyer OneLinks.",
|
|
5
5
|
"repository": "https://github.com/klarna-incubator/add-to-klarna-js",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"type": "module",
|