shopify-static-offline-auth 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -100
- package/dist/cjs/app.js +3 -3
- package/dist/cjs/app.js.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/storage.js +6 -6
- package/dist/cjs/storage.js.map +1 -1
- package/dist/cjs/stores.js +8 -4
- package/dist/cjs/stores.js.map +1 -1
- package/dist/esm/app.mjs +5 -5
- package/dist/esm/app.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/storage.mjs +6 -6
- package/dist/esm/storage.mjs.map +1 -1
- package/dist/esm/stores.mjs +8 -4
- package/dist/esm/stores.mjs.map +1 -1
- package/dist/ts/index.d.ts +2 -0
- package/dist/ts/storage.d.ts +8 -3
- package/dist/ts/stores.d.ts +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# shopify-static-offline-auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read-only session storage for pre-provisioned, non-expiring offline tokens. Use `StaticSessionStorage` with the official Shopify `shopifyApp()` function in custom-distributed React Router apps, without a session database.
|
|
4
4
|
|
|
5
|
-
This independent package
|
|
5
|
+
This independent package is not an official Shopify package. Shopify still validates incoming JWTs and webhook HMACs; the adapter supplies credentials only for configured stores. The optional `createStaticOfflineShopifyApp()` wrapper remains available for applications using the 0.1.x API.
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
9
|
- Node.js 22 or later.
|
|
10
10
|
- `@shopify/shopify-app-react-router` `^4.0.2`, `@shopify/shopify-api` `^15.1.1`, and `@shopify/shopify-app-session-storage` `^7.0.3`.
|
|
11
11
|
- An embedded app with Dev Dashboard **custom distribution** (`SingleMerchant`).
|
|
12
|
-
- An existing **non-expiring offline token for this same app and each configured store**.
|
|
12
|
+
- An existing **non-expiring offline token for this same app and each configured store**. An opaque token cannot be inspected locally to prove its type, lifetime, or granted scopes.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Public App Store apps, legacy admin-created apps, online tokens, refresh tokens, and automatic token provisioning are outside this package's supported use case. Integration tests cover React Router Admin authentication, webhooks, and background Admin calls. Other framework integrations are not certified.
|
|
15
15
|
|
|
16
|
-
Custom apps can currently use non-expiring tokens. Tokens can still be revoked, including
|
|
16
|
+
Custom apps can currently use non-expiring tokens. Tokens can still be revoked, including when the app is uninstalled or its secret is revoked. See [Shopify access tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens).
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -21,32 +21,46 @@ Custom apps can currently use non-expiring tokens. Tokens can still be revoked,
|
|
|
21
21
|
npm install shopify-static-offline-auth @shopify/shopify-app-react-router@^4.0.2 @shopify/shopify-api@^15.1.1 @shopify/shopify-app-session-storage@^7.0.3
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
## Use with the official shopifyApp
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Set the following **server-only** environment variable or secret. The values below are placeholders, not usable tokens:
|
|
26
|
+
Set a **server-only** JSON environment variable or secret. Scopes are not required on each store record:
|
|
29
27
|
|
|
30
28
|
```dotenv
|
|
31
|
-
|
|
29
|
+
SCOPES=read_products
|
|
30
|
+
SHOPIFY_STATIC_OFFLINE_STORES='{"example.myshopify.com":{"accessToken":"REPLACE_WITH_NON_EXPIRING_OFFLINE_TOKEN"}}'
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
In your application's `app/shopify.server.ts`:
|
|
35
34
|
|
|
36
35
|
```ts
|
|
37
|
-
import {ApiVersion} from '@shopify/shopify-app-react-router/server';
|
|
38
36
|
import {
|
|
39
|
-
|
|
37
|
+
ApiVersion,
|
|
38
|
+
AppDistribution,
|
|
39
|
+
shopifyApp,
|
|
40
|
+
} from '@shopify/shopify-app-react-router/server';
|
|
41
|
+
import {
|
|
42
|
+
StaticSessionStorage,
|
|
40
43
|
parseStaticOfflineStores,
|
|
41
44
|
} from 'shopify-static-offline-auth';
|
|
42
45
|
|
|
43
|
-
const
|
|
46
|
+
const scopes = process.env.SCOPES?.split(',') ?? [];
|
|
47
|
+
|
|
48
|
+
const shopify = shopifyApp({
|
|
44
49
|
apiKey: process.env.SHOPIFY_API_KEY!,
|
|
45
50
|
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
|
|
51
|
+
apiVersion: ApiVersion.July26,
|
|
52
|
+
scopes,
|
|
46
53
|
appUrl: process.env.SHOPIFY_APP_URL!,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
authPathPrefix: '/auth',
|
|
55
|
+
sessionStorage: new StaticSessionStorage({
|
|
56
|
+
stores: parseStaticOfflineStores(
|
|
57
|
+
process.env.SHOPIFY_STATIC_OFFLINE_STORES!,
|
|
58
|
+
),
|
|
59
|
+
scopes,
|
|
60
|
+
}),
|
|
61
|
+
distribution: AppDistribution.SingleMerchant,
|
|
62
|
+
useOnlineTokens: false,
|
|
63
|
+
future: {expiringOfflineAccessTokens: false},
|
|
50
64
|
});
|
|
51
65
|
|
|
52
66
|
export default shopify;
|
|
@@ -56,120 +70,126 @@ export const login = shopify.login;
|
|
|
56
70
|
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
|
|
57
71
|
```
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
Keep the official template's App Bridge/AppProvider, auth catch-all route, `boundary` imports, headers, and route-level `authenticate.admin(request)` calls. The `shopify` object remains the official SDK object. The storage adapter does not replace it or remove its methods.
|
|
60
74
|
|
|
61
|
-
The
|
|
75
|
+
The template's `distribution: AppStore` and `expiringOfflineAccessTokens: true` settings must be adjusted for this custom, non-expiring-token use case as shown above. A storage adapter cannot inspect or change its parent `shopifyApp()` configuration. `customShopDomains` also does not expand the static credential allowlist: credential keys remain canonical `.myshopify.com` domains.
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
### Common scopes and granted permissions
|
|
64
78
|
|
|
65
|
-
`
|
|
79
|
+
`shopifyApp({scopes})` is the common list of permissions requested by your application. `StaticSessionStorage({scopes})` is optional metadata used to populate `Session.scope`. Reuse one array, as above; no per-store scope duplication is required. The adapter cannot automatically read the enclosing `shopifyApp` configuration.
|
|
66
80
|
|
|
67
|
-
|
|
81
|
+
A store record may optionally specify `scopes` when its already-granted permissions differ from the common list. An explicit per-store list, including `[]`, takes precedence. Existing 0.1.x records containing scopes still work. When neither a common list nor a per-store list is supplied, `Session.scope` is `undefined`; the adapter does not invent a grant or fetch one from Shopify. Admin JWT authentication still works with the supported SDK version.
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
These values do **not** grant API permissions or verify a token's actual grant. Shopify enforces the real granted permissions on every API call. Verify permissions when provisioning the token. Changing local scope metadata cannot broaden a grant or turn an expiring token into a permanent token.
|
|
70
84
|
|
|
71
|
-
|
|
72
|
-
const {admin, session, sessionToken, cors, redirect} =
|
|
73
|
-
await authenticate.admin(request);
|
|
74
|
-
const response = await admin.graphql('{ shop { name } }');
|
|
75
|
-
return cors(Response.json(await response.json()));
|
|
76
|
-
```
|
|
85
|
+
### Credential snapshot
|
|
77
86
|
|
|
78
|
-
|
|
87
|
+
The package reads no environment variables itself. You may supply the `stores` object directly from a secret manager. Keys must be exact lowercase `<shop>.myshopify.com` domains without a protocol, path, port, wildcard, or custom storefront domain. At least one store is required. Records accept `accessToken` and optional `scopes` only.
|
|
79
88
|
|
|
80
|
-
The
|
|
89
|
+
The adapter copies and freezes its credential snapshot and returns a fresh `Session` for every read. Changing the original input or a returned session does not change another request's credentials. Restart/redeploy all instances to apply token rotation or store removal.
|
|
81
90
|
|
|
82
|
-
|
|
91
|
+
## Read-only adapter behavior
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
This adapter intentionally cannot satisfy the writable CRUD semantics of a database adapter:
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
| Method / condition | Behavior |
|
|
96
|
+
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `loadSession('offline_<configured shop>')` | Returns a fresh offline session with no expiration or refresh token. |
|
|
98
|
+
| Unknown or online session ID | Throws `StaticOfflineShopNotConfiguredError` rather than returning `undefined`, preventing automatic token exchange. |
|
|
99
|
+
| `findSessionsByShop(shop)` | Returns one fresh session for a configured shop, or `[]`. |
|
|
100
|
+
| `storeSession`, `deleteSession`, `deleteSessions` | Throws `StaticOfflineReadOnlyError`; never reports a write or deletion as successful. |
|
|
101
|
+
| Official Admin client's attempt to persist a rejected token | Throws `StaticOfflineTokenRejectedError` before Shopify redirects into reauthentication. |
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
[webhooks]
|
|
90
|
-
api_version = "2026-10"
|
|
103
|
+
The adapter itself does not authenticate an HTTP request, run `afterAuth`, handle login routing, translate exceptions into HTTP status codes, redact the official SDK's logs, or distinguish webhook topics. The official SDK still validates JWTs and HMACs before looking up sessions. Never expose `unauthenticated.admin` through a public route without your own authorization, and never return a session or token map to a browser.
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
topics = ["app/uninstalled"]
|
|
94
|
-
uri = "/webhooks/app/uninstalled"
|
|
95
|
-
```
|
|
105
|
+
Admin token rejection from `authenticate.admin(...).admin.graphql(...)` is a typed error with this adapter. Webhook and background API clients retain their upstream SDK error behavior (for example, a background `401` is an `HttpResponseError`). Applications can map these errors at their route/job boundary. The optional wrapper below supplies consistent HTTP responses and background token-rejection errors.
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
const {topic, shop, session, admin, payload} =
|
|
99
|
-
await authenticate.webhook(request);
|
|
107
|
+
### Webhooks and uninstall
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
// Perform your application-specific cleanup or operator notification.
|
|
103
|
-
// Remove this shop's configured credential and redeploy all instances.
|
|
104
|
-
return new Response(null, {status: 200});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Normal webhook context is available here. Check session/admin before use.
|
|
108
|
-
return new Response(null, {status: 200});
|
|
109
|
-
```
|
|
109
|
+
Use app-specific TOML webhook subscriptions, and remove template code that writes/deletes session rows. An adapter has no request/topic context: on an `APP_UNINSTALLED` webhook, the official SDK can still return a session from the static snapshot. **Do not use that session for API work.** Handle the uninstall topic first, perform application cleanup, and remove the configured credential through your operational process.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
The wrapper does **not** persist an uninstall tombstone. An installed credential remains in the snapshot until an operator removes it. Shopify's revoked token will be rejected on subsequent API calls. Do not add calls to `deleteSession` or Prisma session cleanup to this webhook.
|
|
114
|
-
|
|
115
|
-
### Background jobs
|
|
111
|
+
For valid deliveries from a store already removed from configuration, acknowledge the lookup error in the webhook route after the official HMAC check:
|
|
116
112
|
|
|
117
113
|
```ts
|
|
118
|
-
import {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
import type {ActionFunctionArgs} from 'react-router';
|
|
115
|
+
import {StaticOfflineShopNotConfiguredError} from 'shopify-static-offline-auth';
|
|
116
|
+
import {authenticate} from '../shopify.server';
|
|
117
|
+
|
|
118
|
+
export async function action({request}: ActionFunctionArgs) {
|
|
119
|
+
let context;
|
|
120
|
+
try {
|
|
121
|
+
context = await authenticate.webhook(request);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof StaticOfflineShopNotConfiguredError) {
|
|
124
|
+
return new Response(null, {status: 200});
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (context.topic === 'APP_UNINSTALLED') {
|
|
130
|
+
// Perform application cleanup; remove the configured token and redeploy.
|
|
131
|
+
// Do not call sessionStorage.deleteSession/deleteSessions or the Admin API.
|
|
132
|
+
return new Response(null, {status: 200});
|
|
126
133
|
}
|
|
127
|
-
|
|
134
|
+
|
|
135
|
+
// Process the normal webhook here.
|
|
136
|
+
return new Response(null, {status: 200});
|
|
128
137
|
}
|
|
129
138
|
```
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
### Login and document headers
|
|
140
|
+
There is no shared uninstall tombstone or invalidation cache. Shopify rejects revoked credentials on subsequent API calls until the configuration is changed.
|
|
134
141
|
|
|
135
|
-
|
|
142
|
+
## Optional React Router wrapper
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
Existing `createStaticOfflineShopifyApp()` consumers continue to work. Per-store scopes are now optional here too: the wrapper uses its app-level scopes as the common default.
|
|
138
145
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
| Permission, GraphQL, throttling, or network error | Preserved from Shopify | Preserved from Shopify |
|
|
146
|
+
```ts
|
|
147
|
+
import {ApiVersion} from '@shopify/shopify-app-react-router/server';
|
|
148
|
+
import {
|
|
149
|
+
createStaticOfflineShopifyApp,
|
|
150
|
+
parseStaticOfflineStores,
|
|
151
|
+
} from 'shopify-static-offline-auth';
|
|
146
152
|
|
|
147
|
-
|
|
153
|
+
const shopify = createStaticOfflineShopifyApp({
|
|
154
|
+
apiKey: process.env.SHOPIFY_API_KEY!,
|
|
155
|
+
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
|
|
156
|
+
appUrl: process.env.SHOPIFY_APP_URL!,
|
|
157
|
+
apiVersion: ApiVersion.July26,
|
|
158
|
+
scopes: process.env.SCOPES?.split(',') ?? [],
|
|
159
|
+
stores: parseStaticOfflineStores(process.env.SHOPIFY_STATIC_OFFLINE_STORES!),
|
|
160
|
+
});
|
|
161
|
+
```
|
|
148
162
|
|
|
149
|
-
|
|
163
|
+
The wrapper exposes `authenticate.admin`, `authenticate.webhook`, `unauthenticated.admin`, `login`, and `addDocumentResponseHeaders`. Its Admin context exposes `admin`, `session`, `sessionToken`, `cors`, and `redirect`.
|
|
150
164
|
|
|
151
|
-
|
|
165
|
+
| Behavior | Adapter with official `shopifyApp` | Optional wrapper |
|
|
166
|
+
| ------------------------------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------------ |
|
|
167
|
+
| Unknown authenticated Admin store | Typed lookup error for the application to handle | `403` response |
|
|
168
|
+
| Valid webhook for an unconfigured store | Typed lookup error for the webhook route to acknowledge | Empty `200` response; processing stops |
|
|
169
|
+
| Uninstall webhook | Application handles the topic before using the returned session | Context has no session or Admin client |
|
|
170
|
+
| Admin token rejected with `401` | Typed invalidation error; no reauthentication redirect | Secret-free `503` with `STATIC_OFFLINE_TOKEN_REJECTED` |
|
|
171
|
+
| Background token rejected with `401` | Upstream `HttpResponseError` | `StaticOfflineTokenRejectedError` |
|
|
172
|
+
| Scope defaults | Pass the common array to the adapter if metadata is needed | App-level scopes are used automatically |
|
|
173
|
+
| Startup configuration checks, login allowlist, debug-log redaction | Application / SDK responsibility | Supplied by the wrapper |
|
|
152
174
|
|
|
153
|
-
|
|
154
|
-
2. Confirm the token's actual granted scopes and read-test it against the intended store. Keep the app's client ID and secret paired with that installation.
|
|
155
|
-
3. Replace the server-side credential and granted-scope metadata.
|
|
156
|
-
4. Redeploy/restart **all** instances. Old processes retain their original snapshot until replaced.
|
|
157
|
-
5. Verify an authenticated request and an Admin API call. Remove revoked or uninstalled stores from configuration.
|
|
175
|
+
The wrapper enforces `SingleMerchant`, offline mode, and non-expiring tokens, and rejects conflicting configuration, separate session storage, `hooks`, `billing`, code-based `webhooks`, custom domains, and global token overrides. Use TOML subscriptions instead. Permission, GraphQL, throttling, and network errors remain SDK errors. Token rejection responses have `Cache-Control: no-store` and contain no upstream body or credentials.
|
|
158
176
|
|
|
159
|
-
|
|
177
|
+
## Migrating and operating
|
|
160
178
|
|
|
161
|
-
|
|
179
|
+
1. Provision and verify a non-expiring offline token for the same app on each allowed store. This package does not acquire tokens; use Shopify's supported [token exchange](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/token-exchange) or authorization-code flow separately.
|
|
180
|
+
2. Keep `shopifyApp()` and replace `new PrismaSessionStorage(prisma)` with `new StaticSessionStorage({stores, scopes})`. Use the custom/offline settings shown above. Existing wrapper consumers may keep their current factory instead.
|
|
181
|
+
3. Move `afterAuth` initialization into an explicit provisioning/deployment step. Pre-provisioned sessions already exist, so the official SDK does not run that hook on first use.
|
|
182
|
+
4. Remove session writes/deletions from uninstall and scope-update webhooks. Acknowledge unknown-store webhooks appropriately and map Admin lookup/token errors in your routes, or use the optional wrapper.
|
|
183
|
+
5. Remove Prisma/database dependencies only if no other application data uses them. Do not delete unrelated database data or migrations.
|
|
184
|
+
6. Rotate tokens and update declared scopes outside the request flow, then redeploy/restart **every** instance. Old processes retain their original snapshot until replaced.
|
|
185
|
+
7. Test authenticated requests and API calls before rollout. Keep the previous deployment/database available for rollback until verification completes.
|
|
162
186
|
|
|
163
|
-
|
|
164
|
-
2. Replace `shopifyApp` with this factory and remove `sessionStorage` and the Prisma session-storage adapter import.
|
|
165
|
-
3. Preserve route-level `authenticate.admin`, App Bridge, headers, and framework `boundary` usage. Remove unsupported `sessionStorage`/`registerWebhooks` exports.
|
|
166
|
-
4. Move `afterAuth` work into an explicit provisioning/deployment step. Use TOML subscriptions for webhooks. This wrapper does not run `afterAuth` on first use or on every request.
|
|
167
|
-
5. Remove session delete/update code from uninstall and scope-update webhooks. Handle configuration changes through your operational process instead; return successful webhook acknowledgements after any application cleanup.
|
|
168
|
-
6. Remove Prisma/database dependencies only if no other application data uses them. Do not delete unrelated database data or migrations.
|
|
169
|
-
7. Test before rollout. Keep the previous deployment and its database available for rollback until verification completes.
|
|
187
|
+
Authentication success means the JWT is valid and its shop is configured. Token validity is checked by Shopify on API calls, not by an extra probe on every authentication request. Actual scope changes can require reauthorization; local scope metadata is not authorization.
|
|
170
188
|
|
|
171
189
|
## Development and release
|
|
172
190
|
|
|
191
|
+
This package lives with the other adapters under `packages/apps/session-storage/`. The existing session-storage CI build/test filters include it.
|
|
192
|
+
|
|
173
193
|
From the monorepo root:
|
|
174
194
|
|
|
175
195
|
```sh
|
|
@@ -180,14 +200,12 @@ pnpm --filter shopify-static-offline-auth test
|
|
|
180
200
|
pnpm --filter shopify-static-offline-auth test:package
|
|
181
201
|
```
|
|
182
202
|
|
|
183
|
-
`test` runs unit tests
|
|
184
|
-
|
|
185
|
-
Only `dist`, `README.md`, `LICENSE.md`, and package metadata are published. Source tests, local secrets, adapters' dependencies, and workspace configuration are not bundled. Do not use the monorepo-wide release command to publish this independent package.
|
|
203
|
+
`test` runs unit tests and native Node integration contracts for both the public adapter and optional wrapper. HTTP is mocked; unexpected token exchange, refresh, and network calls fail the tests. `test:package` packs the package, checks its file allowlist, installs it in an isolated consumer with npm registry releases (React Router 4.0.2 / API 15.1.1), reruns the contracts, and checks ESM, CommonJS, and TypeScript. No real stores are called.
|
|
186
204
|
|
|
187
|
-
After
|
|
205
|
+
Only `dist`, `README.md`, `LICENSE.md`, and package metadata are published. Do not use the monorepo-wide release command for this independent package. After validation, inspect `npm pack --dry-run` in this package directory and publish only this package using an authorized npm account:
|
|
188
206
|
|
|
189
207
|
```sh
|
|
190
208
|
npm publish --access public
|
|
191
209
|
```
|
|
192
210
|
|
|
193
|
-
Building or packing does not publish a version. Verify the published version and
|
|
211
|
+
Building or packing does not publish a version. Verify the published version, metadata, and integrity on the npm registry afterward.
|
package/dist/cjs/app.js
CHANGED
|
@@ -72,14 +72,14 @@ function validateConfig(config) {
|
|
|
72
72
|
}
|
|
73
73
|
function createStaticOfflineShopifyApp(config) {
|
|
74
74
|
validateConfig(config);
|
|
75
|
-
const stores$1 = stores.validateStores(config.stores);
|
|
76
75
|
const scopes = stores.validateScopes(config.scopes ?? []);
|
|
76
|
+
const stores$1 = stores.validateStores(config.stores, scopes);
|
|
77
77
|
for (const credential of Object.values(stores$1)) {
|
|
78
|
-
if (!new shopifyApi.AuthScopes([...credential.scopes]).has(scopes))
|
|
78
|
+
if (!new shopifyApi.AuthScopes([...(credential.scopes ?? [])]).has(scopes))
|
|
79
79
|
throw new errors.StaticOfflineConfigurationError('A configured store is missing required app scopes. Update the grant and provision its token before starting.');
|
|
80
80
|
}
|
|
81
81
|
const { stores: _stores, ...appConfig } = config;
|
|
82
|
-
const storage$1 = new storage.
|
|
82
|
+
const storage$1 = new storage.StaticSessionStorage({ stores: stores$1, scopes });
|
|
83
83
|
const app = server.shopifyApp({
|
|
84
84
|
...appConfig,
|
|
85
85
|
scopes,
|
package/dist/cjs/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sources":["../../../src/app.ts"],"sourcesContent":["import {AuthScopes, HttpResponseError} from '@shopify/shopify-api';\nimport {\n AppDistribution,\n LoginErrorType,\n shopifyApp,\n} from '@shopify/shopify-app-react-router/server';\nimport type {\n AdminApiContext,\n UnauthenticatedAdminContext,\n} from '@shopify/shopify-app-react-router/server';\n\nimport {\n StaticOfflineConfigurationError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {redactedLogger} from './logger';\nimport {StaticOfflineSessionStorage} from './storage';\nimport {isShopDomain, validateScopes, validateStores} from './stores';\nimport type {StaticOfflineAppConfig, StaticOfflineShopifyApp} from './types';\n\nfunction forbidden(): Response {\n return Response.json(\n {error: 'STATIC_OFFLINE_SHOP_NOT_CONFIGURED'},\n {status: 403, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction tokenRejectedResponse(): Response {\n return Response.json(\n {\n error: 'STATIC_OFFLINE_TOKEN_REJECTED',\n message: 'Configured Shopify credentials require operator attention.',\n },\n {status: 503, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction wrapAdmin(\n admin: AdminApiContext,\n onRejected: () => never,\n): AdminApiContext {\n // Shopify clears this context's session on a 401. Preserve the error category\n // for later calls on the same client, without changing another request's state.\n let rejected = false;\n const graphql: AdminApiContext['graphql'] = async (operation, options) => {\n if (rejected) return onRejected();\n try {\n return await admin.graphql(operation, options);\n } catch (error) {\n if (\n error instanceof StaticOfflineTokenRejectedError ||\n (error instanceof HttpResponseError && error.response.code === 401) ||\n (error instanceof Response && error.status === 401)\n ) {\n rejected = true;\n return onRejected();\n }\n throw error;\n }\n };\n return {graphql};\n}\n\nfunction validateConfig(config: StaticOfflineAppConfig): void {\n if (!config || typeof config !== 'object')\n throw new StaticOfflineConfigurationError(\n 'An app configuration is required.',\n );\n for (const key of [\n 'sessionStorage',\n 'hooks',\n 'billing',\n 'webhooks',\n 'adminApiAccessToken',\n 'privateAppStorefrontAccessToken',\n 'customShopDomains',\n 'isCustomStoreApp',\n 'isEmbeddedApp',\n ]) {\n if (key in config)\n throw new StaticOfflineConfigurationError(\n `The ${key} option is unsupported in static offline mode.`,\n );\n }\n if (\n (config.distribution !== undefined &&\n config.distribution !== AppDistribution.SingleMerchant) ||\n (config.useOnlineTokens !== undefined && config.useOnlineTokens !== false)\n ) {\n throw new StaticOfflineConfigurationError(\n 'Static offline mode requires SingleMerchant distribution and useOnlineTokens: false.',\n );\n }\n if (\n config.future !== undefined &&\n (!config.future ||\n typeof config.future !== 'object' ||\n Array.isArray(config.future) ||\n Object.keys(config.future).some(\n (key) => key !== 'expiringOfflineAccessTokens',\n ) ||\n (config.future.expiringOfflineAccessTokens !== undefined &&\n config.future.expiringOfflineAccessTokens !== false))\n ) {\n throw new StaticOfflineConfigurationError(\n 'Only expiringOfflineAccessTokens: false is supported.',\n );\n }\n}\n\nexport function createStaticOfflineShopifyApp(\n config: StaticOfflineAppConfig,\n): StaticOfflineShopifyApp {\n validateConfig(config);\n const stores = validateStores(config.stores);\n const scopes = validateScopes(config.scopes ?? []);\n for (const credential of Object.values(stores)) {\n if (!new AuthScopes([...credential.scopes]).has(scopes))\n throw new StaticOfflineConfigurationError(\n 'A configured store is missing required app scopes. Update the grant and provision its token before starting.',\n );\n }\n const {stores: _stores, ...appConfig} = config;\n const storage = new StaticOfflineSessionStorage(stores);\n const app = shopifyApp({\n ...appConfig,\n scopes,\n logger: redactedLogger({...config, stores}),\n distribution: AppDistribution.SingleMerchant,\n useOnlineTokens: false,\n future: {expiringOfflineAccessTokens: false},\n sessionStorage: storage,\n });\n\n return {\n authenticate: {\n async admin(request) {\n try {\n const context = await app.authenticate.admin(request);\n const {session, sessionToken, cors, redirect} = context;\n return {\n session,\n sessionToken,\n cors,\n redirect,\n admin: wrapAdmin(context.admin, () => {\n throw cors(tokenRejectedResponse());\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw forbidden();\n throw error;\n }\n },\n async webhook(request) {\n try {\n // Shopify verifies the raw body HMAC before looking up the session.\n const context = await app.authenticate.webhook(request);\n if (context.topic === 'APP_UNINSTALLED')\n return {...context, session: undefined, admin: undefined};\n if (!context.admin) return context;\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw tokenRejectedResponse();\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw new Response(null, {status: 200});\n throw error;\n }\n },\n },\n unauthenticated: {\n async admin(shop): Promise<UnauthenticatedAdminContext> {\n if (!storage.hasShop(shop))\n throw new StaticOfflineShopNotConfiguredError();\n const context = await app.unauthenticated.admin(shop);\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw new StaticOfflineTokenRejectedError();\n }),\n };\n },\n },\n async login(request) {\n const url = new URL(request.url);\n let shop: unknown = url.searchParams.get('shop');\n if (!shop && request.method === 'GET') return {};\n if (!shop) shop = (await request.clone().formData()).get('shop');\n if (!shop) return {shop: LoginErrorType.MissingShop};\n if (typeof shop !== 'string') return {shop: LoginErrorType.InvalidShop};\n // Match the official login form's short-name/URL convenience, then allowlist.\n shop = shop.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');\n if (typeof shop === 'string' && !shop.includes('.'))\n shop = `${shop}.myshopify.com`;\n if (!isShopDomain(shop)) return {shop: LoginErrorType.InvalidShop};\n if (!storage.hasShop(shop)) throw forbidden();\n return app.login(request);\n },\n addDocumentResponseHeaders: app.addDocumentResponseHeaders,\n };\n}\n"],"names":["StaticOfflineTokenRejectedError","HttpResponseError","StaticOfflineConfigurationError","AppDistribution","stores","validateStores","validateScopes","AuthScopes","storage","StaticOfflineSessionStorage","shopifyApp","redactedLogger","StaticOfflineShopNotConfiguredError","LoginErrorType","isShopDomain"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app.js","sources":["../../../src/app.ts"],"sourcesContent":["import {AuthScopes, HttpResponseError} from '@shopify/shopify-api';\nimport {\n AppDistribution,\n LoginErrorType,\n shopifyApp,\n} from '@shopify/shopify-app-react-router/server';\nimport type {\n AdminApiContext,\n UnauthenticatedAdminContext,\n} from '@shopify/shopify-app-react-router/server';\n\nimport {\n StaticOfflineConfigurationError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {redactedLogger} from './logger';\nimport {StaticSessionStorage} from './storage';\nimport {isShopDomain, validateScopes, validateStores} from './stores';\nimport type {StaticOfflineAppConfig, StaticOfflineShopifyApp} from './types';\n\nfunction forbidden(): Response {\n return Response.json(\n {error: 'STATIC_OFFLINE_SHOP_NOT_CONFIGURED'},\n {status: 403, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction tokenRejectedResponse(): Response {\n return Response.json(\n {\n error: 'STATIC_OFFLINE_TOKEN_REJECTED',\n message: 'Configured Shopify credentials require operator attention.',\n },\n {status: 503, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction wrapAdmin(\n admin: AdminApiContext,\n onRejected: () => never,\n): AdminApiContext {\n // Shopify clears this context's session on a 401. Preserve the error category\n // for later calls on the same client, without changing another request's state.\n let rejected = false;\n const graphql: AdminApiContext['graphql'] = async (operation, options) => {\n if (rejected) return onRejected();\n try {\n return await admin.graphql(operation, options);\n } catch (error) {\n if (\n error instanceof StaticOfflineTokenRejectedError ||\n (error instanceof HttpResponseError && error.response.code === 401) ||\n (error instanceof Response && error.status === 401)\n ) {\n rejected = true;\n return onRejected();\n }\n throw error;\n }\n };\n return {graphql};\n}\n\nfunction validateConfig(config: StaticOfflineAppConfig): void {\n if (!config || typeof config !== 'object')\n throw new StaticOfflineConfigurationError(\n 'An app configuration is required.',\n );\n for (const key of [\n 'sessionStorage',\n 'hooks',\n 'billing',\n 'webhooks',\n 'adminApiAccessToken',\n 'privateAppStorefrontAccessToken',\n 'customShopDomains',\n 'isCustomStoreApp',\n 'isEmbeddedApp',\n ]) {\n if (key in config)\n throw new StaticOfflineConfigurationError(\n `The ${key} option is unsupported in static offline mode.`,\n );\n }\n if (\n (config.distribution !== undefined &&\n config.distribution !== AppDistribution.SingleMerchant) ||\n (config.useOnlineTokens !== undefined && config.useOnlineTokens !== false)\n ) {\n throw new StaticOfflineConfigurationError(\n 'Static offline mode requires SingleMerchant distribution and useOnlineTokens: false.',\n );\n }\n if (\n config.future !== undefined &&\n (!config.future ||\n typeof config.future !== 'object' ||\n Array.isArray(config.future) ||\n Object.keys(config.future).some(\n (key) => key !== 'expiringOfflineAccessTokens',\n ) ||\n (config.future.expiringOfflineAccessTokens !== undefined &&\n config.future.expiringOfflineAccessTokens !== false))\n ) {\n throw new StaticOfflineConfigurationError(\n 'Only expiringOfflineAccessTokens: false is supported.',\n );\n }\n}\n\nexport function createStaticOfflineShopifyApp(\n config: StaticOfflineAppConfig,\n): StaticOfflineShopifyApp {\n validateConfig(config);\n const scopes = validateScopes(config.scopes ?? []);\n const stores = validateStores(config.stores, scopes);\n for (const credential of Object.values(stores)) {\n if (!new AuthScopes([...(credential.scopes ?? [])]).has(scopes))\n throw new StaticOfflineConfigurationError(\n 'A configured store is missing required app scopes. Update the grant and provision its token before starting.',\n );\n }\n const {stores: _stores, ...appConfig} = config;\n const storage = new StaticSessionStorage({stores, scopes});\n const app = shopifyApp({\n ...appConfig,\n scopes,\n logger: redactedLogger({...config, stores}),\n distribution: AppDistribution.SingleMerchant,\n useOnlineTokens: false,\n future: {expiringOfflineAccessTokens: false},\n sessionStorage: storage,\n });\n\n return {\n authenticate: {\n async admin(request) {\n try {\n const context = await app.authenticate.admin(request);\n const {session, sessionToken, cors, redirect} = context;\n return {\n session,\n sessionToken,\n cors,\n redirect,\n admin: wrapAdmin(context.admin, () => {\n throw cors(tokenRejectedResponse());\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw forbidden();\n throw error;\n }\n },\n async webhook(request) {\n try {\n // Shopify verifies the raw body HMAC before looking up the session.\n const context = await app.authenticate.webhook(request);\n if (context.topic === 'APP_UNINSTALLED')\n return {...context, session: undefined, admin: undefined};\n if (!context.admin) return context;\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw tokenRejectedResponse();\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw new Response(null, {status: 200});\n throw error;\n }\n },\n },\n unauthenticated: {\n async admin(shop): Promise<UnauthenticatedAdminContext> {\n if (!storage.hasShop(shop))\n throw new StaticOfflineShopNotConfiguredError();\n const context = await app.unauthenticated.admin(shop);\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw new StaticOfflineTokenRejectedError();\n }),\n };\n },\n },\n async login(request) {\n const url = new URL(request.url);\n let shop: unknown = url.searchParams.get('shop');\n if (!shop && request.method === 'GET') return {};\n if (!shop) shop = (await request.clone().formData()).get('shop');\n if (!shop) return {shop: LoginErrorType.MissingShop};\n if (typeof shop !== 'string') return {shop: LoginErrorType.InvalidShop};\n // Match the official login form's short-name/URL convenience, then allowlist.\n shop = shop.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');\n if (typeof shop === 'string' && !shop.includes('.'))\n shop = `${shop}.myshopify.com`;\n if (!isShopDomain(shop)) return {shop: LoginErrorType.InvalidShop};\n if (!storage.hasShop(shop)) throw forbidden();\n return app.login(request);\n },\n addDocumentResponseHeaders: app.addDocumentResponseHeaders,\n };\n}\n"],"names":["StaticOfflineTokenRejectedError","HttpResponseError","StaticOfflineConfigurationError","AppDistribution","validateScopes","stores","validateStores","AuthScopes","storage","StaticSessionStorage","shopifyApp","redactedLogger","StaticOfflineShopNotConfiguredError","LoginErrorType","isShopDomain"],"mappings":"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"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var app = require('./app.js');
|
|
4
|
+
var storage = require('./storage.js');
|
|
4
5
|
var stores = require('./stores.js');
|
|
5
6
|
var errors = require('./errors.js');
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
exports.createStaticOfflineShopifyApp = app.createStaticOfflineShopifyApp;
|
|
11
|
+
exports.StaticSessionStorage = storage.StaticSessionStorage;
|
|
10
12
|
exports.parseStaticOfflineStores = stores.parseStaticOfflineStores;
|
|
11
13
|
exports.StaticOfflineConfigurationError = errors.StaticOfflineConfigurationError;
|
|
12
14
|
exports.StaticOfflineReadOnlyError = errors.StaticOfflineReadOnlyError;
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;"}
|
package/dist/cjs/storage.js
CHANGED
|
@@ -4,11 +4,11 @@ var shopifyApi = require('@shopify/shopify-api');
|
|
|
4
4
|
var errors = require('./errors.js');
|
|
5
5
|
var stores = require('./stores.js');
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
class
|
|
7
|
+
/** Read-only SessionStorage adapter for pre-provisioned non-expiring offline tokens. */
|
|
8
|
+
class StaticSessionStorage {
|
|
9
9
|
#stores;
|
|
10
|
-
constructor(
|
|
11
|
-
this.#stores = stores.validateStores(stores
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.#stores = stores.validateStores(options?.stores, options?.scopes);
|
|
12
12
|
}
|
|
13
13
|
hasShop(shop) {
|
|
14
14
|
return stores.isShopDomain(shop) && Object.hasOwn(this.#stores, shop);
|
|
@@ -27,7 +27,7 @@ class StaticOfflineSessionStorage {
|
|
|
27
27
|
state: '',
|
|
28
28
|
isOnline: false,
|
|
29
29
|
accessToken: credential.accessToken,
|
|
30
|
-
scope: credential.scopes
|
|
30
|
+
scope: credential.scopes?.join(','),
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
async findSessionsByShop(shop) {
|
|
@@ -53,5 +53,5 @@ class StaticOfflineSessionStorage {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
exports.
|
|
56
|
+
exports.StaticSessionStorage = StaticSessionStorage;
|
|
57
57
|
//# sourceMappingURL=storage.js.map
|
package/dist/cjs/storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sources":["../../../src/storage.ts"],"sourcesContent":["import {Session} from '@shopify/shopify-api';\nimport type {SessionStorage} from '@shopify/shopify-app-session-storage';\n\nimport {\n StaticOfflineReadOnlyError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {isShopDomain, validateStores, type StaticOfflineStores} from './stores';\n\n/**
|
|
1
|
+
{"version":3,"file":"storage.js","sources":["../../../src/storage.ts"],"sourcesContent":["import {Session} from '@shopify/shopify-api';\nimport type {SessionStorage} from '@shopify/shopify-app-session-storage';\n\nimport {\n StaticOfflineReadOnlyError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {isShopDomain, validateStores, type StaticOfflineStores} from './stores';\n\nexport interface StaticSessionStorageOptions {\n stores: StaticOfflineStores;\n /** Common scope metadata for every store; it does not grant API permissions. */\n scopes?: readonly string[];\n}\n\n/** Read-only SessionStorage adapter for pre-provisioned non-expiring offline tokens. */\nexport class StaticSessionStorage implements SessionStorage {\n readonly #stores: StaticOfflineStores;\n\n constructor(options: StaticSessionStorageOptions) {\n this.#stores = validateStores(options?.stores, options?.scopes);\n }\n\n hasShop(shop: string): boolean {\n return isShopDomain(shop) && Object.hasOwn(this.#stores, shop);\n }\n\n async loadSession(id: string): Promise<Session> {\n if (\n !id.startsWith('offline_') ||\n !this.hasShop(id.slice('offline_'.length))\n ) {\n // Returning undefined would trigger Shopify's token exchange strategy.\n throw new StaticOfflineShopNotConfiguredError();\n }\n const shop = id.slice('offline_'.length);\n const credential = this.#stores[shop];\n return new Session({\n id,\n shop,\n state: '',\n isOnline: false,\n accessToken: credential.accessToken,\n scope: credential.scopes?.join(','),\n });\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n return this.hasShop(shop)\n ? [await this.loadSession(`offline_${shop}`)]\n : [];\n }\n\n async storeSession(session: Session): Promise<boolean> {\n // The official admin strategy clears a rejected token and tries to persist it.\n // Throw before its reauthentication redirect; never mutate the source credentials.\n if (\n session.id === `offline_${session.shop}` &&\n this.hasShop(session.shop) &&\n !session.accessToken\n ) {\n throw new StaticOfflineTokenRejectedError();\n }\n throw new StaticOfflineReadOnlyError();\n }\n\n async deleteSession(_id: string): Promise<boolean> {\n throw new StaticOfflineReadOnlyError();\n }\n\n async deleteSessions(_ids: string[]): Promise<boolean> {\n throw new StaticOfflineReadOnlyError();\n }\n}\n"],"names":["validateStores","isShopDomain","StaticOfflineShopNotConfiguredError","Session","StaticOfflineTokenRejectedError","StaticOfflineReadOnlyError"],"mappings":";;;;;;AAgBA;MACa,oBAAoB,CAAA;AACtB,IAAA,OAAO;AAEhB,IAAA,WAAA,CAAY,OAAoC,EAAA;AAC9C,QAAA,IAAI,CAAC,OAAO,GAAGA,qBAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC;IACjE;AAEA,IAAA,OAAO,CAAC,IAAY,EAAA;AAClB,QAAA,OAAOC,mBAAY,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC;IAChE;IAEA,MAAM,WAAW,CAAC,EAAU,EAAA;AAC1B,QAAA,IACE,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;AAC1B,YAAA,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAC1C;;YAEA,MAAM,IAAIC,0CAAmC,EAAE;QACjD;QACA,MAAM,IAAI,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;QACrC,OAAO,IAAIC,kBAAO,CAAC;YACjB,EAAE;YACF,IAAI;AACJ,YAAA,KAAK,EAAE,EAAE;AACT,YAAA,QAAQ,EAAE,KAAK;YACf,WAAW,EAAE,UAAU,CAAC,WAAW;YACnC,KAAK,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC;AACpC,SAAA,CAAC;IACJ;IAEA,MAAM,kBAAkB,CAAC,IAAY,EAAA;AACnC,QAAA,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI;cACpB,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,CAAA,QAAA,EAAW,IAAI,CAAA,CAAE,CAAC;cAC1C,EAAE;IACR;IAEA,MAAM,YAAY,CAAC,OAAgB,EAAA;;;QAGjC,IACE,OAAO,CAAC,EAAE,KAAK,WAAW,OAAO,CAAC,IAAI,CAAA,CAAE;AACxC,YAAA,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;AAC1B,YAAA,CAAC,OAAO,CAAC,WAAW,EACpB;YACA,MAAM,IAAIC,sCAA+B,EAAE;QAC7C;QACA,MAAM,IAAIC,iCAA0B,EAAE;IACxC;IAEA,MAAM,aAAa,CAAC,GAAW,EAAA;QAC7B,MAAM,IAAIA,iCAA0B,EAAE;IACxC;IAEA,MAAM,cAAc,CAAC,IAAc,EAAA;QACjC,MAAM,IAAIA,iCAA0B,EAAE;IACxC;AACD;;;;"}
|
package/dist/cjs/stores.js
CHANGED
|
@@ -20,9 +20,10 @@ function validateScopes(value) {
|
|
|
20
20
|
return [...new Set(value)];
|
|
21
21
|
}
|
|
22
22
|
/** Validate and copy configuration so callers cannot mutate the credential snapshot. */
|
|
23
|
-
function validateStores(value) {
|
|
23
|
+
function validateStores(value, defaultScopes) {
|
|
24
|
+
const commonScopes = defaultScopes === undefined ? undefined : validateScopes(defaultScopes);
|
|
24
25
|
if (!isRecord(value) || Object.keys(value).length === 0) {
|
|
25
|
-
throw new errors.StaticOfflineConfigurationError('Configure at least one store with an accessToken
|
|
26
|
+
throw new errors.StaticOfflineConfigurationError('Configure at least one store with an accessToken.');
|
|
26
27
|
}
|
|
27
28
|
const stores = Object.create(null);
|
|
28
29
|
for (const [shop, credential] of Object.entries(value)) {
|
|
@@ -31,16 +32,19 @@ function validateStores(value) {
|
|
|
31
32
|
}
|
|
32
33
|
if (!isRecord(credential) ||
|
|
33
34
|
Object.keys(credential).some((key) => key !== 'accessToken' && key !== 'scopes')) {
|
|
34
|
-
throw new errors.StaticOfflineConfigurationError('Each store
|
|
35
|
+
throw new errors.StaticOfflineConfigurationError('Each store may contain only accessToken and optional scopes; expiring or online token records are unsupported.');
|
|
35
36
|
}
|
|
36
37
|
if (typeof credential.accessToken !== 'string' ||
|
|
37
38
|
!credential.accessToken ||
|
|
38
39
|
/\s/.test(credential.accessToken)) {
|
|
39
40
|
throw new errors.StaticOfflineConfigurationError('Each accessToken must be a nonempty string without whitespace.');
|
|
40
41
|
}
|
|
42
|
+
const scopes = credential.scopes === undefined
|
|
43
|
+
? commonScopes
|
|
44
|
+
: validateScopes(credential.scopes);
|
|
41
45
|
stores[shop] = Object.freeze({
|
|
42
46
|
accessToken: credential.accessToken,
|
|
43
|
-
scopes: Object.freeze(
|
|
47
|
+
...(scopes === undefined ? {} : { scopes: Object.freeze([...scopes]) }),
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
return Object.freeze(stores);
|
package/dist/cjs/stores.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stores.js","sources":["../../../src/stores.ts"],"sourcesContent":["import {StaticOfflineConfigurationError} from './errors';\n\nexport interface StaticOfflineCredential {\n readonly accessToken: string;\n /**
|
|
1
|
+
{"version":3,"file":"stores.js","sources":["../../../src/stores.ts"],"sourcesContent":["import {StaticOfflineConfigurationError} from './errors';\n\nexport interface StaticOfflineCredential {\n readonly accessToken: string;\n /** Optional granted-scope metadata overriding the common scopes. */\n readonly scopes?: readonly string[];\n}\n\nexport type StaticOfflineStores = Readonly<\n Record<string, StaticOfflineCredential>\n>;\n\nexport function isShopDomain(shop: unknown): shop is string {\n return (\n typeof shop === 'string' &&\n /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.myshopify\\.com$/.test(shop)\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nexport function validateScopes(value: unknown): string[] {\n if (\n !Array.isArray(value) ||\n value.some(\n (scope) => typeof scope !== 'string' || !/^[a-z][a-z0-9_]*$/.test(scope),\n )\n ) {\n throw new StaticOfflineConfigurationError(\n 'Scopes must be an array of nonempty Shopify scope names.',\n );\n }\n return [...new Set(value as string[])];\n}\n\n/** Validate and copy configuration so callers cannot mutate the credential snapshot. */\nexport function validateStores(\n value: unknown,\n defaultScopes?: readonly string[],\n): StaticOfflineStores {\n const commonScopes =\n defaultScopes === undefined ? undefined : validateScopes(defaultScopes);\n if (!isRecord(value) || Object.keys(value).length === 0) {\n throw new StaticOfflineConfigurationError(\n 'Configure at least one store with an accessToken.',\n );\n }\n const stores: Record<string, StaticOfflineCredential> = Object.create(null);\n for (const [shop, credential] of Object.entries(value)) {\n if (!isShopDomain(shop)) {\n throw new StaticOfflineConfigurationError(\n 'Store keys must be lowercase <shop>.myshopify.com domains without a URL, path or port.',\n );\n }\n if (\n !isRecord(credential) ||\n Object.keys(credential).some(\n (key) => key !== 'accessToken' && key !== 'scopes',\n )\n ) {\n throw new StaticOfflineConfigurationError(\n 'Each store may contain only accessToken and optional scopes; expiring or online token records are unsupported.',\n );\n }\n if (\n typeof credential.accessToken !== 'string' ||\n !credential.accessToken ||\n /\\s/.test(credential.accessToken)\n ) {\n throw new StaticOfflineConfigurationError(\n 'Each accessToken must be a nonempty string without whitespace.',\n );\n }\n const scopes =\n credential.scopes === undefined\n ? commonScopes\n : validateScopes(credential.scopes);\n stores[shop] = Object.freeze({\n accessToken: credential.accessToken,\n ...(scopes === undefined ? {} : {scopes: Object.freeze([...scopes])}),\n });\n }\n return Object.freeze(stores);\n}\n\n/** Parse a server-side JSON secret. Parse errors never echo the input. */\nexport function parseStaticOfflineStores(json: string): StaticOfflineStores {\n let value: unknown;\n try {\n if (typeof json !== 'string') throw new Error();\n value = JSON.parse(json);\n } catch {\n throw new StaticOfflineConfigurationError(\n 'Static offline stores must be valid JSON.',\n );\n }\n return validateStores(value);\n}\n"],"names":["StaticOfflineConfigurationError"],"mappings":"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"}
|
package/dist/esm/app.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { AuthScopes, HttpResponseError } from '@shopify/shopify-api';
|
|
|
2
2
|
import { shopifyApp, AppDistribution, LoginErrorType } from '@shopify/shopify-app-react-router/server';
|
|
3
3
|
import { StaticOfflineConfigurationError, StaticOfflineShopNotConfiguredError, StaticOfflineTokenRejectedError } from './errors.mjs';
|
|
4
4
|
import { redactedLogger } from './logger.mjs';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { StaticSessionStorage } from './storage.mjs';
|
|
6
|
+
import { validateScopes, validateStores, isShopDomain } from './stores.mjs';
|
|
7
7
|
|
|
8
8
|
function forbidden() {
|
|
9
9
|
return Response.json({ error: 'STATIC_OFFLINE_SHOP_NOT_CONFIGURED' }, { status: 403, headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -70,14 +70,14 @@ function validateConfig(config) {
|
|
|
70
70
|
}
|
|
71
71
|
function createStaticOfflineShopifyApp(config) {
|
|
72
72
|
validateConfig(config);
|
|
73
|
-
const stores = validateStores(config.stores);
|
|
74
73
|
const scopes = validateScopes(config.scopes ?? []);
|
|
74
|
+
const stores = validateStores(config.stores, scopes);
|
|
75
75
|
for (const credential of Object.values(stores)) {
|
|
76
|
-
if (!new AuthScopes([...credential.scopes]).has(scopes))
|
|
76
|
+
if (!new AuthScopes([...(credential.scopes ?? [])]).has(scopes))
|
|
77
77
|
throw new StaticOfflineConfigurationError('A configured store is missing required app scopes. Update the grant and provision its token before starting.');
|
|
78
78
|
}
|
|
79
79
|
const { stores: _stores, ...appConfig } = config;
|
|
80
|
-
const storage = new
|
|
80
|
+
const storage = new StaticSessionStorage({ stores, scopes });
|
|
81
81
|
const app = shopifyApp({
|
|
82
82
|
...appConfig,
|
|
83
83
|
scopes,
|
package/dist/esm/app.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.mjs","sources":["../../../src/app.ts"],"sourcesContent":["import {AuthScopes, HttpResponseError} from '@shopify/shopify-api';\nimport {\n AppDistribution,\n LoginErrorType,\n shopifyApp,\n} from '@shopify/shopify-app-react-router/server';\nimport type {\n AdminApiContext,\n UnauthenticatedAdminContext,\n} from '@shopify/shopify-app-react-router/server';\n\nimport {\n StaticOfflineConfigurationError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {redactedLogger} from './logger';\nimport {StaticOfflineSessionStorage} from './storage';\nimport {isShopDomain, validateScopes, validateStores} from './stores';\nimport type {StaticOfflineAppConfig, StaticOfflineShopifyApp} from './types';\n\nfunction forbidden(): Response {\n return Response.json(\n {error: 'STATIC_OFFLINE_SHOP_NOT_CONFIGURED'},\n {status: 403, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction tokenRejectedResponse(): Response {\n return Response.json(\n {\n error: 'STATIC_OFFLINE_TOKEN_REJECTED',\n message: 'Configured Shopify credentials require operator attention.',\n },\n {status: 503, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction wrapAdmin(\n admin: AdminApiContext,\n onRejected: () => never,\n): AdminApiContext {\n // Shopify clears this context's session on a 401. Preserve the error category\n // for later calls on the same client, without changing another request's state.\n let rejected = false;\n const graphql: AdminApiContext['graphql'] = async (operation, options) => {\n if (rejected) return onRejected();\n try {\n return await admin.graphql(operation, options);\n } catch (error) {\n if (\n error instanceof StaticOfflineTokenRejectedError ||\n (error instanceof HttpResponseError && error.response.code === 401) ||\n (error instanceof Response && error.status === 401)\n ) {\n rejected = true;\n return onRejected();\n }\n throw error;\n }\n };\n return {graphql};\n}\n\nfunction validateConfig(config: StaticOfflineAppConfig): void {\n if (!config || typeof config !== 'object')\n throw new StaticOfflineConfigurationError(\n 'An app configuration is required.',\n );\n for (const key of [\n 'sessionStorage',\n 'hooks',\n 'billing',\n 'webhooks',\n 'adminApiAccessToken',\n 'privateAppStorefrontAccessToken',\n 'customShopDomains',\n 'isCustomStoreApp',\n 'isEmbeddedApp',\n ]) {\n if (key in config)\n throw new StaticOfflineConfigurationError(\n `The ${key} option is unsupported in static offline mode.`,\n );\n }\n if (\n (config.distribution !== undefined &&\n config.distribution !== AppDistribution.SingleMerchant) ||\n (config.useOnlineTokens !== undefined && config.useOnlineTokens !== false)\n ) {\n throw new StaticOfflineConfigurationError(\n 'Static offline mode requires SingleMerchant distribution and useOnlineTokens: false.',\n );\n }\n if (\n config.future !== undefined &&\n (!config.future ||\n typeof config.future !== 'object' ||\n Array.isArray(config.future) ||\n Object.keys(config.future).some(\n (key) => key !== 'expiringOfflineAccessTokens',\n ) ||\n (config.future.expiringOfflineAccessTokens !== undefined &&\n config.future.expiringOfflineAccessTokens !== false))\n ) {\n throw new StaticOfflineConfigurationError(\n 'Only expiringOfflineAccessTokens: false is supported.',\n );\n }\n}\n\nexport function createStaticOfflineShopifyApp(\n config: StaticOfflineAppConfig,\n): StaticOfflineShopifyApp {\n validateConfig(config);\n const stores = validateStores(config.stores);\n const scopes = validateScopes(config.scopes ?? []);\n for (const credential of Object.values(stores)) {\n if (!new AuthScopes([...credential.scopes]).has(scopes))\n throw new StaticOfflineConfigurationError(\n 'A configured store is missing required app scopes. Update the grant and provision its token before starting.',\n );\n }\n const {stores: _stores, ...appConfig} = config;\n const storage = new StaticOfflineSessionStorage(stores);\n const app = shopifyApp({\n ...appConfig,\n scopes,\n logger: redactedLogger({...config, stores}),\n distribution: AppDistribution.SingleMerchant,\n useOnlineTokens: false,\n future: {expiringOfflineAccessTokens: false},\n sessionStorage: storage,\n });\n\n return {\n authenticate: {\n async admin(request) {\n try {\n const context = await app.authenticate.admin(request);\n const {session, sessionToken, cors, redirect} = context;\n return {\n session,\n sessionToken,\n cors,\n redirect,\n admin: wrapAdmin(context.admin, () => {\n throw cors(tokenRejectedResponse());\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw forbidden();\n throw error;\n }\n },\n async webhook(request) {\n try {\n // Shopify verifies the raw body HMAC before looking up the session.\n const context = await app.authenticate.webhook(request);\n if (context.topic === 'APP_UNINSTALLED')\n return {...context, session: undefined, admin: undefined};\n if (!context.admin) return context;\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw tokenRejectedResponse();\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw new Response(null, {status: 200});\n throw error;\n }\n },\n },\n unauthenticated: {\n async admin(shop): Promise<UnauthenticatedAdminContext> {\n if (!storage.hasShop(shop))\n throw new StaticOfflineShopNotConfiguredError();\n const context = await app.unauthenticated.admin(shop);\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw new StaticOfflineTokenRejectedError();\n }),\n };\n },\n },\n async login(request) {\n const url = new URL(request.url);\n let shop: unknown = url.searchParams.get('shop');\n if (!shop && request.method === 'GET') return {};\n if (!shop) shop = (await request.clone().formData()).get('shop');\n if (!shop) return {shop: LoginErrorType.MissingShop};\n if (typeof shop !== 'string') return {shop: LoginErrorType.InvalidShop};\n // Match the official login form's short-name/URL convenience, then allowlist.\n shop = shop.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');\n if (typeof shop === 'string' && !shop.includes('.'))\n shop = `${shop}.myshopify.com`;\n if (!isShopDomain(shop)) return {shop: LoginErrorType.InvalidShop};\n if (!storage.hasShop(shop)) throw forbidden();\n return app.login(request);\n },\n addDocumentResponseHeaders: app.addDocumentResponseHeaders,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app.mjs","sources":["../../../src/app.ts"],"sourcesContent":["import {AuthScopes, HttpResponseError} from '@shopify/shopify-api';\nimport {\n AppDistribution,\n LoginErrorType,\n shopifyApp,\n} from '@shopify/shopify-app-react-router/server';\nimport type {\n AdminApiContext,\n UnauthenticatedAdminContext,\n} from '@shopify/shopify-app-react-router/server';\n\nimport {\n StaticOfflineConfigurationError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {redactedLogger} from './logger';\nimport {StaticSessionStorage} from './storage';\nimport {isShopDomain, validateScopes, validateStores} from './stores';\nimport type {StaticOfflineAppConfig, StaticOfflineShopifyApp} from './types';\n\nfunction forbidden(): Response {\n return Response.json(\n {error: 'STATIC_OFFLINE_SHOP_NOT_CONFIGURED'},\n {status: 403, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction tokenRejectedResponse(): Response {\n return Response.json(\n {\n error: 'STATIC_OFFLINE_TOKEN_REJECTED',\n message: 'Configured Shopify credentials require operator attention.',\n },\n {status: 503, headers: {'Cache-Control': 'no-store'}},\n );\n}\n\nfunction wrapAdmin(\n admin: AdminApiContext,\n onRejected: () => never,\n): AdminApiContext {\n // Shopify clears this context's session on a 401. Preserve the error category\n // for later calls on the same client, without changing another request's state.\n let rejected = false;\n const graphql: AdminApiContext['graphql'] = async (operation, options) => {\n if (rejected) return onRejected();\n try {\n return await admin.graphql(operation, options);\n } catch (error) {\n if (\n error instanceof StaticOfflineTokenRejectedError ||\n (error instanceof HttpResponseError && error.response.code === 401) ||\n (error instanceof Response && error.status === 401)\n ) {\n rejected = true;\n return onRejected();\n }\n throw error;\n }\n };\n return {graphql};\n}\n\nfunction validateConfig(config: StaticOfflineAppConfig): void {\n if (!config || typeof config !== 'object')\n throw new StaticOfflineConfigurationError(\n 'An app configuration is required.',\n );\n for (const key of [\n 'sessionStorage',\n 'hooks',\n 'billing',\n 'webhooks',\n 'adminApiAccessToken',\n 'privateAppStorefrontAccessToken',\n 'customShopDomains',\n 'isCustomStoreApp',\n 'isEmbeddedApp',\n ]) {\n if (key in config)\n throw new StaticOfflineConfigurationError(\n `The ${key} option is unsupported in static offline mode.`,\n );\n }\n if (\n (config.distribution !== undefined &&\n config.distribution !== AppDistribution.SingleMerchant) ||\n (config.useOnlineTokens !== undefined && config.useOnlineTokens !== false)\n ) {\n throw new StaticOfflineConfigurationError(\n 'Static offline mode requires SingleMerchant distribution and useOnlineTokens: false.',\n );\n }\n if (\n config.future !== undefined &&\n (!config.future ||\n typeof config.future !== 'object' ||\n Array.isArray(config.future) ||\n Object.keys(config.future).some(\n (key) => key !== 'expiringOfflineAccessTokens',\n ) ||\n (config.future.expiringOfflineAccessTokens !== undefined &&\n config.future.expiringOfflineAccessTokens !== false))\n ) {\n throw new StaticOfflineConfigurationError(\n 'Only expiringOfflineAccessTokens: false is supported.',\n );\n }\n}\n\nexport function createStaticOfflineShopifyApp(\n config: StaticOfflineAppConfig,\n): StaticOfflineShopifyApp {\n validateConfig(config);\n const scopes = validateScopes(config.scopes ?? []);\n const stores = validateStores(config.stores, scopes);\n for (const credential of Object.values(stores)) {\n if (!new AuthScopes([...(credential.scopes ?? [])]).has(scopes))\n throw new StaticOfflineConfigurationError(\n 'A configured store is missing required app scopes. Update the grant and provision its token before starting.',\n );\n }\n const {stores: _stores, ...appConfig} = config;\n const storage = new StaticSessionStorage({stores, scopes});\n const app = shopifyApp({\n ...appConfig,\n scopes,\n logger: redactedLogger({...config, stores}),\n distribution: AppDistribution.SingleMerchant,\n useOnlineTokens: false,\n future: {expiringOfflineAccessTokens: false},\n sessionStorage: storage,\n });\n\n return {\n authenticate: {\n async admin(request) {\n try {\n const context = await app.authenticate.admin(request);\n const {session, sessionToken, cors, redirect} = context;\n return {\n session,\n sessionToken,\n cors,\n redirect,\n admin: wrapAdmin(context.admin, () => {\n throw cors(tokenRejectedResponse());\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw forbidden();\n throw error;\n }\n },\n async webhook(request) {\n try {\n // Shopify verifies the raw body HMAC before looking up the session.\n const context = await app.authenticate.webhook(request);\n if (context.topic === 'APP_UNINSTALLED')\n return {...context, session: undefined, admin: undefined};\n if (!context.admin) return context;\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw tokenRejectedResponse();\n }),\n };\n } catch (error) {\n if (error instanceof StaticOfflineShopNotConfiguredError)\n throw new Response(null, {status: 200});\n throw error;\n }\n },\n },\n unauthenticated: {\n async admin(shop): Promise<UnauthenticatedAdminContext> {\n if (!storage.hasShop(shop))\n throw new StaticOfflineShopNotConfiguredError();\n const context = await app.unauthenticated.admin(shop);\n return {\n ...context,\n admin: wrapAdmin(context.admin, () => {\n throw new StaticOfflineTokenRejectedError();\n }),\n };\n },\n },\n async login(request) {\n const url = new URL(request.url);\n let shop: unknown = url.searchParams.get('shop');\n if (!shop && request.method === 'GET') return {};\n if (!shop) shop = (await request.clone().formData()).get('shop');\n if (!shop) return {shop: LoginErrorType.MissingShop};\n if (typeof shop !== 'string') return {shop: LoginErrorType.InvalidShop};\n // Match the official login form's short-name/URL convenience, then allowlist.\n shop = shop.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');\n if (typeof shop === 'string' && !shop.includes('.'))\n shop = `${shop}.myshopify.com`;\n if (!isShopDomain(shop)) return {shop: LoginErrorType.InvalidShop};\n if (!storage.hasShop(shop)) throw forbidden();\n return app.login(request);\n },\n addDocumentResponseHeaders: app.addDocumentResponseHeaders,\n };\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createStaticOfflineShopifyApp } from './app.mjs';
|
|
2
|
+
export { StaticSessionStorage } from './storage.mjs';
|
|
2
3
|
export { parseStaticOfflineStores } from './stores.mjs';
|
|
3
4
|
export { StaticOfflineConfigurationError, StaticOfflineReadOnlyError, StaticOfflineShopNotConfiguredError, StaticOfflineTokenRejectedError } from './errors.mjs';
|
|
4
5
|
//# sourceMappingURL=index.mjs.map
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
|
package/dist/esm/storage.mjs
CHANGED
|
@@ -2,11 +2,11 @@ import { Session } from '@shopify/shopify-api';
|
|
|
2
2
|
import { StaticOfflineShopNotConfiguredError, StaticOfflineTokenRejectedError, StaticOfflineReadOnlyError } from './errors.mjs';
|
|
3
3
|
import { validateStores, isShopDomain } from './stores.mjs';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
class
|
|
5
|
+
/** Read-only SessionStorage adapter for pre-provisioned non-expiring offline tokens. */
|
|
6
|
+
class StaticSessionStorage {
|
|
7
7
|
#stores;
|
|
8
|
-
constructor(
|
|
9
|
-
this.#stores = validateStores(stores);
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.#stores = validateStores(options?.stores, options?.scopes);
|
|
10
10
|
}
|
|
11
11
|
hasShop(shop) {
|
|
12
12
|
return isShopDomain(shop) && Object.hasOwn(this.#stores, shop);
|
|
@@ -25,7 +25,7 @@ class StaticOfflineSessionStorage {
|
|
|
25
25
|
state: '',
|
|
26
26
|
isOnline: false,
|
|
27
27
|
accessToken: credential.accessToken,
|
|
28
|
-
scope: credential.scopes
|
|
28
|
+
scope: credential.scopes?.join(','),
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
async findSessionsByShop(shop) {
|
|
@@ -51,5 +51,5 @@ class StaticOfflineSessionStorage {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export {
|
|
54
|
+
export { StaticSessionStorage };
|
|
55
55
|
//# sourceMappingURL=storage.mjs.map
|
package/dist/esm/storage.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.mjs","sources":["../../../src/storage.ts"],"sourcesContent":["import {Session} from '@shopify/shopify-api';\nimport type {SessionStorage} from '@shopify/shopify-app-session-storage';\n\nimport {\n StaticOfflineReadOnlyError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {isShopDomain, validateStores, type StaticOfflineStores} from './stores';\n\n/**
|
|
1
|
+
{"version":3,"file":"storage.mjs","sources":["../../../src/storage.ts"],"sourcesContent":["import {Session} from '@shopify/shopify-api';\nimport type {SessionStorage} from '@shopify/shopify-app-session-storage';\n\nimport {\n StaticOfflineReadOnlyError,\n StaticOfflineShopNotConfiguredError,\n StaticOfflineTokenRejectedError,\n} from './errors';\nimport {isShopDomain, validateStores, type StaticOfflineStores} from './stores';\n\nexport interface StaticSessionStorageOptions {\n stores: StaticOfflineStores;\n /** Common scope metadata for every store; it does not grant API permissions. */\n scopes?: readonly string[];\n}\n\n/** Read-only SessionStorage adapter for pre-provisioned non-expiring offline tokens. */\nexport class StaticSessionStorage implements SessionStorage {\n readonly #stores: StaticOfflineStores;\n\n constructor(options: StaticSessionStorageOptions) {\n this.#stores = validateStores(options?.stores, options?.scopes);\n }\n\n hasShop(shop: string): boolean {\n return isShopDomain(shop) && Object.hasOwn(this.#stores, shop);\n }\n\n async loadSession(id: string): Promise<Session> {\n if (\n !id.startsWith('offline_') ||\n !this.hasShop(id.slice('offline_'.length))\n ) {\n // Returning undefined would trigger Shopify's token exchange strategy.\n throw new StaticOfflineShopNotConfiguredError();\n }\n const shop = id.slice('offline_'.length);\n const credential = this.#stores[shop];\n return new Session({\n id,\n shop,\n state: '',\n isOnline: false,\n accessToken: credential.accessToken,\n scope: credential.scopes?.join(','),\n });\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n return this.hasShop(shop)\n ? [await this.loadSession(`offline_${shop}`)]\n : [];\n }\n\n async storeSession(session: Session): Promise<boolean> {\n // The official admin strategy clears a rejected token and tries to persist it.\n // Throw before its reauthentication redirect; never mutate the source credentials.\n if (\n session.id === `offline_${session.shop}` &&\n this.hasShop(session.shop) &&\n !session.accessToken\n ) {\n throw new StaticOfflineTokenRejectedError();\n }\n throw new StaticOfflineReadOnlyError();\n }\n\n async deleteSession(_id: string): Promise<boolean> {\n throw new StaticOfflineReadOnlyError();\n }\n\n async deleteSessions(_ids: string[]): Promise<boolean> {\n throw new StaticOfflineReadOnlyError();\n }\n}\n"],"names":[],"mappings":";;;;AAgBA;MACa,oBAAoB,CAAA;AACtB,IAAA,OAAO;AAEhB,IAAA,WAAA,CAAY,OAAoC,EAAA;AAC9C,QAAA,IAAI,CAAC,OAAO,GAAG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC;IACjE;AAEA,IAAA,OAAO,CAAC,IAAY,EAAA;AAClB,QAAA,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC;IAChE;IAEA,MAAM,WAAW,CAAC,EAAU,EAAA;AAC1B,QAAA,IACE,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;AAC1B,YAAA,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAC1C;;YAEA,MAAM,IAAI,mCAAmC,EAAE;QACjD;QACA,MAAM,IAAI,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;QACrC,OAAO,IAAI,OAAO,CAAC;YACjB,EAAE;YACF,IAAI;AACJ,YAAA,KAAK,EAAE,EAAE;AACT,YAAA,QAAQ,EAAE,KAAK;YACf,WAAW,EAAE,UAAU,CAAC,WAAW;YACnC,KAAK,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC;AACpC,SAAA,CAAC;IACJ;IAEA,MAAM,kBAAkB,CAAC,IAAY,EAAA;AACnC,QAAA,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI;cACpB,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,CAAA,QAAA,EAAW,IAAI,CAAA,CAAE,CAAC;cAC1C,EAAE;IACR;IAEA,MAAM,YAAY,CAAC,OAAgB,EAAA;;;QAGjC,IACE,OAAO,CAAC,EAAE,KAAK,WAAW,OAAO,CAAC,IAAI,CAAA,CAAE;AACxC,YAAA,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;AAC1B,YAAA,CAAC,OAAO,CAAC,WAAW,EACpB;YACA,MAAM,IAAI,+BAA+B,EAAE;QAC7C;QACA,MAAM,IAAI,0BAA0B,EAAE;IACxC;IAEA,MAAM,aAAa,CAAC,GAAW,EAAA;QAC7B,MAAM,IAAI,0BAA0B,EAAE;IACxC;IAEA,MAAM,cAAc,CAAC,IAAc,EAAA;QACjC,MAAM,IAAI,0BAA0B,EAAE;IACxC;AACD;;;;"}
|
package/dist/esm/stores.mjs
CHANGED
|
@@ -18,9 +18,10 @@ function validateScopes(value) {
|
|
|
18
18
|
return [...new Set(value)];
|
|
19
19
|
}
|
|
20
20
|
/** Validate and copy configuration so callers cannot mutate the credential snapshot. */
|
|
21
|
-
function validateStores(value) {
|
|
21
|
+
function validateStores(value, defaultScopes) {
|
|
22
|
+
const commonScopes = defaultScopes === undefined ? undefined : validateScopes(defaultScopes);
|
|
22
23
|
if (!isRecord(value) || Object.keys(value).length === 0) {
|
|
23
|
-
throw new StaticOfflineConfigurationError('Configure at least one store with an accessToken
|
|
24
|
+
throw new StaticOfflineConfigurationError('Configure at least one store with an accessToken.');
|
|
24
25
|
}
|
|
25
26
|
const stores = Object.create(null);
|
|
26
27
|
for (const [shop, credential] of Object.entries(value)) {
|
|
@@ -29,16 +30,19 @@ function validateStores(value) {
|
|
|
29
30
|
}
|
|
30
31
|
if (!isRecord(credential) ||
|
|
31
32
|
Object.keys(credential).some((key) => key !== 'accessToken' && key !== 'scopes')) {
|
|
32
|
-
throw new StaticOfflineConfigurationError('Each store
|
|
33
|
+
throw new StaticOfflineConfigurationError('Each store may contain only accessToken and optional scopes; expiring or online token records are unsupported.');
|
|
33
34
|
}
|
|
34
35
|
if (typeof credential.accessToken !== 'string' ||
|
|
35
36
|
!credential.accessToken ||
|
|
36
37
|
/\s/.test(credential.accessToken)) {
|
|
37
38
|
throw new StaticOfflineConfigurationError('Each accessToken must be a nonempty string without whitespace.');
|
|
38
39
|
}
|
|
40
|
+
const scopes = credential.scopes === undefined
|
|
41
|
+
? commonScopes
|
|
42
|
+
: validateScopes(credential.scopes);
|
|
39
43
|
stores[shop] = Object.freeze({
|
|
40
44
|
accessToken: credential.accessToken,
|
|
41
|
-
scopes: Object.freeze(
|
|
45
|
+
...(scopes === undefined ? {} : { scopes: Object.freeze([...scopes]) }),
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
return Object.freeze(stores);
|
package/dist/esm/stores.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stores.mjs","sources":["../../../src/stores.ts"],"sourcesContent":["import {StaticOfflineConfigurationError} from './errors';\n\nexport interface StaticOfflineCredential {\n readonly accessToken: string;\n /**
|
|
1
|
+
{"version":3,"file":"stores.mjs","sources":["../../../src/stores.ts"],"sourcesContent":["import {StaticOfflineConfigurationError} from './errors';\n\nexport interface StaticOfflineCredential {\n readonly accessToken: string;\n /** Optional granted-scope metadata overriding the common scopes. */\n readonly scopes?: readonly string[];\n}\n\nexport type StaticOfflineStores = Readonly<\n Record<string, StaticOfflineCredential>\n>;\n\nexport function isShopDomain(shop: unknown): shop is string {\n return (\n typeof shop === 'string' &&\n /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.myshopify\\.com$/.test(shop)\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nexport function validateScopes(value: unknown): string[] {\n if (\n !Array.isArray(value) ||\n value.some(\n (scope) => typeof scope !== 'string' || !/^[a-z][a-z0-9_]*$/.test(scope),\n )\n ) {\n throw new StaticOfflineConfigurationError(\n 'Scopes must be an array of nonempty Shopify scope names.',\n );\n }\n return [...new Set(value as string[])];\n}\n\n/** Validate and copy configuration so callers cannot mutate the credential snapshot. */\nexport function validateStores(\n value: unknown,\n defaultScopes?: readonly string[],\n): StaticOfflineStores {\n const commonScopes =\n defaultScopes === undefined ? undefined : validateScopes(defaultScopes);\n if (!isRecord(value) || Object.keys(value).length === 0) {\n throw new StaticOfflineConfigurationError(\n 'Configure at least one store with an accessToken.',\n );\n }\n const stores: Record<string, StaticOfflineCredential> = Object.create(null);\n for (const [shop, credential] of Object.entries(value)) {\n if (!isShopDomain(shop)) {\n throw new StaticOfflineConfigurationError(\n 'Store keys must be lowercase <shop>.myshopify.com domains without a URL, path or port.',\n );\n }\n if (\n !isRecord(credential) ||\n Object.keys(credential).some(\n (key) => key !== 'accessToken' && key !== 'scopes',\n )\n ) {\n throw new StaticOfflineConfigurationError(\n 'Each store may contain only accessToken and optional scopes; expiring or online token records are unsupported.',\n );\n }\n if (\n typeof credential.accessToken !== 'string' ||\n !credential.accessToken ||\n /\\s/.test(credential.accessToken)\n ) {\n throw new StaticOfflineConfigurationError(\n 'Each accessToken must be a nonempty string without whitespace.',\n );\n }\n const scopes =\n credential.scopes === undefined\n ? commonScopes\n : validateScopes(credential.scopes);\n stores[shop] = Object.freeze({\n accessToken: credential.accessToken,\n ...(scopes === undefined ? {} : {scopes: Object.freeze([...scopes])}),\n });\n }\n return Object.freeze(stores);\n}\n\n/** Parse a server-side JSON secret. Parse errors never echo the input. */\nexport function parseStaticOfflineStores(json: string): StaticOfflineStores {\n let value: unknown;\n try {\n if (typeof json !== 'string') throw new Error();\n value = JSON.parse(json);\n } catch {\n throw new StaticOfflineConfigurationError(\n 'Static offline stores must be valid JSON.',\n );\n }\n return validateStores(value);\n}\n"],"names":[],"mappings":"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"}
|
package/dist/ts/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createStaticOfflineShopifyApp } from './app';
|
|
2
|
+
export { StaticSessionStorage } from './storage';
|
|
3
|
+
export type { StaticSessionStorageOptions } from './storage';
|
|
2
4
|
export { parseStaticOfflineStores } from './stores';
|
|
3
5
|
export type { StaticOfflineCredential, StaticOfflineStores } from './stores';
|
|
4
6
|
export type { StaticOfflineAppConfig, StaticOfflineAdminContext, StaticOfflineShopifyApp, } from './types';
|
package/dist/ts/storage.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Session } from '@shopify/shopify-api';
|
|
2
2
|
import type { SessionStorage } from '@shopify/shopify-app-session-storage';
|
|
3
3
|
import { type StaticOfflineStores } from './stores';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export interface StaticSessionStorageOptions {
|
|
5
|
+
stores: StaticOfflineStores;
|
|
6
|
+
/** Common scope metadata for every store; it does not grant API permissions. */
|
|
7
|
+
scopes?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** Read-only SessionStorage adapter for pre-provisioned non-expiring offline tokens. */
|
|
10
|
+
export declare class StaticSessionStorage implements SessionStorage {
|
|
6
11
|
#private;
|
|
7
|
-
constructor(
|
|
12
|
+
constructor(options: StaticSessionStorageOptions);
|
|
8
13
|
hasShop(shop: string): boolean;
|
|
9
14
|
loadSession(id: string): Promise<Session>;
|
|
10
15
|
findSessionsByShop(shop: string): Promise<Session[]>;
|
package/dist/ts/stores.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export interface StaticOfflineCredential {
|
|
2
2
|
readonly accessToken: string;
|
|
3
|
-
/**
|
|
4
|
-
readonly scopes
|
|
3
|
+
/** Optional granted-scope metadata overriding the common scopes. */
|
|
4
|
+
readonly scopes?: readonly string[];
|
|
5
5
|
}
|
|
6
6
|
export type StaticOfflineStores = Readonly<Record<string, StaticOfflineCredential>>;
|
|
7
7
|
export declare function isShopDomain(shop: unknown): shop is string;
|
|
8
8
|
export declare function validateScopes(value: unknown): string[];
|
|
9
9
|
/** Validate and copy configuration so callers cannot mutate the credential snapshot. */
|
|
10
|
-
export declare function validateStores(value: unknown): StaticOfflineStores;
|
|
10
|
+
export declare function validateStores(value: unknown, defaultScopes?: readonly string[]): StaticOfflineStores;
|
|
11
11
|
/** Parse a server-side JSON secret. Parse errors never echo the input. */
|
|
12
12
|
export declare function parseStaticOfflineStores(json: string): StaticOfflineStores;
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shopify-static-offline-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Read-only, pre-provisioned offline credentials for custom Shopify React Router apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/sanghogu/shopify-app-js.git",
|
|
9
|
-
"directory": "packages/apps/shopify-static-offline-auth"
|
|
9
|
+
"directory": "packages/apps/session-storage/shopify-static-offline-auth"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/sanghogu/shopify-app-js/tree/main/packages/apps/shopify-static-offline-auth#readme",
|
|
11
|
+
"homepage": "https://github.com/sanghogu/shopify-app-js/tree/main/packages/apps/session-storage/shopify-static-offline-auth#readme",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=22.0.0"
|
|
14
14
|
},
|