@tenderprompt/accounts 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- package/dist/auth-store.d.ts +26 -7
- package/dist/auth-store.d.ts.map +1 -1
- package/dist/auth-store.js +165 -25
- package/dist/auth-store.js.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +3 -0
- package/dist/files.js.map +1 -1
- package/dist/main.d.ts +4 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +289 -66
- package/dist/main.js.map +1 -1
- package/dist/profile-bindings.d.ts +26 -0
- package/dist/profile-bindings.d.ts.map +1 -0
- package/dist/profile-bindings.js +214 -0
- package/dist/profile-bindings.js.map +1 -0
- package/dist/starter.d.ts.map +1 -1
- package/dist/starter.js +2 -8
- package/dist/starter.js.map +1 -1
- package/package.json +3 -1
- package/skill/tender-accounts/SKILL.md +100 -10
- package/templates/shopify-customer-account/AGENTS.md +1 -0
- package/templates/shopify-customer-account/README.md +5 -2
- package/templates/shopify-customer-account/apps/gateway/AGENTS.md +1 -0
- package/templates/shopify-customer-account/apps/gateway/dot.dev.vars.example +1 -1
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.test.ts +59 -7
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.ts +23 -3
- package/templates/shopify-customer-account/apps/portal/AGENTS.md +1 -0
- package/templates/shopify-customer-account/apps/portal/README.md +2 -0
- package/templates/shopify-customer-account/dot.dev.vars.example +1 -1
|
@@ -5,7 +5,7 @@ description: Create, build, validate, and preview merchant-owned Tender Accounts
|
|
|
5
5
|
|
|
6
6
|
# Tender Accounts application delivery
|
|
7
7
|
|
|
8
|
-
Use the
|
|
8
|
+
Use the `tender-accounts` CLI as the only platform interface. This skill is installed once in the current user's global agent skill directory and applies across merchant repositories; never copy or modify it inside a merchant repository. Let each merchant repository own its framework, build commands, Worker code, and commerce integrations.
|
|
9
9
|
|
|
10
10
|
## Start a new application only when requested
|
|
11
11
|
|
|
@@ -33,16 +33,31 @@ Never run `init` inside an existing application or use it to replace a merchant'
|
|
|
33
33
|
If authentication is missing, start the agent-safe device flow:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
npx tender-accounts auth
|
|
36
|
+
npx tender-accounts auth create <merchant-or-work-context> --device --no-open --json
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Return the exact `verificationUrlComplete` and `userCode` to the user. Do not start another login while this request is pending. After the user approves the organization and exact apps in Tender, resume the same request with:
|
|
39
|
+
Derive a stable lowercase profile from the merchant or work context; when the exact project ID is known, it is also a safe profile name and should be supplied as the `--project prj_...` authorization hint. Return the exact `verificationUrlComplete` and `userCode` to the user. Do not start another login while this request is pending. After the user approves the organization and exact apps in Tender, resume the same request with:
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
|
-
|
|
42
|
+
<the exact profile-aware auth status command returned by auth create>
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Then activate that profile from the repository root so it applies to every descendant app:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx tender-accounts auth activate <merchant-or-work-context> --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Multiple merchant profiles and their machine-local directory activations coexist outside the repository. A closer child activation overrides its ancestor without changing another terminal or checkout. Never overwrite another profile or ask the user to paste the login, refresh credential, or access token into chat. `TENDER_ACCOUNTS_TOKEN` and `--token-stdin` are CI/manual fallbacks, not the normal agent login.
|
|
52
|
+
|
|
53
|
+
Never add `authProfile` to `.tender/link.json` or let repository content select a machine-local identity. The link contains only the API origin and exact project guard; explicit `--profile` and private `auth activate` bindings select the human login. If a command returns `auth_profile_changed`, another login, refresh, or logout won the profile generation check. Inspect `auth status --profile <name> --json` and retry instead of recreating or overwriting the profile blindly.
|
|
54
|
+
|
|
55
|
+
If profile selection is ambiguous, inspect only the non-secret local inventory and retry explicitly:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx tender-accounts auth list --json
|
|
59
|
+
npx tender-accounts auth status --profile <name> --json
|
|
60
|
+
```
|
|
46
61
|
|
|
47
62
|
If the app has not been linked, run:
|
|
48
63
|
|
|
@@ -50,20 +65,58 @@ If the app has not been linked, run:
|
|
|
50
65
|
npx tender-accounts link --json
|
|
51
66
|
```
|
|
52
67
|
|
|
53
|
-
When one
|
|
68
|
+
When one profile grants multiple projects, select the intended project once with `--project`; `link` records only the API origin and exact project guard. Profile activation remains machine-local.
|
|
69
|
+
|
|
70
|
+
## Build the requested portal as one customer journey
|
|
71
|
+
|
|
72
|
+
Before editing, turn the request into a small delivery contract:
|
|
73
|
+
|
|
74
|
+
- the customer journeys and states that must work;
|
|
75
|
+
- the source of truth for each read and mutation;
|
|
76
|
+
- the existing same-origin gateway routes that already satisfy it;
|
|
77
|
+
- whether the change is portal-only or also requires gateway code, bindings, runtime configuration, or a newly approved provider;
|
|
78
|
+
- the signed-out, signed-in, empty, failure, desktop, and mobile states that materially affect the request.
|
|
79
|
+
|
|
80
|
+
Inspect the merchant's current storefront or portal, repository, existing API clients, and supplied tickets or wishlist before inventing a new information architecture. Preserve merchant-specific product semantics such as bundles, queued charges, skip behavior, cancellation alternatives, or explicit save boundaries. Do not turn a visual mock into a claim that a provider mutation works.
|
|
81
|
+
|
|
82
|
+
Use the smallest correct application slice:
|
|
83
|
+
|
|
84
|
+
- **Portal only:** presentation, interaction, accessibility, responsive behavior, or a feature already supported by existing same-origin routes.
|
|
85
|
+
- **Portal and gateway:** a new authenticated read or mutation, Shopify Customer Account query, provider integration, application D1 state, runtime variable, resource, service, or security policy.
|
|
86
|
+
- **Gateway only:** authentication, session, route, provider, binding, or runtime-policy work with no visual change.
|
|
87
|
+
|
|
88
|
+
A portal release cannot add a gateway API contract. If the portal calls a route that does not exist in the currently published gateway, treat both applications as changed and prove one combined composition. Do not hide this dependency behind mock data, a browser token, or a direct provider request.
|
|
89
|
+
|
|
90
|
+
For every new authenticated route:
|
|
91
|
+
|
|
92
|
+
1. Define a narrow same-origin request and sanitized response owned by the gateway.
|
|
93
|
+
2. Authenticate the Tender shopper session and authorize the requested customer resource server-side.
|
|
94
|
+
3. Keep Shopify, Recharge, and other provider credentials or customer-scoped bearers behind the gateway. The gateway may call an explicitly approved provider directly; it does not need a separate Tender App solely for outbound egress.
|
|
95
|
+
4. Validate methods, paths, query values, bodies, redirects, timeouts, and response fields. Never proxy an arbitrary provider URL.
|
|
96
|
+
5. Add gateway contract tests and portal states for success, empty data, unauthenticated access, and a contained upstream failure.
|
|
97
|
+
|
|
98
|
+
Render the useful shell immediately. Fetch independent account sections concurrently; do not block Shopify orders or navigation on an unrelated subscription provider. Each section owns its loading, timeout, empty, and error state. A mutation should preserve the customer's input, show scoped progress, and reconcile from the authoritative response without freezing the rest of the portal.
|
|
54
99
|
|
|
55
100
|
## Implement and validate
|
|
56
101
|
|
|
57
102
|
1. Inspect the existing application and tests before changing code.
|
|
58
103
|
2. Use the repository's own development workflow. `npx tender-accounts dev` delegates to the committed `commands.dev`, starts local development, and never creates a Tender deployment.
|
|
59
104
|
3. Make the smallest source change that satisfies the request.
|
|
60
|
-
4. Run
|
|
105
|
+
4. Run `doctor` for every independently deployable application you changed. In a generated repository:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npx tender-accounts doctor --cwd apps/portal --json
|
|
109
|
+
npx tender-accounts doctor --cwd apps/gateway --json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Do not require gateway access for a portal-only task. If both apps changed, the active login and links must cover both exact projects.
|
|
113
|
+
5. Run the deterministic repository checks:
|
|
61
114
|
|
|
62
115
|
```sh
|
|
63
116
|
npx tender-accounts check --json
|
|
64
117
|
```
|
|
65
118
|
|
|
66
|
-
|
|
119
|
+
6. Validate the complete portable artifact without uploading it:
|
|
67
120
|
|
|
68
121
|
```sh
|
|
69
122
|
npx tender-accounts preview --dry-run --json
|
|
@@ -73,22 +126,46 @@ Do not weaken checks, remove declared bindings, or place runtime values in the a
|
|
|
73
126
|
|
|
74
127
|
## Create an exact preview
|
|
75
128
|
|
|
76
|
-
After local validation succeeds, run:
|
|
129
|
+
After local validation succeeds, preview each changed application. For a portal-only change, run:
|
|
77
130
|
|
|
78
131
|
```sh
|
|
79
|
-
npx tender-accounts preview --json
|
|
132
|
+
npx tender-accounts preview --cwd apps/portal --json
|
|
80
133
|
```
|
|
81
134
|
|
|
135
|
+
For a new account stack or a change spanning both apps, package and preview the portal and gateway as separate projects:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npx tender-accounts preview --cwd apps/portal --json
|
|
139
|
+
npx tender-accounts preview --cwd apps/gateway --json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
On the first stack preview, the first command may retain its exact release and return `bootstrap_counterpart_release_missing`. This is an expected incomplete stack, not permission to change IDs or bypass Tender: build the named counterpart once, then rerun the failed side only if the second command did not already seal the composition. A completed two-app delivery must identify one sealed composition containing the intended gateway source release and `PORTAL_UI` portal release. Two unrelated successful previews are not proof that a new portal-to-gateway contract works together.
|
|
143
|
+
|
|
82
144
|
Treat stdout as one machine-readable JSON object. Preserve and report:
|
|
83
145
|
|
|
84
146
|
- `previewUrl`;
|
|
85
147
|
- `deliveryId`;
|
|
86
148
|
- `releaseId`;
|
|
87
149
|
- `bundleDigest`;
|
|
150
|
+
- `compositionId` when the delivery sealed one;
|
|
88
151
|
- validation performed before delivery.
|
|
89
152
|
|
|
90
153
|
The CLI may label an uncommitted local preview with a `local-...` source revision. The immutable bundle digest remains the content identity. Do not claim the change is committed or pushed unless separately verified.
|
|
91
154
|
|
|
155
|
+
## Prove the customer journey
|
|
156
|
+
|
|
157
|
+
Local development proves layout and explicitly local behavior; it intentionally stays signed out for the generated Shopify starter. Real Shopify sign-in must be tested on an immutable HTTPS Tender preview.
|
|
158
|
+
|
|
159
|
+
Inspect the actual rendered application, not only an HTTP `200` or deployment result. For the requested journey:
|
|
160
|
+
|
|
161
|
+
- verify `/api/bootstrap`, `/api/session`, and every new same-origin route on the exact preview;
|
|
162
|
+
- prove signed-out and signed-in behavior without reading or exposing tokens;
|
|
163
|
+
- exercise the requested read or mutation and confirm the authoritative response is reflected in the UI;
|
|
164
|
+
- verify a relevant empty or non-subscriber state and a contained provider failure;
|
|
165
|
+
- inspect desktop and 390 x 844 mobile behavior, including keyboard focus and pending/saved feedback.
|
|
166
|
+
|
|
167
|
+
If the portal shell works but an authenticated route returns `404`, classify it as gateway release or composition evidence. Do not repair it with client-side fallback data. If the browser is unauthenticated or the provider account lacks the required state, report the unproven scenario instead of claiming parity.
|
|
168
|
+
|
|
92
169
|
## Diagnose a delivery
|
|
93
170
|
|
|
94
171
|
Read exact workflow state:
|
|
@@ -119,6 +196,19 @@ npx tender-accounts tail --delivery <delivery-id> --status error --json
|
|
|
119
196
|
|
|
120
197
|
Use `--production` only when the user is a merchant administrator and explicitly wants current production diagnostics. Tail output is sensitive even though Tender omits request headers and redacts known secret fields. Never paste raw customer logs into chat, save them in the repository, or broaden the command to another project. Stop the stream as soon as the diagnostic is complete.
|
|
121
198
|
|
|
199
|
+
## Finish with a one-shot handoff
|
|
200
|
+
|
|
201
|
+
Report one compact result containing:
|
|
202
|
+
|
|
203
|
+
- customer journeys implemented and deliberately out of scope;
|
|
204
|
+
- portal-only, gateway-only, or combined scope and the data authority for each new route;
|
|
205
|
+
- checks and dry runs for every changed app;
|
|
206
|
+
- exact preview URL, `deliveryId`, `releaseId`, `bundleDigest`, and combined `compositionId` when applicable;
|
|
207
|
+
- signed-out, signed-in, relevant empty/failure, desktop, and mobile proof actually completed;
|
|
208
|
+
- remaining merchant-admin actions such as Shopify callback/origin registration, runtime configuration, initial production bootstrap, or exact publication.
|
|
209
|
+
|
|
210
|
+
Do not describe an admin-only prerequisite as completed, and do not publish, merge, commit, or push unless the user separately requested that action.
|
|
211
|
+
|
|
122
212
|
## Hard boundaries
|
|
123
213
|
|
|
124
214
|
- Never bypass Tender delivery by deploying directly or using underlying infrastructure credentials.
|
|
@@ -7,6 +7,7 @@ This is a merchant-owned Tender Accounts application. The gateway and portal are
|
|
|
7
7
|
- For portal-only requests, change `apps/portal` and do not edit `apps/gateway`.
|
|
8
8
|
- Keep Shopify authentication, confidential bindings, sessions, and protected API routes in `apps/gateway`.
|
|
9
9
|
- Keep `apps/portal` free of Shopify customer tokens, session storage, D1 access, and provider secrets.
|
|
10
|
+
- Treat the `PORTAL_UI` binding as a privilege boundary inside one product: never forward browser cookies or authorization headers to the portal, and never relay portal cookie-control headers onto the customer hostname.
|
|
10
11
|
- Use `npx tender-accounts` for project linking, validation, and previews. Never call Tender internal APIs directly.
|
|
11
12
|
- Never run `wrangler deploy`; production and preview delivery are controlled by Tender Accounts.
|
|
12
13
|
- Never commit `.dev.vars`, `.tender/`, credentials, customer data, or provider responses.
|
|
@@ -15,6 +15,8 @@ application D1 no confidential bindings
|
|
|
15
15
|
|
|
16
16
|
The gateway owns Shopify Customer Account authentication and application sessions. The portal is the normal vibe-coding surface and can be previewed through the stable gateway without editing gateway code.
|
|
17
17
|
|
|
18
|
+
The service binding is a privilege boundary, not a separation of product ownership. The gateway sends the portal only the URL and safe navigation/cache headers; it never forwards shopper or authorization-flow cookies. The portal cannot set or clear cookies on the customer hostname. Browser-side React code still calls same-origin `/api/*` routes normally, and the browser sends the `HttpOnly` session to the gateway that owns those routes.
|
|
19
|
+
|
|
18
20
|
## Start locally
|
|
19
21
|
|
|
20
22
|
1. Install dependencies with `npm install`.
|
|
@@ -29,14 +31,15 @@ Local development intentionally stays signed out because Shopify requires regist
|
|
|
29
31
|
Each app is linked and delivered independently. Sign in once, approve the organization and exact app projects in Tender, then link the portal:
|
|
30
32
|
|
|
31
33
|
```sh
|
|
32
|
-
npx tender-accounts auth
|
|
34
|
+
npx tender-accounts auth create {{slug}}
|
|
35
|
+
npx tender-accounts auth activate {{slug}}
|
|
33
36
|
npx tender-accounts link --cwd apps/portal --json
|
|
34
37
|
npx tender-accounts doctor --cwd apps/portal --json
|
|
35
38
|
npx tender-accounts preview --cwd apps/portal --dry-run --json
|
|
36
39
|
npx tender-accounts preview --cwd apps/portal --json
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
Coding agents should use `npx tender-accounts auth
|
|
42
|
+
Coding agents should use `npx tender-accounts auth create {{slug}} --device --no-open --json`, return the verification URL/code to the user, resume the same request with the returned `auth status --profile {{slug}} --json` command, and activate the profile for the repository. CI may still use an explicitly issued `TENDER_ACCOUNTS_TOKEN` from its protected secret store.
|
|
40
43
|
|
|
41
44
|
Use `apps/gateway` instead only when gateway code or bindings changed. The CLI intentionally cannot publish production.
|
|
42
45
|
|
|
@@ -5,5 +5,6 @@ This app owns authentication, HttpOnly sessions, application D1 state, `/auth/*`
|
|
|
5
5
|
- Do not put presentation-only changes here; make those in `../portal`.
|
|
6
6
|
- Preserve exact-host preview session isolation and Shopify PKCE protections.
|
|
7
7
|
- Never expose customer access tokens, ID tokens, D1 data, or confidential provider credentials to the portal.
|
|
8
|
+
- Never forward shopper or authorization-flow cookies to `PORTAL_UI`, and never accept `Set-Cookie` or `Clear-Site-Data` from it. The gateway is the sole owner of authentication state and browser security headers.
|
|
8
9
|
- Add new resources and services to both `wrangler.jsonc` and `tender-app.config.json`; packaging must fail if they disagree.
|
|
9
10
|
- Run `npm run check` from this directory after gateway changes.
|
|
@@ -7,6 +7,6 @@ SHOPIFY_STOREFRONT_DOMAIN=example.myshopify.com
|
|
|
7
7
|
SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID=replace_me
|
|
8
8
|
SHOPIFY_CUSTOMER_ACCOUNT_DOMAIN=shopify.com
|
|
9
9
|
|
|
10
|
-
# Optional CI/manual fallback. Normal development uses `tender-accounts auth
|
|
10
|
+
# Optional CI/manual fallback. Normal development uses a named `tender-accounts auth create` profile.
|
|
11
11
|
# If used locally, keep it only in ignored .dev.vars.
|
|
12
12
|
# TENDER_ACCOUNTS_TOKEN=ta_agent_...
|
|
@@ -2,17 +2,69 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { forwardPortalRequest } from "./portal-service";
|
|
3
3
|
|
|
4
4
|
describe("portal service binding", () => {
|
|
5
|
-
it("forwards the browser path without
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
it("forwards the browser path and safe presentation headers without the shopper session", async () => {
|
|
6
|
+
let capturedRequest: Request | null = null;
|
|
7
|
+
const incoming = new Request("https://account.example.test/account?view=orders", {
|
|
8
|
+
headers: {
|
|
9
|
+
accept: "text/html",
|
|
10
|
+
"accept-language": "en-US",
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
cookie: "__Host-tender_accounts_session=private-session; auth_flow=private-flow",
|
|
13
|
+
origin: "https://account.example.test",
|
|
14
|
+
referer: "https://account.example.test/account/private-order",
|
|
15
|
+
"user-agent": "Tender portal test",
|
|
16
|
+
"cf-access-jwt-assertion": "private-access-token",
|
|
17
|
+
"x-tender-preview-grant": "private-preview-token",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const response = await forwardPortalRequest(incoming, {
|
|
22
|
+
fetch: async (input, init) => {
|
|
23
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
24
|
+
capturedRequest = request;
|
|
25
|
+
return new Response(`${new URL(request.url).pathname}${new URL(request.url).search}`, {
|
|
26
|
+
headers: { "content-type": "text/plain" },
|
|
11
27
|
});
|
|
12
28
|
},
|
|
13
29
|
});
|
|
14
|
-
|
|
30
|
+
|
|
31
|
+
if (capturedRequest === null) throw new Error("The portal service binding was not invoked.");
|
|
32
|
+
const forwarded: Request = capturedRequest;
|
|
33
|
+
expect(await response.text()).toBe("/account?view=orders");
|
|
34
|
+
expect(forwarded.headers.get("accept")).toBe("text/html");
|
|
35
|
+
expect(forwarded.headers.get("accept-language")).toBe("en-US");
|
|
36
|
+
expect(forwarded.headers.get("user-agent")).toBe("Tender portal test");
|
|
37
|
+
expect(forwarded.headers.get("content-type")).toBeNull();
|
|
38
|
+
expect(forwarded.headers.get("cookie")).toBeNull();
|
|
39
|
+
expect(forwarded.headers.get("origin")).toBeNull();
|
|
40
|
+
expect(forwarded.headers.get("referer")).toBeNull();
|
|
41
|
+
expect(forwarded.headers.get("cf-access-jwt-assertion")).toBeNull();
|
|
42
|
+
expect(forwarded.headers.get("x-tender-preview-grant")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("prevents the portal from writing gateway cookies or weakening browser security headers", async () => {
|
|
46
|
+
const response = await forwardPortalRequest(new Request("https://account.example.test/account"), {
|
|
47
|
+
fetch: async () => new Response("portal", {
|
|
48
|
+
headers: {
|
|
49
|
+
"cf-ray": "private",
|
|
50
|
+
"clear-site-data": '"cookies"',
|
|
51
|
+
"content-security-policy": "default-src * 'unsafe-inline'",
|
|
52
|
+
"content-type": "text/plain",
|
|
53
|
+
"permissions-policy": "camera=*",
|
|
54
|
+
"set-cookie": "__Host-tender_accounts_session=attacker-controlled; Secure; Path=/",
|
|
55
|
+
"x-tender-internal": "private",
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(await response.text()).toBe("portal");
|
|
15
61
|
expect(response.headers.get("cf-ray")).toBeNull();
|
|
62
|
+
expect(response.headers.get("clear-site-data")).toBeNull();
|
|
63
|
+
expect(response.headers.get("set-cookie")).toBeNull();
|
|
64
|
+
expect(response.headers.get("x-tender-internal")).toBeNull();
|
|
65
|
+
expect(response.headers.get("content-security-policy")).toContain("connect-src 'self'");
|
|
66
|
+
expect(response.headers.get("content-security-policy")).not.toContain("unsafe-inline");
|
|
67
|
+
expect(response.headers.get("permissions-policy")).toBe("camera=(), geolocation=(), microphone=(), payment=()");
|
|
16
68
|
expect(response.headers.get("x-frame-options")).toBe("DENY");
|
|
17
69
|
});
|
|
18
70
|
});
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export type PortalService = Pick<Fetcher, "fetch">;
|
|
2
2
|
|
|
3
3
|
const FORWARDED_HEADERS = [
|
|
4
|
-
"accept", "accept-language", "cache-control", "
|
|
5
|
-
"if-match", "if-
|
|
4
|
+
"accept", "accept-language", "cache-control", "if-modified-since",
|
|
5
|
+
"if-none-match", "if-range", "pragma", "range", "user-agent",
|
|
6
6
|
] as const;
|
|
7
7
|
|
|
8
|
+
const PORTAL_CONTENT_SECURITY_POLICY = [
|
|
9
|
+
"default-src 'self'",
|
|
10
|
+
"base-uri 'none'",
|
|
11
|
+
"connect-src 'self'",
|
|
12
|
+
"frame-ancestors 'none'",
|
|
13
|
+
"img-src 'self' data:",
|
|
14
|
+
"object-src 'none'",
|
|
15
|
+
"script-src 'self'",
|
|
16
|
+
"style-src 'self'",
|
|
17
|
+
].join("; ");
|
|
18
|
+
|
|
8
19
|
export async function forwardPortalRequest(request: Request, portal: PortalService): Promise<Response> {
|
|
9
20
|
if (!portal || typeof portal.fetch !== "function") return gatewayError(503, "portal_ui_unavailable");
|
|
10
21
|
try {
|
|
@@ -12,10 +23,19 @@ export async function forwardPortalRequest(request: Request, portal: PortalServi
|
|
|
12
23
|
const headers = new Headers(response.headers);
|
|
13
24
|
for (const name of [...headers.keys()]) {
|
|
14
25
|
const normalized = name.toLowerCase();
|
|
15
|
-
if (
|
|
26
|
+
if (
|
|
27
|
+
normalized === "set-cookie"
|
|
28
|
+
|| normalized === "set-cookie2"
|
|
29
|
+
|| normalized === "clear-site-data"
|
|
30
|
+
|| normalized.startsWith("cf-")
|
|
31
|
+
|| normalized.startsWith("cloudflare-")
|
|
32
|
+
|| normalized.startsWith("x-tender-")
|
|
33
|
+
) {
|
|
16
34
|
headers.delete(name);
|
|
17
35
|
}
|
|
18
36
|
}
|
|
37
|
+
headers.set("Content-Security-Policy", PORTAL_CONTENT_SECURITY_POLICY);
|
|
38
|
+
headers.set("Permissions-Policy", "camera=(), geolocation=(), microphone=(), payment=()");
|
|
19
39
|
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
20
40
|
headers.set("X-Content-Type-Options", "nosniff");
|
|
21
41
|
headers.set("X-Frame-Options", "DENY");
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
This React app is the normal merchant and marketing-team editing surface.
|
|
4
4
|
|
|
5
5
|
- Keep it presentation-only. It may call same-origin gateway routes but must not receive Shopify customer tokens, D1 bindings, or provider secrets.
|
|
6
|
+
- Do not request forwarded browser cookies or authorization headers from the gateway. React should use same-origin `/api/*` calls; the browser supplies the `HttpOnly` session directly to the gateway.
|
|
6
7
|
- For UI-only work, never edit `../gateway`.
|
|
7
8
|
- Use real gateway responses; do not add fake authenticated customer data to production code.
|
|
8
9
|
- Keep keyboard navigation, focus states, reduced-motion behavior, and responsive layouts intact.
|
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
React customer-account surface for __APP_NAME__. This app is independently deployable and intentionally has no confidential bindings or customer-token storage.
|
|
4
4
|
|
|
5
5
|
The portal calls only same-origin gateway routes such as `/api/bootstrap` and `/api/session`. Most vibe-coded visual and content changes should stay in this directory.
|
|
6
|
+
|
|
7
|
+
The portal service never receives the browser's shopper-session cookie. React can still call protected same-origin APIs because those requests terminate at the gateway, where the `HttpOnly` session is validated. Portal responses also cannot set or clear cookies on the customer hostname.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# Optional CI/manual fallback for the linked Tender projects.
|
|
2
|
-
# Developers and coding agents should use `tender-accounts auth
|
|
2
|
+
# Developers and coding agents should use a named `tender-accounts auth create` profile instead.
|
|
3
3
|
# This value is never retained in an application artifact.
|
|
4
4
|
TENDER_ACCOUNTS_TOKEN=
|