@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.
@@ -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 repository-installed `tender-accounts` CLI as the only platform interface. Let the merchant repository own its framework, build commands, Worker code, and commerce integrations.
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 login --device --no-open --json
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
- npx tender-accounts auth status --json
42
+ <the exact profile-aware auth status command returned by auth create>
43
43
  ```
44
44
 
45
- The CLI stores the resulting login outside the repository and rotates short-lived project credentials automatically. Never 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.
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 credential grants multiple projects, select the intended project once with `--project`.
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 the deterministic repository checks:
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
- 5. Validate the complete portable artifact without uploading it:
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 login
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 login --device --no-open --json`, return the verification URL/code to the user, and resume the same request with `auth status --json` after approval. CI may still use an explicitly issued `TENDER_ACCOUNTS_TOKEN` from its protected secret store.
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 login`.
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 exposing platform headers", async () => {
6
- const response = await forwardPortalRequest(new Request("https://account.example.test/account"), {
7
- fetch: async (request) => {
8
- const url = request instanceof Request ? request.url : request.toString();
9
- return new Response(new URL(url).pathname, {
10
- headers: { "cf-ray": "private", "content-type": "text/plain" },
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
- expect(await response.text()).toBe("/account");
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", "content-type", "cookie",
5
- "if-match", "if-none-match", "origin", "pragma", "referer", "user-agent",
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 (normalized.startsWith("cf-") || normalized.startsWith("cloudflare-") || normalized.startsWith("x-tender-")) {
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 login` instead.
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=