@swell/apps-sdk 2.0.0-alpha.0 → 2.0.0-alpha.2
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 +73 -36
- package/dist/backend.d.cts +5 -5
- package/dist/backend.d.ts +5 -5
- package/dist/backend.js +8 -6
- package/dist/context.d.cts +2 -16
- package/dist/context.d.ts +2 -16
- package/dist/context.js +1 -14
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/jwks.d.cts +1 -0
- package/dist/jwks.d.ts +1 -0
- package/dist/jwks.js +70 -0
- package/dist/request-context.d.cts +37 -0
- package/dist/request-context.d.ts +37 -0
- package/dist/request-context.js +95 -0
- package/dist/store-user.d.cts +7 -0
- package/dist/store-user.d.ts +7 -0
- package/dist/store-user.js +7 -0
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/staff.d.cts +0 -16
- package/dist/staff.d.ts +0 -16
- package/dist/staff.js +0 -33
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The Swell Apps SDK is a TypeScript library for building server-side Swell apps. It
|
|
4
4
|
provides access to the Backend and Storefront APIs, along with helpers for app
|
|
5
|
-
configuration, customer sessions and
|
|
5
|
+
configuration, customer sessions and store user identity.
|
|
6
6
|
|
|
7
7
|
Use it in Swell-hosted apps, Cloudflare Workers or Node.js servers. For browser
|
|
8
8
|
applications, use [`swell-js`](https://github.com/swellstores/swell-js).
|
|
@@ -21,22 +21,30 @@ Version 2 replaces the 1.x theme API. Existing theme applications should remain
|
|
|
21
21
|
|
|
22
22
|
## Getting started
|
|
23
23
|
|
|
24
|
-
###
|
|
24
|
+
### Request context
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
Swell supplies the current store's configuration, credentials and store user identity
|
|
27
|
+
with each request. Verify this context once, then reuse it to create clients and check
|
|
28
|
+
store user access. Use the same pattern for Swell-hosted and self-hosted frontends.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
```ts
|
|
31
|
+
import { verifySwellContext } from '@swell/apps-sdk';
|
|
32
|
+
|
|
33
|
+
const context = await verifySwellContext(request.headers, {
|
|
34
|
+
appId: 'my-app', // Your app's configured slug.
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In a server component, pass `await headers()`. Keep the context and clients on the
|
|
39
|
+
server, scoped to the incoming request.
|
|
32
40
|
|
|
33
41
|
### Backend API calls
|
|
34
42
|
|
|
35
43
|
```ts
|
|
36
44
|
import { SwellBackendAPI } from '@swell/apps-sdk';
|
|
37
45
|
|
|
38
|
-
//
|
|
39
|
-
const backend = new SwellBackendAPI({
|
|
46
|
+
// Reuse the context resolved for this request.
|
|
47
|
+
const backend = new SwellBackendAPI({ context });
|
|
40
48
|
|
|
41
49
|
// Fetch products from the Backend API.
|
|
42
50
|
const products = await backend.get('/products', { limit: 10 });
|
|
@@ -58,8 +66,8 @@ pending writes and deletions.
|
|
|
58
66
|
import { getStorefrontConfig } from '@swell/apps-sdk';
|
|
59
67
|
import { createStorefrontClient } from '@swell/apps-sdk/storefront';
|
|
60
68
|
|
|
61
|
-
// Build
|
|
62
|
-
const config = getStorefrontConfig(
|
|
69
|
+
// Build public storefront configuration from the request context.
|
|
70
|
+
const config = getStorefrontConfig(context);
|
|
63
71
|
const storefront = createStorefrontClient(config, {
|
|
64
72
|
cookies: {
|
|
65
73
|
get: name => cookies.get(name)?.value,
|
|
@@ -86,33 +94,29 @@ import swell from 'swell-js';
|
|
|
86
94
|
swell.init(config.storeId, config.publicKey, config);
|
|
87
95
|
```
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
`parseSwellHeaders(headers)`. It reads headers without verifying their signature;
|
|
91
|
-
keep its result on the server because it includes the backend token.
|
|
97
|
+
Send only this public config to the browser; the request context contains server credentials.
|
|
92
98
|
|
|
93
|
-
###
|
|
99
|
+
### Store users
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
A store user is someone signed in to the store's Swell dashboard. Use
|
|
102
|
+
`requireStoreUser` to check that a request belongs to one before applying your
|
|
103
|
+
application's permission checks:
|
|
97
104
|
|
|
98
105
|
```ts
|
|
99
|
-
import {
|
|
106
|
+
import { requireStoreUser } from '@swell/apps-sdk';
|
|
100
107
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
method: request.method,
|
|
104
|
-
origin: appOrigin, // Your configured app origin.
|
|
105
|
-
cookies: { get: name => cookies.get(name)?.value },
|
|
106
|
-
});
|
|
108
|
+
const storeUser = requireStoreUser(context); // { userId, storeId }, or a 401 SwellError.
|
|
109
|
+
const optional = context.storeUser; // null for a visitor; no exception needed.
|
|
107
110
|
```
|
|
108
111
|
|
|
109
112
|
## API reference
|
|
110
113
|
|
|
111
114
|
### Backend client
|
|
112
115
|
|
|
113
|
-
`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
Pass `{ context }` for a frontend request, or explicit credentials for an external
|
|
117
|
+
server. Backend calls require an access token or secret key and an absolute HTTP(S)
|
|
118
|
+
`apiHost`. Do not mix input sources. Invalid constructor options throw immediately;
|
|
119
|
+
all backend methods return promises and reject on failure.
|
|
116
120
|
|
|
117
121
|
| Method | Result |
|
|
118
122
|
| --- | --- |
|
|
@@ -142,7 +146,7 @@ defaults: `limit` counts additional attempts, and delays are in milliseconds. On
|
|
|
142
146
|
`transaction_conflict` and `transaction_throttled` retry. Ordinary requests and network
|
|
143
147
|
failures are not retried.
|
|
144
148
|
|
|
145
|
-
**Private functions:** use the app slug from `
|
|
149
|
+
**Private functions:** use the app slug from `context.appId` and authorize the caller
|
|
146
150
|
first. `options.method` defaults to `post`; `get`, `put` and `delete` are also supported.
|
|
147
151
|
GET data must contain only flat string, number or boolean values. Caller headers are
|
|
148
152
|
not forwarded; response status and headers are not returned. Function errors and
|
|
@@ -166,14 +170,45 @@ rotation during GET requests. A supplied writer may throw or skip a write; after
|
|
|
166
170
|
its reader must still report the actual state. Writer return values are ignored.
|
|
167
171
|
Persist cookies in writable route handlers or actions.
|
|
168
172
|
|
|
169
|
-
For caching, replace `storefront.request` before first use. The SDK
|
|
173
|
+
For API response caching, replace `storefront.request` before first use. The SDK
|
|
174
|
+
leaves response caching to your application.
|
|
175
|
+
|
|
176
|
+
### Request context options
|
|
177
|
+
|
|
178
|
+
`verifySwellContext(headers, { env?, appId?, storeId?, vaultUrl? })` returns the request
|
|
179
|
+
context or throws if verification fails. Set `appId` and, for a single-store app,
|
|
180
|
+
`storeId` from trusted configuration to reject contexts intended for another app or
|
|
181
|
+
store. Without these options, it verifies the source but does not restrict the
|
|
182
|
+
destination. `vaultUrl` provides an optional vault endpoint override.
|
|
183
|
+
|
|
184
|
+
Configuration is read from `process.env`, or from `env` when supplied. Workers without
|
|
185
|
+
Node compatibility should pass their bindings as `env`.
|
|
186
|
+
|
|
187
|
+
| Variable | Default | Purpose |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `SWELL_VERIFY_HEADERS` | enabled | Set exactly `"false"` to skip signature verification during local development. |
|
|
190
|
+
| `SWELL_HEADERS_JWKS_URL` | `https://swell.store/.well-known/jwks.json` | Override the verification-key endpoint. |
|
|
191
|
+
|
|
192
|
+
For local development against a local Swell instance, put `SWELL_VERIFY_HEADERS=false`
|
|
193
|
+
in `.dev.vars`. Remove it or set it to `"true"` to restore verification. Token structure
|
|
194
|
+
and claim validation still apply, but the context is no longer authenticated: anyone
|
|
195
|
+
who can reach the frontend directly can supply forged context, including a store user.
|
|
196
|
+
Use this bypass only for local development.
|
|
197
|
+
|
|
198
|
+
For server integrations, construct `SwellBackendAPI` with explicit credentials and
|
|
199
|
+
`createStorefrontClient` with explicit public configuration. These clients do not
|
|
200
|
+
require an HTTP request or a store user.
|
|
201
|
+
|
|
202
|
+
### Store users
|
|
170
203
|
|
|
171
|
-
|
|
204
|
+
`requireStoreUser(context)` returns `{ userId, storeId }` or throws `SwellError` with
|
|
205
|
+
status 401 and code `store_user_required`. For optional access, read
|
|
206
|
+
`context.storeUser`, which is null for visitors.
|
|
172
207
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
A store user is anyone signed in to the store's dashboard, including partners and Swell
|
|
209
|
+
support, who may not appear in the store's own user list. Swell's proxy handles their
|
|
210
|
+
authentication and write-origin checks; your application decides what each store user
|
|
211
|
+
may do.
|
|
177
212
|
|
|
178
213
|
### Errors
|
|
179
214
|
|
|
@@ -185,8 +220,10 @@ error handling; `message` is for people and may change.
|
|
|
185
220
|
`body`. Successful GET responses containing `errors` are returned as data.
|
|
186
221
|
- Function invocation failures retain the response payload, or the invocation envelope
|
|
187
222
|
when the payload is null or absent, in `body`.
|
|
188
|
-
-
|
|
189
|
-
|
|
223
|
+
- Header verification uses 401 / `invalid_swell_context` for absent, malformed, expired
|
|
224
|
+
or rejected tokens, and 503 / `swell_jwks_unavailable` for key-service failures.
|
|
225
|
+
- Backend/storefront network errors remain native. Local configuration errors may be
|
|
226
|
+
ordinary `Error` instances; not every failure is a `SwellError`.
|
|
190
227
|
|
|
191
228
|
## Development
|
|
192
229
|
|
package/dist/backend.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SwellRequestContext } from './request-context.cjs';
|
|
2
2
|
export type SwellData = Record<string, any>;
|
|
3
3
|
export type BackendOptions = {
|
|
4
|
-
|
|
4
|
+
context: SwellRequestContext;
|
|
5
5
|
storeId?: never;
|
|
6
6
|
apiHost?: never;
|
|
7
7
|
accessToken?: never;
|
|
@@ -9,7 +9,7 @@ export type BackendOptions = {
|
|
|
9
9
|
appId?: never;
|
|
10
10
|
requestId?: never;
|
|
11
11
|
} | ({
|
|
12
|
-
|
|
12
|
+
context?: never;
|
|
13
13
|
storeId: string;
|
|
14
14
|
apiHost: string;
|
|
15
15
|
appId?: string;
|
|
@@ -47,7 +47,7 @@ export interface TransactionOptions {
|
|
|
47
47
|
jitter?: boolean;
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
/**
|
|
50
|
+
/** Backend client from a frontend request context or explicit server credentials. */
|
|
51
51
|
export declare class SwellBackendAPI {
|
|
52
52
|
#private;
|
|
53
53
|
protected get userAgent(): string;
|
|
@@ -59,7 +59,7 @@ export declare class SwellBackendAPI {
|
|
|
59
59
|
/**
|
|
60
60
|
* Invokes an app's private function from server code and resolves to its response payload,
|
|
61
61
|
* without the envelope's status or headers.
|
|
62
|
-
* `appId` is the app identifier from
|
|
62
|
+
* `appId` is the app identifier from the request context (`context.appId`).
|
|
63
63
|
* `method` (default `post`) selects the function's handler. GET data reaches the function
|
|
64
64
|
* as query parameters, so its values must be flat strings, numbers or booleans.
|
|
65
65
|
* Throws `SwellError` when the function reports a non-2xx status or an error.
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SwellRequestContext } from './request-context.js';
|
|
2
2
|
export type SwellData = Record<string, any>;
|
|
3
3
|
export type BackendOptions = {
|
|
4
|
-
|
|
4
|
+
context: SwellRequestContext;
|
|
5
5
|
storeId?: never;
|
|
6
6
|
apiHost?: never;
|
|
7
7
|
accessToken?: never;
|
|
@@ -9,7 +9,7 @@ export type BackendOptions = {
|
|
|
9
9
|
appId?: never;
|
|
10
10
|
requestId?: never;
|
|
11
11
|
} | ({
|
|
12
|
-
|
|
12
|
+
context?: never;
|
|
13
13
|
storeId: string;
|
|
14
14
|
apiHost: string;
|
|
15
15
|
appId?: string;
|
|
@@ -47,7 +47,7 @@ export interface TransactionOptions {
|
|
|
47
47
|
jitter?: boolean;
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
/**
|
|
50
|
+
/** Backend client from a frontend request context or explicit server credentials. */
|
|
51
51
|
export declare class SwellBackendAPI {
|
|
52
52
|
#private;
|
|
53
53
|
protected get userAgent(): string;
|
|
@@ -59,7 +59,7 @@ export declare class SwellBackendAPI {
|
|
|
59
59
|
/**
|
|
60
60
|
* Invokes an app's private function from server code and resolves to its response payload,
|
|
61
61
|
* without the envelope's status or headers.
|
|
62
|
-
* `appId` is the app identifier from
|
|
62
|
+
* `appId` is the app identifier from the request context (`context.appId`).
|
|
63
63
|
* `method` (default `post`) selects the function's handler. GET data reaches the function
|
|
64
64
|
* as query parameters, so its values must be flat strings, numbers or booleans.
|
|
65
65
|
* Throws `SwellError` when the function reports a non-2xx status or an error.
|
package/dist/backend.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireString, validateUrl } from './context.js';
|
|
2
2
|
import { SwellError } from './error.js';
|
|
3
3
|
import { validateWorkflowParams } from './workflow.js';
|
|
4
4
|
import { USER_AGENT } from './version.js';
|
|
@@ -17,7 +17,7 @@ function queryParts(query, prefix = '') {
|
|
|
17
17
|
: [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/** Backend client from a frontend request context or explicit server credentials. */
|
|
21
21
|
export class SwellBackendAPI {
|
|
22
22
|
#baseUrl;
|
|
23
23
|
#authorization;
|
|
@@ -37,7 +37,7 @@ export class SwellBackendAPI {
|
|
|
37
37
|
/**
|
|
38
38
|
* Invokes an app's private function from server code and resolves to its response payload,
|
|
39
39
|
* without the envelope's status or headers.
|
|
40
|
-
* `appId` is the app identifier from
|
|
40
|
+
* `appId` is the app identifier from the request context (`context.appId`).
|
|
41
41
|
* `method` (default `post`) selects the function's handler. GET data reaches the function
|
|
42
42
|
* as query parameters, so its values must be flat strings, numbers or booleans.
|
|
43
43
|
* Throws `SwellError` when the function reports a non-2xx status or an error.
|
|
@@ -65,10 +65,12 @@ export class SwellBackendAPI {
|
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
67
|
constructor(options) {
|
|
68
|
-
if (
|
|
69
|
-
throw new Error('
|
|
68
|
+
if ('headers' in options)
|
|
69
|
+
throw new Error('Pass a request context or explicit credentials, not raw headers');
|
|
70
|
+
if (options.context && ['storeId', 'apiHost', 'accessToken', 'secretKey', 'appId', 'requestId'].some(key => key in options)) {
|
|
71
|
+
throw new Error('context and explicit backend credentials are mutually exclusive');
|
|
70
72
|
}
|
|
71
|
-
const config = options.
|
|
73
|
+
const config = options.context ?? options;
|
|
72
74
|
const secretKey = 'secretKey' in config ? config.secretKey : undefined;
|
|
73
75
|
requireString(config.storeId, 'storeId');
|
|
74
76
|
if ((config.accessToken !== undefined) === (secretKey !== undefined))
|
package/dist/context.d.cts
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import type { PublicConfig } from 'swell-js';
|
|
2
|
+
import type { SwellRequestContext } from './request-context.cjs';
|
|
2
3
|
export type HeaderReader = Pick<Headers, 'get'>;
|
|
3
|
-
export interface SwellContext {
|
|
4
|
-
storeId?: string;
|
|
5
|
-
appId?: string;
|
|
6
|
-
environmentId?: string;
|
|
7
|
-
storefrontId?: string;
|
|
8
|
-
accessToken?: string;
|
|
9
|
-
publicKey?: string;
|
|
10
|
-
apiHost?: string;
|
|
11
|
-
adminUrl?: string;
|
|
12
|
-
vaultUrl?: string;
|
|
13
|
-
requestId?: string;
|
|
14
|
-
isLocalDev: boolean;
|
|
15
|
-
}
|
|
16
|
-
/** Parses trusted ingress headers; does not verify their signature. */
|
|
17
|
-
export declare function parseSwellHeaders(headers: HeaderReader): SwellContext;
|
|
18
4
|
export declare function requireString(value: unknown, field: string): asserts value is string;
|
|
19
5
|
export declare function validateUrl(value: unknown, field: string): string;
|
|
20
6
|
/** Projects only public configuration. Deliver it with Cache-Control: private, no-store. */
|
|
21
|
-
export declare function getStorefrontConfig(
|
|
7
|
+
export declare function getStorefrontConfig(context: SwellRequestContext): PublicConfig;
|
package/dist/context.d.ts
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import type { PublicConfig } from 'swell-js';
|
|
2
|
+
import type { SwellRequestContext } from './request-context.js';
|
|
2
3
|
export type HeaderReader = Pick<Headers, 'get'>;
|
|
3
|
-
export interface SwellContext {
|
|
4
|
-
storeId?: string;
|
|
5
|
-
appId?: string;
|
|
6
|
-
environmentId?: string;
|
|
7
|
-
storefrontId?: string;
|
|
8
|
-
accessToken?: string;
|
|
9
|
-
publicKey?: string;
|
|
10
|
-
apiHost?: string;
|
|
11
|
-
adminUrl?: string;
|
|
12
|
-
vaultUrl?: string;
|
|
13
|
-
requestId?: string;
|
|
14
|
-
isLocalDev: boolean;
|
|
15
|
-
}
|
|
16
|
-
/** Parses trusted ingress headers; does not verify their signature. */
|
|
17
|
-
export declare function parseSwellHeaders(headers: HeaderReader): SwellContext;
|
|
18
4
|
export declare function requireString(value: unknown, field: string): asserts value is string;
|
|
19
5
|
export declare function validateUrl(value: unknown, field: string): string;
|
|
20
6
|
/** Projects only public configuration. Deliver it with Cache-Control: private, no-store. */
|
|
21
|
-
export declare function getStorefrontConfig(
|
|
7
|
+
export declare function getStorefrontConfig(context: SwellRequestContext): PublicConfig;
|
package/dist/context.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/** Parses trusted ingress headers; does not verify their signature. */
|
|
2
|
-
export function parseSwellHeaders(headers) {
|
|
3
|
-
const read = (name) => headers.get(`Swell-${name}`) ?? undefined;
|
|
4
|
-
return {
|
|
5
|
-
storeId: read('Store-Id'), appId: read('App-Id'),
|
|
6
|
-
environmentId: read('Environment-Id'), storefrontId: read('Storefront-Id'),
|
|
7
|
-
accessToken: read('Access-Token'), publicKey: read('Public-Key'),
|
|
8
|
-
apiHost: read('API-Host'), adminUrl: read('Admin-Url'),
|
|
9
|
-
vaultUrl: read('Vault-Url'), requestId: read('Request-ID'),
|
|
10
|
-
isLocalDev: read('Local-Dev') === 'true',
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
1
|
export function requireString(value, field) {
|
|
14
2
|
if (typeof value !== 'string' || !value.trim())
|
|
15
3
|
throw new Error(`Missing or invalid ${field}`);
|
|
@@ -29,8 +17,7 @@ export function validateUrl(value, field) {
|
|
|
29
17
|
return url.href.replace(/\/$/, '');
|
|
30
18
|
}
|
|
31
19
|
/** Projects only public configuration. Deliver it with Cache-Control: private, no-store. */
|
|
32
|
-
export function getStorefrontConfig(
|
|
33
|
-
const context = parseSwellHeaders(headers);
|
|
20
|
+
export function getStorefrontConfig(context) {
|
|
34
21
|
requireString(context.storeId, 'storeId');
|
|
35
22
|
requireString(context.publicKey, 'publicKey');
|
|
36
23
|
return {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import './guard.cjs';
|
|
2
|
-
export {
|
|
3
|
-
export type { HeaderReader
|
|
2
|
+
export { getStorefrontConfig } from './context.cjs';
|
|
3
|
+
export type { HeaderReader } from './context.cjs';
|
|
4
|
+
export { verifySwellContext } from './request-context.cjs';
|
|
5
|
+
export type { SwellRequestContext, SwellHeadersEnv, VerifySwellContextOptions } from './request-context.cjs';
|
|
4
6
|
export { SwellBackendAPI } from './backend.cjs';
|
|
5
7
|
export type { BackendOptions, SwellCollection, SwellData, TransactionOperation, TransactionOptions } from './backend.cjs';
|
|
6
8
|
export { SwellError } from './error.cjs';
|
|
7
9
|
export type { SwellErrorOptions } from './error.cjs';
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
10
|
+
export { requireStoreUser } from './store-user.cjs';
|
|
11
|
+
export type { StoreUser } from './store-user.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import './guard.js';
|
|
2
|
-
export {
|
|
3
|
-
export type { HeaderReader
|
|
2
|
+
export { getStorefrontConfig } from './context.js';
|
|
3
|
+
export type { HeaderReader } from './context.js';
|
|
4
|
+
export { verifySwellContext } from './request-context.js';
|
|
5
|
+
export type { SwellRequestContext, SwellHeadersEnv, VerifySwellContextOptions } from './request-context.js';
|
|
4
6
|
export { SwellBackendAPI } from './backend.js';
|
|
5
7
|
export type { BackendOptions, SwellCollection, SwellData, TransactionOperation, TransactionOptions } from './backend.js';
|
|
6
8
|
export { SwellError } from './error.js';
|
|
7
9
|
export type { SwellErrorOptions } from './error.js';
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
10
|
+
export { requireStoreUser } from './store-user.js';
|
|
11
|
+
export type { StoreUser } from './store-user.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import './guard.js';
|
|
2
|
-
export {
|
|
2
|
+
export { getStorefrontConfig } from './context.js';
|
|
3
|
+
export { verifySwellContext } from './request-context.js';
|
|
3
4
|
export { SwellBackendAPI } from './backend.js';
|
|
4
5
|
export { SwellError } from './error.js';
|
|
5
|
-
export {
|
|
6
|
+
export { requireStoreUser } from './store-user.js';
|
package/dist/jwks.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getVerificationKey(url: string, kid: string): Promise<CryptoKey>;
|
package/dist/jwks.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getVerificationKey(url: string, kid: string): Promise<CryptoKey>;
|
package/dist/jwks.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { SwellError } from './error.js';
|
|
2
|
+
const CACHE_TTL = 300_000;
|
|
3
|
+
const REFRESH_INTERVAL = 30_000;
|
|
4
|
+
const FAILURE_RETRY_INTERVAL = 1000;
|
|
5
|
+
const MAX_URLS = 8;
|
|
6
|
+
// Public keys only: no tokens, credentials, identities or request results.
|
|
7
|
+
const keySets = new Map();
|
|
8
|
+
async function refresh(url, entry) {
|
|
9
|
+
entry.refreshAfter = Date.now() + REFRESH_INTERVAL;
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(5000) });
|
|
12
|
+
if (!response.ok)
|
|
13
|
+
throw new Error('JWKS request failed');
|
|
14
|
+
const body = await response.json();
|
|
15
|
+
if (!Array.isArray(body?.keys))
|
|
16
|
+
throw new Error('Invalid JWKS');
|
|
17
|
+
const keys = new Map();
|
|
18
|
+
for (const jwk of body.keys) {
|
|
19
|
+
if (!jwk || jwk.kty !== 'EC' || jwk.crv !== 'P-256' || (jwk.alg && jwk.alg !== 'ES256') || (jwk.use && jwk.use !== 'sig'))
|
|
20
|
+
continue;
|
|
21
|
+
if (typeof jwk.kid !== 'string' || !jwk.kid || keys.has(jwk.kid) || jwk.d)
|
|
22
|
+
throw new Error('Invalid JWKS key');
|
|
23
|
+
keys.set(jwk.kid, await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']));
|
|
24
|
+
}
|
|
25
|
+
if (!keys.size)
|
|
26
|
+
throw new Error('Empty JWKS');
|
|
27
|
+
entry.keys = keys;
|
|
28
|
+
entry.expiresAt = Date.now() + CACHE_TTL;
|
|
29
|
+
entry.error = undefined;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
entry.refreshAfter = Date.now() + FAILURE_RETRY_INTERVAL;
|
|
33
|
+
entry.error = new SwellError('Swell verification keys unavailable', { status: 503, code: 'swell_jwks_unavailable' });
|
|
34
|
+
throw entry.error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function getVerificationKey(url, kid) {
|
|
38
|
+
let entry = keySets.get(url);
|
|
39
|
+
if (!entry) {
|
|
40
|
+
if (keySets.size >= MAX_URLS)
|
|
41
|
+
keySets.delete(keySets.keys().next().value);
|
|
42
|
+
entry = { keys: new Map(), expiresAt: 0, refreshAfter: 0 };
|
|
43
|
+
keySets.set(url, entry);
|
|
44
|
+
}
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const cached = entry.keys.get(kid);
|
|
47
|
+
if (cached && now < entry.expiresAt)
|
|
48
|
+
return cached;
|
|
49
|
+
if (entry.pending) {
|
|
50
|
+
await entry.pending;
|
|
51
|
+
}
|
|
52
|
+
else if (now >= entry.expiresAt || !entry.keys.has(kid)) {
|
|
53
|
+
if (now >= entry.refreshAfter) {
|
|
54
|
+
entry.pending = refresh(url, entry);
|
|
55
|
+
try {
|
|
56
|
+
await entry.pending;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
entry.pending = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (now >= entry.expiresAt) {
|
|
63
|
+
throw entry.error ?? new SwellError('Swell verification keys unavailable', { status: 503, code: 'swell_jwks_unavailable' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const key = entry.keys.get(kid);
|
|
67
|
+
if (!key)
|
|
68
|
+
throw new SwellError('Unknown Swell signing key', { status: 401, code: 'invalid_swell_context' });
|
|
69
|
+
return key;
|
|
70
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HeaderReader } from './context.cjs';
|
|
2
|
+
import type { StoreUser } from './store-user.cjs';
|
|
3
|
+
export interface SwellHeadersEnv {
|
|
4
|
+
SWELL_VERIFY_HEADERS?: string;
|
|
5
|
+
SWELL_HEADERS_JWKS_URL?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface VerifySwellContextOptions {
|
|
8
|
+
/** Explicit Worker bindings or server configuration. Omit to read process.env when available. */
|
|
9
|
+
env?: SwellHeadersEnv;
|
|
10
|
+
/** Expected destination IDs from trusted app configuration, never incoming headers. */
|
|
11
|
+
appId?: string;
|
|
12
|
+
storeId?: string;
|
|
13
|
+
/** Trusted vault override; the unsigned Swell-Vault-Url header is not used. */
|
|
14
|
+
vaultUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Request-local server data. Only getStorefrontConfig's projection may be sent to the browser. */
|
|
17
|
+
export interface SwellRequestContext {
|
|
18
|
+
readonly storeId: string;
|
|
19
|
+
readonly appId: string;
|
|
20
|
+
readonly installationId: string;
|
|
21
|
+
readonly environmentId?: string;
|
|
22
|
+
readonly storefrontId?: string;
|
|
23
|
+
readonly apiHost: string;
|
|
24
|
+
readonly adminUrl: string;
|
|
25
|
+
readonly accessToken?: string;
|
|
26
|
+
readonly publicKey?: string;
|
|
27
|
+
readonly requestId?: string;
|
|
28
|
+
readonly vaultUrl?: string;
|
|
29
|
+
readonly storeUser: Readonly<StoreUser> | null;
|
|
30
|
+
/** False only when trusted runtime configuration explicitly disables verification. */
|
|
31
|
+
readonly signatureVerified: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves Swell-Context once per request. Uses ES256 and a pinned JWKS endpoint;
|
|
35
|
+
* never discovers keys from the request. No fallback to plain headers or dashboard cookies.
|
|
36
|
+
*/
|
|
37
|
+
export declare function verifySwellContext(headers: HeaderReader, options?: VerifySwellContextOptions): Promise<SwellRequestContext>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HeaderReader } from './context.js';
|
|
2
|
+
import type { StoreUser } from './store-user.js';
|
|
3
|
+
export interface SwellHeadersEnv {
|
|
4
|
+
SWELL_VERIFY_HEADERS?: string;
|
|
5
|
+
SWELL_HEADERS_JWKS_URL?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface VerifySwellContextOptions {
|
|
8
|
+
/** Explicit Worker bindings or server configuration. Omit to read process.env when available. */
|
|
9
|
+
env?: SwellHeadersEnv;
|
|
10
|
+
/** Expected destination IDs from trusted app configuration, never incoming headers. */
|
|
11
|
+
appId?: string;
|
|
12
|
+
storeId?: string;
|
|
13
|
+
/** Trusted vault override; the unsigned Swell-Vault-Url header is not used. */
|
|
14
|
+
vaultUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Request-local server data. Only getStorefrontConfig's projection may be sent to the browser. */
|
|
17
|
+
export interface SwellRequestContext {
|
|
18
|
+
readonly storeId: string;
|
|
19
|
+
readonly appId: string;
|
|
20
|
+
readonly installationId: string;
|
|
21
|
+
readonly environmentId?: string;
|
|
22
|
+
readonly storefrontId?: string;
|
|
23
|
+
readonly apiHost: string;
|
|
24
|
+
readonly adminUrl: string;
|
|
25
|
+
readonly accessToken?: string;
|
|
26
|
+
readonly publicKey?: string;
|
|
27
|
+
readonly requestId?: string;
|
|
28
|
+
readonly vaultUrl?: string;
|
|
29
|
+
readonly storeUser: Readonly<StoreUser> | null;
|
|
30
|
+
/** False only when trusted runtime configuration explicitly disables verification. */
|
|
31
|
+
readonly signatureVerified: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves Swell-Context once per request. Uses ES256 and a pinned JWKS endpoint;
|
|
35
|
+
* never discovers keys from the request. No fallback to plain headers or dashboard cookies.
|
|
36
|
+
*/
|
|
37
|
+
export declare function verifySwellContext(headers: HeaderReader, options?: VerifySwellContextOptions): Promise<SwellRequestContext>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { requireString, validateUrl } from './context.js';
|
|
2
|
+
import { SwellError } from './error.js';
|
|
3
|
+
import { getVerificationKey } from './jwks.js';
|
|
4
|
+
function invalidContext() {
|
|
5
|
+
return new SwellError('Missing or invalid Swell context', { status: 401, code: 'invalid_swell_context' });
|
|
6
|
+
}
|
|
7
|
+
function decode(value) {
|
|
8
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
9
|
+
throw invalidContext();
|
|
10
|
+
return Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), char => char.charCodeAt(0));
|
|
11
|
+
}
|
|
12
|
+
function object(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
14
|
+
throw invalidContext();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolves Swell-Context once per request. Uses ES256 and a pinned JWKS endpoint;
|
|
18
|
+
* never discovers keys from the request. No fallback to plain headers or dashboard cookies.
|
|
19
|
+
*/
|
|
20
|
+
export async function verifySwellContext(headers, options = {}) {
|
|
21
|
+
const runtime = globalThis;
|
|
22
|
+
const env = options.env ?? runtime.process?.env ?? {};
|
|
23
|
+
const verify = env.SWELL_VERIFY_HEADERS !== 'false';
|
|
24
|
+
const jwksUrl = validateUrl(env.SWELL_HEADERS_JWKS_URL ?? 'https://swell.store/.well-known/jwks.json', 'SWELL_HEADERS_JWKS_URL');
|
|
25
|
+
for (const field of ['appId', 'storeId']) {
|
|
26
|
+
if (options[field] !== undefined)
|
|
27
|
+
requireString(options[field], field);
|
|
28
|
+
}
|
|
29
|
+
const vaultUrl = options.vaultUrl === undefined ? undefined : validateUrl(options.vaultUrl, 'vaultUrl');
|
|
30
|
+
const token = headers.get('Swell-Context');
|
|
31
|
+
if (!token || token.length > 16_384)
|
|
32
|
+
throw invalidContext();
|
|
33
|
+
let payload;
|
|
34
|
+
let header;
|
|
35
|
+
let signature;
|
|
36
|
+
const parts = token.split('.');
|
|
37
|
+
try {
|
|
38
|
+
if (parts.length !== 3)
|
|
39
|
+
throw invalidContext();
|
|
40
|
+
header = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(decode(parts[0])));
|
|
41
|
+
payload = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(decode(parts[1])));
|
|
42
|
+
object(header);
|
|
43
|
+
object(payload);
|
|
44
|
+
signature = decode(parts[2]);
|
|
45
|
+
if (header.alg !== 'ES256' || typeof header.kid !== 'string' || !header.kid || header.crit !== undefined || header.b64 !== undefined || signature.length !== 64)
|
|
46
|
+
throw invalidContext();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw invalidContext();
|
|
50
|
+
}
|
|
51
|
+
if (verify) {
|
|
52
|
+
const key = await getVerificationKey(jwksUrl, header.kid);
|
|
53
|
+
if (!await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, signature, new TextEncoder().encode(`${parts[0]}.${parts[1]}`)))
|
|
54
|
+
throw invalidContext();
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const now = Date.now() / 1000;
|
|
58
|
+
// The issuer controls token lifetime; allow five seconds for clock skew.
|
|
59
|
+
if (typeof payload.iat !== 'number' || !Number.isFinite(payload.iat) || payload.iat > now + 5 ||
|
|
60
|
+
typeof payload.exp !== 'number' || !Number.isFinite(payload.exp) || payload.exp <= now - 5 ||
|
|
61
|
+
payload.exp <= payload.iat)
|
|
62
|
+
throw invalidContext();
|
|
63
|
+
if (payload.nbf !== undefined && (typeof payload.nbf !== 'number' || !Number.isFinite(payload.nbf) || payload.nbf > now + 5))
|
|
64
|
+
throw invalidContext();
|
|
65
|
+
for (const field of ['store_id', 'app_id', 'installation_id'])
|
|
66
|
+
requireString(payload[field], field);
|
|
67
|
+
if (payload.aud !== payload.app_id || (options.appId !== undefined && payload.aud !== options.appId) ||
|
|
68
|
+
(options.storeId !== undefined && payload.store_id !== options.storeId))
|
|
69
|
+
throw invalidContext();
|
|
70
|
+
for (const field of ['environment_id', 'storefront_id']) {
|
|
71
|
+
if (payload[field] != null)
|
|
72
|
+
requireString(payload[field], field);
|
|
73
|
+
}
|
|
74
|
+
let storeUser = null;
|
|
75
|
+
if (payload.admin !== null) {
|
|
76
|
+
object(payload.admin);
|
|
77
|
+
requireString(payload.admin.user_id, 'admin.user_id');
|
|
78
|
+
storeUser = Object.freeze({ userId: payload.admin.user_id, storeId: payload.store_id });
|
|
79
|
+
}
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
storeId: payload.store_id, appId: payload.app_id,
|
|
82
|
+
installationId: payload.installation_id,
|
|
83
|
+
environmentId: payload.environment_id ?? undefined,
|
|
84
|
+
storefrontId: payload.storefront_id ?? undefined,
|
|
85
|
+
apiHost: validateUrl(payload.api_host, 'api_host'), adminUrl: validateUrl(payload.admin_url, 'admin_url'),
|
|
86
|
+
accessToken: headers.get('Swell-Access-Token') ?? undefined,
|
|
87
|
+
publicKey: headers.get('Swell-Public-Key') ?? undefined,
|
|
88
|
+
requestId: headers.get('Swell-Request-ID') ?? undefined,
|
|
89
|
+
vaultUrl, storeUser, signatureVerified: verify,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw invalidContext();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SwellRequestContext } from './request-context.cjs';
|
|
2
|
+
export interface StoreUser {
|
|
3
|
+
readonly userId: string;
|
|
4
|
+
readonly storeId: string;
|
|
5
|
+
}
|
|
6
|
+
/** Identity only; any dashboard role counts. The proxy owns write-origin checks; the app owns permissions. */
|
|
7
|
+
export declare function requireStoreUser(context: SwellRequestContext): StoreUser;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SwellRequestContext } from './request-context.js';
|
|
2
|
+
export interface StoreUser {
|
|
3
|
+
readonly userId: string;
|
|
4
|
+
readonly storeId: string;
|
|
5
|
+
}
|
|
6
|
+
/** Identity only; any dashboard role counts. The proxy owns write-origin checks; the app owns permissions. */
|
|
7
|
+
export declare function requireStoreUser(context: SwellRequestContext): StoreUser;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SwellError } from './error.js';
|
|
2
|
+
/** Identity only; any dashboard role counts. The proxy owns write-origin checks; the app owns permissions. */
|
|
3
|
+
export function requireStoreUser(context) {
|
|
4
|
+
if (!context.storeUser)
|
|
5
|
+
throw new SwellError('Store user required', { status: 401, code: 'store_user_required' });
|
|
6
|
+
return context.storeUser;
|
|
7
|
+
}
|
package/dist/version.d.cts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const USER_AGENT = "swell-apps-sdk/2.0.0-alpha.
|
|
1
|
+
export declare const USER_AGENT = "swell-apps-sdk/2.0.0-alpha.2";
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const USER_AGENT = "swell-apps-sdk/2.0.0-alpha.
|
|
1
|
+
export declare const USER_AGENT = "swell-apps-sdk/2.0.0-alpha.2";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Replaced with the package version by the build.
|
|
2
|
-
export const USER_AGENT = 'swell-apps-sdk/2.0.0-alpha.
|
|
2
|
+
export const USER_AGENT = 'swell-apps-sdk/2.0.0-alpha.2';
|
package/package.json
CHANGED
package/dist/staff.d.cts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { HeaderReader } from './context.cjs';
|
|
2
|
-
export interface StaffIdentity {
|
|
3
|
-
userId: string;
|
|
4
|
-
storeId: string;
|
|
5
|
-
}
|
|
6
|
-
export interface StaffOptions {
|
|
7
|
-
headers: HeaderReader;
|
|
8
|
-
method: string;
|
|
9
|
-
/** Trusted app/iframe origin, never an unchecked forwarded-host value. */
|
|
10
|
-
origin: string;
|
|
11
|
-
cookies: {
|
|
12
|
-
get(name: string): string | undefined;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/** Verifies store staff identity only. The application still owns authorization policy. */
|
|
16
|
-
export declare function requireStaff({ headers, method, origin, cookies }: StaffOptions): Promise<StaffIdentity>;
|
package/dist/staff.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { HeaderReader } from './context.js';
|
|
2
|
-
export interface StaffIdentity {
|
|
3
|
-
userId: string;
|
|
4
|
-
storeId: string;
|
|
5
|
-
}
|
|
6
|
-
export interface StaffOptions {
|
|
7
|
-
headers: HeaderReader;
|
|
8
|
-
method: string;
|
|
9
|
-
/** Trusted app/iframe origin, never an unchecked forwarded-host value. */
|
|
10
|
-
origin: string;
|
|
11
|
-
cookies: {
|
|
12
|
-
get(name: string): string | undefined;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/** Verifies store staff identity only. The application still owns authorization policy. */
|
|
16
|
-
export declare function requireStaff({ headers, method, origin, cookies }: StaffOptions): Promise<StaffIdentity>;
|
package/dist/staff.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { parseSwellHeaders, requireString, validateUrl } from './context.js';
|
|
2
|
-
import { SwellError } from './error.js';
|
|
3
|
-
import { USER_AGENT } from './version.js';
|
|
4
|
-
/** Verifies store staff identity only. The application still owns authorization policy. */
|
|
5
|
-
export async function requireStaff({ headers, method, origin, cookies }) {
|
|
6
|
-
const { storeId, adminUrl } = parseSwellHeaders(headers);
|
|
7
|
-
requireString(storeId, 'storeId');
|
|
8
|
-
const url = validateUrl(adminUrl, 'adminUrl');
|
|
9
|
-
requireString(method, 'method');
|
|
10
|
-
if (method.toUpperCase() !== 'GET') {
|
|
11
|
-
const expected = validateUrl(origin, 'origin');
|
|
12
|
-
const supplied = headers.get('Origin');
|
|
13
|
-
if (new URL(expected).origin !== origin)
|
|
14
|
-
throw new Error('origin must be an absolute app origin');
|
|
15
|
-
if (supplied !== origin || (headers.get('Sec-Fetch-Site') !== null && headers.get('Sec-Fetch-Site') !== 'same-origin')) {
|
|
16
|
-
throw new SwellError('Staff request origin rejected', { status: 403 });
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
const sessionId = cookies.get('_swell_admin_session');
|
|
20
|
-
if (!sessionId)
|
|
21
|
-
throw new SwellError('Staff session required', { status: 401 });
|
|
22
|
-
const response = await fetch(new URL('/admin/api/session', url), {
|
|
23
|
-
headers: { 'X-Session': sessionId, 'User-Agent': USER_AGENT }, redirect: 'manual',
|
|
24
|
-
});
|
|
25
|
-
if (response.status === 401 || response.status === 403)
|
|
26
|
-
throw new SwellError('Invalid staff session', { status: 401 });
|
|
27
|
-
if (!response.ok)
|
|
28
|
-
throw new SwellError('Staff session verification failed', { status: response.status });
|
|
29
|
-
const session = await response.json();
|
|
30
|
-
if (typeof session?.user_id !== 'string' || !session.user_id || session.client_id !== storeId)
|
|
31
|
-
throw new SwellError('Invalid staff session', { status: 401 });
|
|
32
|
-
return { userId: session.user_id, storeId };
|
|
33
|
-
}
|