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 CHANGED
@@ -1,19 +1,19 @@
1
1
  # shopify-static-offline-auth
2
2
 
3
- Pre-provisioned, non-expiring offline access tokens for **custom-distributed Shopify React Router apps**, without a session database.
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 wraps the official Shopify library. It preserves Shopify's embedded-app JWT validation and uses a server-side store allowlist to supply credentials. It is not an official Shopify package and does not bypass authentication.
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**. This package cannot inspect an opaque token to prove its type or expiration.
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
- It does not support public App Store apps, legacy admin-created apps, online tokens, refresh tokens, Remix, Billing, dynamic scope requests, Flow, POS, app proxies, or Storefront clients.
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 by uninstalling the app or revoking its secret. See [Shopify access tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens).
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
- Keep your existing React Router/React dependencies and Shopify framework setup.
24
+ ## Use with the official shopifyApp
25
25
 
26
- ## Configure
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
- SHOPIFY_STATIC_OFFLINE_STORES='{"example.myshopify.com":{"accessToken":"REPLACE_WITH_NON_EXPIRING_OFFLINE_TOKEN","scopes":["read_products"]}}'
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
- createStaticOfflineShopifyApp,
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 shopify = createStaticOfflineShopifyApp({
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
- apiVersion: ApiVersion.October26,
48
- scopes: ['read_products'],
49
- stores: parseStaticOfflineStores(process.env.SHOPIFY_STATIC_OFFLINE_STORES!),
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
- The package reads no environment variables. You can instead supply a `stores` object from your secret manager **before** constructing the app. Store keys must be exact lowercase `<shop>.myshopify.com` domains, with no protocol, path, port, wildcard, or custom storefront domain. At least one store is required. Each record must contain only `accessToken` and a `scopes` array; an empty scope array is valid only when the app requires no scopes.
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 map is copied and frozen when the app is constructed. It is a credential snapshot, not a live secret subscription. Every session read creates a fresh `Session`; changing a returned session does not change the snapshot or another request's credentials.
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
- `scopes` on a store record describes permissions **already granted** to its token. Required app scopes must be included (including Shopify's implied write-to-read permissions). This is a local consistency check, not a remote grant verification. Editing scopes or omitting an expiration field cannot grant permissions or turn an expiring token into a permanent token.
77
+ ### Common scopes and granted permissions
64
78
 
65
- `SingleMerchant`, `useOnlineTokens: false`, and `expiringOfflineAccessTokens: false` are enforced. An explicitly conflicting option fails at startup. `sessionStorage`, `hooks`, `billing`, code-based `webhooks`, custom shop domains, and global access-token overrides are rejected. Configure webhooks in TOML instead.
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
- ## Supported interfaces
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
- ### Authenticated Admin requests
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
- ```ts
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
- Keep the template's App Bridge/AppProvider, auth catch-all route, `boundary` imports, and document-response-header integration from `@shopify/shopify-app-react-router`. Header JWTs and document `id_token` requests continue through the official authentication flow. Initial App Bridge redirects to obtain a JWT are normal; the wrapper does not exchange it for another access token.
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 JWT's validated destination selects the credential. A query-string `shop` cannot select another store's token. An unconfigured destination produces a `403` response without token exchange. JWT failures retain Shopify's normal responses and bootstrap behavior.
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
- The context exposes only `admin`, `session`, `sessionToken`, `cors`, and `redirect`. Offline requests use the app installation's permissions; they do not acquire staff-specific online tokens. Never return a `session`, token map, or server configuration to the browser.
91
+ ## Read-only adapter behavior
83
92
 
84
- ### Webhooks
93
+ This adapter intentionally cannot satisfy the writable CRUD semantics of a database adapter:
85
94
 
86
- Define app-specific webhook subscriptions in your existing `shopify.app.toml`:
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
- ```toml
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
- [[webhooks.subscriptions]]
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
- ```ts
98
- const {topic, shop, session, admin, payload} =
99
- await authenticate.webhook(request);
107
+ ### Webhooks and uninstall
100
108
 
101
- if (topic === 'APP_UNINSTALLED') {
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
- The official library verifies the raw-body HMAC first. Valid deliveries for an unconfigured shop throw an empty `200` Response, which the route must allow React Router to handle; business logic after authentication does not run. Invalid HMACs are still rejected. `APP_UNINSTALLED` deliveries expose their metadata and payload but no `session` or `admin`.
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 {StaticOfflineTokenRejectedError} from 'shopify-static-offline-auth';
119
-
120
- try {
121
- const {admin} = await unauthenticated.admin('example.myshopify.com');
122
- await admin.graphql('{ shop { name } }');
123
- } catch (error) {
124
- if (error instanceof StaticOfflineTokenRejectedError) {
125
- // Stop the job and notify an operator using your own reporting system.
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
- throw error;
134
+
135
+ // Process the normal webhook here.
136
+ return new Response(null, {status: 200});
128
137
  }
129
138
  ```
130
139
 
131
- `unauthenticated.admin` is for **trusted server code**. It applies the allowlist but does not authenticate a caller. Do not expose it through a public route without your own request authorization.
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
- `login(request)` preserves the official login form and redirects only configured shops. Short names such as `example` are accepted on the login form. `addDocumentResponseHeaders(request, headers)` delegates to Shopify. No session storage or additional authentication surfaces are exposed.
142
+ ## Optional React Router wrapper
136
143
 
137
- ## Errors and token lifecycle
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
- | Condition | Request-based API | Background API |
140
- | ------------------------------------------------- | ---------------------------------------------------- | ------------------------------------- |
141
- | Unconfigured store | Admin: `403`; valid webhook: ignored with `200` | `StaticOfflineShopNotConfiguredError` |
142
- | Shopify rejects the token with HTTP `401` | `503` JSON with `STATIC_OFFLINE_TOKEN_REJECTED` | `StaticOfflineTokenRejectedError` |
143
- | Invalid configuration | `StaticOfflineConfigurationError` during startup | Same |
144
- | Attempted session mutation | Internal adapter throws `StaticOfflineReadOnlyError` | Same |
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
- Token rejection responses use `Cache-Control: no-store`, contain no token or upstream response body, and do not request reauthentication or token refresh. The internal adapter also converts Shopify's attempt to persist an invalidated token into a token-rejection error. It never claims that a write or deletion succeeded. Debug logs passed through the wrapper redact configured secrets and JWTs.
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
- Authentication success means the JWT is valid and its shop is configured. No remote token probe is added to each authentication call. Revocation or incorrect grants can therefore be discovered on the first API call. Repeated requests continue to fail until credentials are replaced; there is no shared invalidation cache.
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
- To provision or rotate a token:
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
- 1. Install/authorize the same custom-distributed app on the intended store and obtain a non-expiring offline token using Shopify's supported [token exchange](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/token-exchange) or authorization-code flow. This package does not implement a provisioning flow.
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
- Changes to app-required scopes may require reauthorization and new credentials before the app can start.
177
+ ## Migrating and operating
160
178
 
161
- ## Migrating from Prisma/session storage
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
- 1. Provision and verify tokens for every allowed store before changing the app.
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 plus native Node integration contracts. HTTP is mocked; the contracts fail on token-exchange, refresh, or unexpected network calls. `test:package` packs the built package, inspects its file allowlist, installs it in a temporary consumer with **npm registry releases** (React Router 4.0.2 / API 15.1.1), reruns the same contracts, and checks ESM, CommonJS, private-subpath blocking, and TypeScript declarations. It needs network access to install dependencies, but calls no real stores. The temporary consumer is removed afterward.
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 checks pass, inspect `npm pack --dry-run` from this package directory, then publish only this package with an authorized npm account:
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 its integrity on the npm registry after publication.
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.StaticOfflineSessionStorage(stores$1);
82
+ const storage$1 = new storage.StaticSessionStorage({ stores: stores$1, scopes });
83
83
  const app = server.shopifyApp({
84
84
  ...appConfig,
85
85
  scopes,
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;"}
@@ -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
- /** Internal bridge only: intentionally does not implement mutable storage semantics. */
8
- class StaticOfflineSessionStorage {
7
+ /** Read-only SessionStorage adapter for pre-provisioned non-expiring offline tokens. */
8
+ class StaticSessionStorage {
9
9
  #stores;
10
- constructor(stores$1) {
11
- this.#stores = stores.validateStores(stores$1);
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.join(','),
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.StaticOfflineSessionStorage = StaticOfflineSessionStorage;
56
+ exports.StaticSessionStorage = StaticSessionStorage;
57
57
  //# sourceMappingURL=storage.js.map
@@ -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/** Internal bridge only: intentionally does not implement mutable storage semantics. */\nexport class StaticOfflineSessionStorage implements SessionStorage {\n readonly #stores: StaticOfflineStores;\n\n constructor(stores: StaticOfflineStores) {\n this.#stores = validateStores(stores);\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":["stores","validateStores","isShopDomain","StaticOfflineShopNotConfiguredError","Session","StaticOfflineTokenRejectedError","StaticOfflineReadOnlyError"],"mappings":";;;;;;AAUA;MACa,2BAA2B,CAAA;AAC7B,IAAA,OAAO;AAEhB,IAAA,WAAA,CAAYA,QAA2B,EAAA;AACrC,QAAA,IAAI,CAAC,OAAO,GAAGC,qBAAc,CAACD,QAAM,CAAC;IACvC;AAEA,IAAA,OAAO,CAAC,IAAY,EAAA;AAClB,QAAA,OAAOE,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,CAAC,IAAI,CAAC,GAAG,CAAC;AACnC,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;;;;"}
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;;;;"}
@@ -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 and scopes array.');
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 must contain only accessToken and scopes; expiring or online token records are unsupported.');
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(validateScopes(credential.scopes)),
47
+ ...(scopes === undefined ? {} : { scopes: Object.freeze([...scopes]) }),
44
48
  });
45
49
  }
46
50
  return Object.freeze(stores);
@@ -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 /** Actual granted scopes, not a request for additional permissions. */\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(value: unknown): StaticOfflineStores {\n if (!isRecord(value) || Object.keys(value).length === 0) {\n throw new StaticOfflineConfigurationError(\n 'Configure at least one store with an accessToken and scopes array.',\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 must contain only accessToken and 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 stores[shop] = Object.freeze({\n accessToken: credential.accessToken,\n scopes: Object.freeze(validateScopes(credential.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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"}
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 { StaticOfflineSessionStorage } from './storage.mjs';
6
- import { validateStores, validateScopes, isShopDomain } from './stores.mjs';
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 StaticOfflineSessionStorage(stores);
80
+ const storage = new StaticSessionStorage({ stores, scopes });
81
81
  const app = shopifyApp({
82
82
  ...appConfig,
83
83
  scopes,
@@ -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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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -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
- /** Internal bridge only: intentionally does not implement mutable storage semantics. */
6
- class StaticOfflineSessionStorage {
5
+ /** Read-only SessionStorage adapter for pre-provisioned non-expiring offline tokens. */
6
+ class StaticSessionStorage {
7
7
  #stores;
8
- constructor(stores) {
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.join(','),
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 { StaticOfflineSessionStorage };
54
+ export { StaticSessionStorage };
55
55
  //# sourceMappingURL=storage.mjs.map
@@ -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/** Internal bridge only: intentionally does not implement mutable storage semantics. */\nexport class StaticOfflineSessionStorage implements SessionStorage {\n readonly #stores: StaticOfflineStores;\n\n constructor(stores: StaticOfflineStores) {\n this.#stores = validateStores(stores);\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":";;;;AAUA;MACa,2BAA2B,CAAA;AAC7B,IAAA,OAAO;AAEhB,IAAA,WAAA,CAAY,MAA2B,EAAA;AACrC,QAAA,IAAI,CAAC,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC;IACvC;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,CAAC,IAAI,CAAC,GAAG,CAAC;AACnC,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;;;;"}
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;;;;"}
@@ -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 and scopes array.');
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 must contain only accessToken and scopes; expiring or online token records are unsupported.');
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(validateScopes(credential.scopes)),
45
+ ...(scopes === undefined ? {} : { scopes: Object.freeze([...scopes]) }),
42
46
  });
43
47
  }
44
48
  return Object.freeze(stores);
@@ -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 /** Actual granted scopes, not a request for additional permissions. */\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(value: unknown): StaticOfflineStores {\n if (!isRecord(value) || Object.keys(value).length === 0) {\n throw new StaticOfflineConfigurationError(\n 'Configure at least one store with an accessToken and scopes array.',\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 must contain only accessToken and 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 stores[shop] = Object.freeze({\n accessToken: credential.accessToken,\n scopes: Object.freeze(validateScopes(credential.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":";;AAYM,SAAU,YAAY,CAAC,IAAa,EAAA;AACxC,IAAA,QACE,OAAO,IAAI,KAAK,QAAQ;AACxB,QAAA,wDAAwD,CAAC,IAAI,CAAC,IAAI,CAAC;AAEvE;AAEA,SAAS,QAAQ,CAAC,KAAc,EAAA;AAC9B,IAAA,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;IAC7E,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC;IAC9C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI;AAC7D;AAEM,SAAU,cAAc,CAAC,KAAc,EAAA;AAC3C,IAAA,IACE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACrB,KAAK,CAAC,IAAI,CACR,CAAC,KAAK,KAAK,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,CACzE,EACD;AACA,QAAA,MAAM,IAAI,+BAA+B,CACvC,0DAA0D,CAC3D;IACH;IACA,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAiB,CAAC,CAAC;AACxC;AAEA;AACM,SAAU,cAAc,CAAC,KAAc,EAAA;AAC3C,IAAA,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE;AACvD,QAAA,MAAM,IAAI,+BAA+B,CACvC,oEAAoE,CACrE;IACH;IACA,MAAM,MAAM,GAA4C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;AAC3E,IAAA,KAAK,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;AACtD,QAAA,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE;AACvB,YAAA,MAAM,IAAI,+BAA+B,CACvC,wFAAwF,CACzF;QACH;AACA,QAAA,IACE,CAAC,QAAQ,CAAC,UAAU,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAC1B,CAAC,GAAG,KAAK,GAAG,KAAK,aAAa,IAAI,GAAG,KAAK,QAAQ,CACnD,EACD;AACA,YAAA,MAAM,IAAI,+BAA+B,CACvC,wGAAwG,CACzG;QACH;AACA,QAAA,IACE,OAAO,UAAU,CAAC,WAAW,KAAK,QAAQ;YAC1C,CAAC,UAAU,CAAC,WAAW;YACvB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EACjC;AACA,YAAA,MAAM,IAAI,+BAA+B,CACvC,gEAAgE,CACjE;QACH;AACA,QAAA,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;YAC3B,WAAW,EAAE,UAAU,CAAC,WAAW;YACnC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACzD,SAAA,CAAC;IACJ;AACA,IAAA,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;AAC9B;AAEA;AACM,SAAU,wBAAwB,CAAC,IAAY,EAAA;AACnD,IAAA,IAAI,KAAc;AAClB,IAAA,IAAI;QACF,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,EAAE;AAC/C,QAAA,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IAC1B;AAAE,IAAA,MAAM;AACN,QAAA,MAAM,IAAI,+BAA+B,CACvC,2CAA2C,CAC5C;IACH;AACA,IAAA,OAAO,cAAc,CAAC,KAAK,CAAC;AAC9B;;;;"}
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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"}
@@ -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';
@@ -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
- /** Internal bridge only: intentionally does not implement mutable storage semantics. */
5
- export declare class StaticOfflineSessionStorage implements SessionStorage {
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(stores: StaticOfflineStores);
12
+ constructor(options: StaticSessionStorageOptions);
8
13
  hasShop(shop: string): boolean;
9
14
  loadSession(id: string): Promise<Session>;
10
15
  findSessionsByShop(shop: string): Promise<Session[]>;
@@ -1,12 +1,12 @@
1
1
  export interface StaticOfflineCredential {
2
2
  readonly accessToken: string;
3
- /** Actual granted scopes, not a request for additional permissions. */
4
- readonly scopes: readonly string[];
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.1.1",
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
  },