minutework 0.1.36 → 0.1.38

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.
Files changed (31) hide show
  1. package/EXTERNAL_ALPHA.md +5 -0
  2. package/README.md +12 -4
  3. package/assets/claude-local/CLAUDE.md.template +21 -0
  4. package/assets/claude-local/skills/README.md +7 -0
  5. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -0
  6. package/assets/claude-local/skills/capability-gap-reporting/SKILL.md +44 -3
  7. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +14 -0
  8. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +16 -0
  9. package/assets/templates/mobile-app/.env.example +5 -0
  10. package/assets/templates/mobile-app/AGENTS.md +11 -5
  11. package/assets/templates/mobile-app/README.md +26 -9
  12. package/assets/templates/mobile-app/app/(auth)/login.tsx +108 -14
  13. package/assets/templates/mobile-app/src/mw/client.ts +19 -1
  14. package/assets/templates/mobile-app/src/mw/env.ts +6 -2
  15. package/assets/templates/mobile-app/template.json +1 -1
  16. package/assets/templates/next-tenant-app/README.md +7 -0
  17. package/assets/templates/next-tenant-app/package.json +1 -1
  18. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +3 -3
  19. package/assets/templates/next-tenant-app/src/features/shell/components/authenticated-app-layout-shell.tsx +4 -9
  20. package/dist/cli-json.d.ts +1 -1
  21. package/dist/cli-json.js.map +1 -1
  22. package/dist/developer-client.d.ts +40 -0
  23. package/dist/developer-client.js +27 -0
  24. package/dist/developer-client.js.map +1 -1
  25. package/dist/gaps.d.ts +13 -0
  26. package/dist/gaps.js +340 -0
  27. package/dist/gaps.js.map +1 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +17 -1
  30. package/dist/index.js.map +1 -1
  31. package/package.json +1 -1
package/EXTERNAL_ALPHA.md CHANGED
@@ -63,6 +63,11 @@ the same source so they cannot drift) plus exported `skills/` guidance so the
63
63
  local coding agent sees the combined-web, mobile standalone-client,
64
64
  snapshot-delivery, and `mw.core.site` baseline workflow. Claude Code reads
65
65
  `CLAUDE.md`; Codex and other IDE agents read `AGENTS.md`.
66
+ The generated capability-gap guidance is JSON-first and platform-submitted:
67
+ agents record sanitized gaps in
68
+ `.minutework/runtime/capability-gap-report.json`, run `minutework gaps submit`
69
+ for likely reusable substrate gaps, and do not use GitHub credentials or create
70
+ GitHub Issues/PRs from the generated workspace.
66
71
  The exported guidance includes a project-orientation fast path so broad
67
72
  questions like "what is this project about?", "what can we build?", and "can we
68
73
  make an existing product agentic?" route through the relevant skills in one
package/README.md CHANGED
@@ -12,6 +12,9 @@ The current external alpha is intentionally narrow:
12
12
  - Developer-local broker surface: `minutework session start|resume|status`
13
13
  - Hosted release class: `ssr_container`
14
14
  - Deploy surface: `minutework deploy --preview`
15
+ - Capability gap queue: `minutework gaps submit|status` sends sanitized
16
+ generated-workspace gap reports to platform `BuilderCapabilityRequest`
17
+ records; GitHub Issue/PR promotion is server-side follow-up automation
15
18
  - Deferred: `--live`, additional local coding engines, sidecar/runtime-backed deploys, and the commercial marketplace listing/pricing layer
16
19
  - Marketplace publish: `minutework publish` submits the compiled, digest-bound app pack through the platform publication-review + attestation gate and publishes it to the public marketplace catalog **only** on an approved (or approved-with-warnings) attestation; a rejected attestation prints the review findings and exits 1 without publishing
17
20
 
@@ -96,6 +99,10 @@ minutework session resume
96
99
  to the combined web, mobile, published-site, runtime-agent, OSS-adoption, and
97
100
  ontology workflows. Claude Code reads `CLAUDE.md`; Codex and other IDE agents
98
101
  read `AGENTS.md`.
102
+ - Generated guidance tells Claude/Codex/Cursor to record capability gaps as
103
+ sanitized JSON first, submit likely reusable gaps with `minutework gaps
104
+ submit`, and avoid direct GitHub Issue/PR workflows from the generated
105
+ workspace.
99
106
  - Broad project questions route through a project-orientation fast path so the
100
107
  agent can answer "what is this project about?" or "can we make an existing
101
108
  product agentic?" without the user naming individual skills.
@@ -128,12 +135,12 @@ For direct third-party web hosts such as Vercel, deploy `tenant-app` only. In Ve
128
135
 
129
136
  ## Machine-readable output (`--json`)
130
137
 
131
- `validate`, `compile`, `codegen`, and `deploy --preview` accept `--json` so an IDE coding agent can drive them unattended and parse one stable shape. With `--json` the command prints a single result envelope to stdout and nothing else:
138
+ `validate`, `compile`, `codegen`, `deploy --preview`, `publish`, and `gaps` accept `--json` so an IDE coding agent can drive them unattended and parse one stable shape. With `--json` the command prints a single result envelope to stdout and nothing else:
132
139
 
133
140
  ```json
134
141
  {
135
142
  "cliJsonVersion": 1,
136
- "command": "validate | compile | codegen | deploy",
143
+ "command": "validate | compile | codegen | deploy | publish | gaps",
137
144
  "ok": true,
138
145
  "status": "ok | compiled | generated | activated | failed | rolled_back | confirmation_required | not_implemented | error",
139
146
  "result": {},
@@ -142,7 +149,7 @@ For direct third-party web hosts such as Vercel, deploy `tenant-app` only. In Ve
142
149
  ```
143
150
 
144
151
  - `ok` mirrors the process exit code: `0` when `ok` is `true`, non-zero otherwise (fail-closed).
145
- - `result` is the typed payload — the validate report (`validate`), the compile graph (`compile` / `codegen`), or the terminal deploy receipt (`deploy`).
152
+ - `result` is the typed payload — the validate report (`validate`), the compile graph (`compile` / `codegen`), the terminal deploy receipt (`deploy`), publish review status (`publish`), or platform capability request status (`gaps`).
146
153
  - `deploy --preview --json` runs unattended and never prompts. Pair it with `--yes` to authorize the deploy; without `--yes` it returns `status: "confirmation_required"` (exit 1) instead of blocking on a prompt.
147
154
  - In `--json` mode the automatic diagnostic report is suppressed — the envelope and exit code are the whole contract.
148
155
 
@@ -161,7 +168,8 @@ CLI failures automatically send a sanitized diagnostic report to the MinuteWork
161
168
 
162
169
  - Node.js 18 or newer
163
170
  - [Poetry](https://python-poetry.org/) on `PATH` when your workspace includes the FastAPI sidecar and you plan to run it locally (then run `pnpm run install:sidecar` or `cd sidecar && poetry install`)
164
- - A MinuteWork platform that exposes the developer CLI auth and public-site preview deploy endpoints
171
+ - A MinuteWork platform that exposes developer CLI auth, public-site preview
172
+ deploy, and developer Builder capability-request endpoints
165
173
  - An auth profile with interactive developer access, or a deploy token that includes `deploy.preview.request`
166
174
 
167
175
  If the hosted preview provider is not configured, preview deploy returns a typed failure or rollback-preserved receipt instead of a fake success.
@@ -9,11 +9,22 @@ private authenticated workspace under `/app`.
9
9
  It uses `@minutework/web-auth` through a thin `src/mw/` substrate and
10
10
  same-origin `/_mw` routes; do not add per-app auth/gateway BFF routes or
11
11
  browser-stored tokens.
12
+ For web password auth ergonomics, call `signUp` / `signInWithPassword` from
13
+ `@minutework/web-auth`; these are cookie-backed web aliases and do not return
14
+ access or refresh tokens. Use `useMinuteWorkSession().status` for guards with
15
+ this precedence: `loading` -> `verification_required` -> `authenticated` ->
16
+ `unauthenticated`.
12
17
 
13
18
  `mobile` is the standalone Expo/React Native client starter. It talks directly
14
19
  to the platform with native device-flow auth and bearer tokens; it is not a
15
20
  `tenant-app` web SDK cookie client and not a `sidecar`. It ships through the
16
21
  developer's own EAS/App Store/Play Store pipeline, not `minutework deploy`.
22
+ Mobile may use `@minutework/native-auth` `signInWithPassword`, but that method
23
+ returns/stores a native access/refresh token pair. Set
24
+ `EXPO_PUBLIC_MW_TENANT_ID` to the non-secret tenant UUID so native password and
25
+ browser-assisted flows send tenant context. The same method name in
26
+ `@minutework/web-auth` is same-origin cookie auth for tenant web and does not
27
+ return tokens.
17
28
 
18
29
  ## Refresh Managed Guidance
19
30
 
@@ -39,6 +50,16 @@ coding agent can use them as reference: browse `skills/` and read the relevant
39
50
  automatically and `/skill-name` invokes one directly; other agents (Codex,
40
51
  Cursor) can open the files directly or ask "What skills are available?".
41
52
 
53
+ ## Capability Gaps
54
+
55
+ When a request exposes missing shared MinuteWork substrate, read
56
+ `skills/capability-gap-reporting/SKILL.md`. Inspect runtime/platform truth
57
+ first, write sanitized JSON to
58
+ `.minutework/runtime/capability-gap-report.json`, then submit likely reusable
59
+ gaps with `minutework gaps submit`. Do not use GitHub credentials, create
60
+ GitHub Issues, or open GitHub PRs from this generated workspace; the platform
61
+ `BuilderCapabilityRequest` queue is the trusted handoff.
62
+
42
63
  ## Project Orientation Fast Path
43
64
 
44
65
  When the user asks broad context questions like "what is this project about?",
@@ -17,6 +17,10 @@ receive:
17
17
  - `tenant-app` is the combined public plus private web starter
18
18
  - `tenant-app` auth/data uses `@minutework/web-auth` and thin `src/mw/`
19
19
  substrate, not per-app auth/gateway BFF routes
20
+ - `tenant-app` uses cookie-backed `signUp` / `signInWithPassword` aliases and
21
+ `useMinuteWorkSession().status`; mobile uses `@minutework/native-auth`
22
+ bearer-token storage for its same-named password method and sends the
23
+ configured `EXPO_PUBLIC_MW_TENANT_ID` tenant context
20
24
  - `mw.core.site` is a runtime baseline capability
21
25
  - tenant public authoring is runtime/CMS-backed through `mw.core.site`
22
26
  - Vuilder-owned public sites use `vuilder-public-site` and public-dj CMS
@@ -27,6 +31,9 @@ receive:
27
31
  - tenant-specific extensions belong in adjacent schemas or extension packs
28
32
  - per-customer deliverables usually come from runtime content/workflow, not a
29
33
  fresh product rebuild
34
+ - reusable capability gaps are JSON-first and platform-submitted with
35
+ `minutework gaps submit`; generated workspaces do not open GitHub Issues or
36
+ PRs directly
30
37
 
31
38
  Generated-workspace-first guidance should live here, especially:
32
39
 
@@ -27,6 +27,14 @@ An `app pack` is the shipped product unit.
27
27
  - The template may render a local `/login` page that calls SDK hooks. The SDK
28
28
  also exposes hosted UI helpers for same-origin `/_mw/login`,
29
29
  `/_mw/signup`, and `/_mw/verify-email`; both choices remain SDK-only.
30
+ - For Supabase-like naming, web product UI may call
31
+ `signUp` / `signInWithPassword` from `@minutework/web-auth`. In `tenant-app`
32
+ these aliases remain cookie-backed web customer auth and do not return
33
+ bearer tokens.
34
+ - Use `useMinuteWorkSession().status` for web guards instead of hand-rolled
35
+ `loading && !authenticated` branches. Status precedence is `loading` ->
36
+ `verification_required` -> `authenticated` -> `unauthenticated`; keep
37
+ request errors separate from status.
30
38
  - Client-side `/app` session checks are UX gating only. Real authorization is
31
39
  enforced server-side by platform `/_mw` routes and runtime dispatch checks:
32
40
  active customer membership, email verification, app publication, and
@@ -39,6 +47,12 @@ An `app pack` is the shipped product unit.
39
47
  - The authenticated `tenant-app` principal is `tenant_customer`. Do not model
40
48
  the generated customer app as an operator/platform-session app, and do not
41
49
  persist bearer/session/runtime tokens in browser JavaScript.
50
+ - Native/mobile password auth belongs in `@minutework/native-auth`, not
51
+ `tenant-app`. Its `signInWithPassword(...)` method shares the web alias name
52
+ but returns/stores a native bearer access/refresh token pair for the platform
53
+ native API. Configure `EXPO_PUBLIC_MW_TENANT_ID` with the non-secret tenant
54
+ UUID so native password and browser-assisted flows send explicit tenant
55
+ context.
42
56
  - Use `sidecar` for internal APIs, workers, integrations, or Python-heavy compute.
43
57
  - For member-facing collaboration and operator workflows, check shell fit first
44
58
  before proposing standalone `tenant-app` routes or dashboards.
@@ -8,9 +8,43 @@ description: "A request does not cleanly fit current MinuteWork substrate and a
8
8
  Use this skill when a generated workspace discovers that the requested
9
9
  implementation does not cleanly fit current MinuteWork substrate.
10
10
 
11
+ ## Workflow
12
+
13
+ 1. Inspect available runtime/platform truth first:
14
+ - read the workspace MCP capability inventory and snapshot;
15
+ - check existing skills before inventing new substrate;
16
+ - prefer shipped MinuteWork primitives, baseline capabilities, app packs,
17
+ overlays, or reviewed skills when they already fit.
18
+ 2. Record the gap locally in
19
+ `.minutework/runtime/capability-gap-report.json` using the sanitized JSON
20
+ shape below.
21
+ 3. Submit likely reusable gaps to the platform queue:
22
+
23
+ ```bash
24
+ minutework gaps submit
25
+ ```
26
+
27
+ Use `minutework gaps submit --gap <gapId>` for one gap, and
28
+ `minutework gaps submit --all` only when the developer intentionally wants
29
+ tenant-local gaps submitted too.
30
+ 4. Check queue state with:
31
+
32
+ ```bash
33
+ minutework gaps status
34
+ ```
35
+
36
+ Platform `BuilderCapabilityRequest` records are the trusted queue for reusable
37
+ substrate requests. GitHub Issues and PRs are a later server-side automation
38
+ step after Codex reviews the submitted platform record against the monorepo.
39
+ Generated workspaces must not use GitHub credentials, create GitHub Issues,
40
+ open GitHub PRs, or require monorepo permissions for capability gaps.
41
+
42
+ ## JSON Report
43
+
11
44
  - Record architecture gaps in `.minutework/runtime/capability-gap-report.json`.
12
45
  - Keep the report sanitized and tenant-safe. Do not include secrets, raw
13
- prompts, private provider responses, or full payload copies.
46
+ prompts, private provider responses, full payload copies, absolute local
47
+ paths, env values, or source diffs.
14
48
  - Use the `MinuteWorkCapabilityGapReportV1` shape:
15
49
  - `version`
16
50
  - `generatedAt`
@@ -52,5 +86,12 @@ implementation does not cleanly fit current MinuteWork substrate.
52
86
  - `attached_app` when the right home is an attached-app integration surface
53
87
  rather than shared core substrate.
54
88
  - Prefer one concrete gap per missing shared capability.
55
- - Use gap reports to tell humans where MinuteWork needs new substrate. Do not
56
- treat them as automatic implementation instructions.
89
+ - Use gap reports to tell MinuteWork where shared substrate may be missing. Do
90
+ not treat them as automatic implementation instructions.
91
+ - `minutework gaps submit` submits only `reusability: "likelyReusable"` gaps by
92
+ default and fails closed on missing auth, missing link, invalid reports, or
93
+ platform rejection.
94
+ - The first slice stops at platform records:
95
+ `Builder agent -> gap JSON -> minutework gaps submit -> BuilderCapabilityRequest`.
96
+ - Later automation may validate accepted requests, create or link GitHub Issues,
97
+ open PRs, and mark platform records resolved after protected-branch merge.
@@ -32,6 +32,13 @@ the monorepo or a live tenant runtime.
32
32
  - A local `/login` page may call SDK hooks, while SDK hosted UI helpers point
33
33
  to same-origin `/_mw/login`, `/_mw/signup`, and `/_mw/verify-email`; both
34
34
  are valid only when they stay on the SDK/`/_mw` contract.
35
+ - For Supabase-like naming in `tenant-app`, prefer
36
+ `signUp` / `signInWithPassword` from `@minutework/web-auth`. These are web
37
+ aliases over the same cookie-backed customer session; they do not return
38
+ access or refresh tokens.
39
+ - Use `useMinuteWorkSession().status` for web app guards. Its precedence is
40
+ `loading` -> `verification_required` -> `authenticated` ->
41
+ `unauthenticated`; keep `error` as a separate field.
35
42
  - Client-side app guards are UX only. Platform `/_mw` routes and runtime
36
43
  dispatch enforce active customer membership, email verification, app
37
44
  publication, and manifest exposure server-side.
@@ -45,6 +52,13 @@ the monorepo or a live tenant runtime.
45
52
  platform-issued native session token, rather than the `tenant-app` web SDK /
46
53
  hosted-cookie session. The mobile starter remains npm-owned standalone app
47
54
  code and should not be added to the generated root `pnpm-workspace.yaml`.
55
+ - Mobile may call `@minutework/native-auth` `signInWithPassword(...)`, but this
56
+ is native/member password auth that returns and stores a bearer access/refresh
57
+ token pair through the starter's secure storage. Configure
58
+ `EXPO_PUBLIC_MW_TENANT_ID` with the non-secret tenant UUID so native password
59
+ and browser-assisted flows send explicit tenant context. Do not copy
60
+ tenant-app web auth code into mobile, and do not expect the web SDK's
61
+ same-named method to return tokens.
48
62
  - If the `mobile` starter is enabled (`starters.mobile.enabled`), read
49
63
  `standalone-mobile-client/SKILL.md` before proposing the mobile surface.
50
64
  - `vuilder-public-site` is a Vuilder-owned public-site authoring workspace for
@@ -80,10 +80,26 @@ The native token flow mirrors the existing CLI developer-token flow
80
80
  freshly returned pair. Re-run the browser-assisted authorize step only when
81
81
  the refresh token itself is invalid or revoked.
82
82
 
83
+ The native SDK also exposes `signInWithPassword({ email, password, tenantId? })`
84
+ for member password sign-in. That method posts credentials to the native
85
+ password-grant endpoint, persists the returned access/refresh pair through the
86
+ starter's secure storage adapter, and returns a `NativeTokenPair`.
87
+ The generated Expo starter wires `tenantId` from `EXPO_PUBLIC_MW_TENANT_ID`, a
88
+ non-secret tenant UUID. Keep that configured and pass it through password and
89
+ browser-assisted authorize flows; tenant membership is still enforced by the
90
+ platform before a token pair is issued.
91
+
83
92
  The `tenant-app` web SDK / hosted-cookie path is **web-only**. The mobile app
84
93
  does not ride the `tenant-app` cookie jar, and it does not read or forward
85
94
  CSRF -- native uses the bearer token directly against the platform.
86
95
 
96
+ `@minutework/web-auth` and `@minutework/native-auth` intentionally share the
97
+ `signInWithPassword` name for developer ergonomics, but the mechanisms are
98
+ different. Web `signInWithPassword` sets/uses same-origin cookies and returns a
99
+ tenant customer session. Native `signInWithPassword` returns/stores a bearer
100
+ access/refresh token pair. Do not copy code between those surfaces without
101
+ switching SDKs and session expectations.
102
+
87
103
  ### Token binding (what is and isn't enforced)
88
104
 
89
105
  The token is bound to one tenant + membership, and that binding **is** enforced:
@@ -14,5 +14,10 @@
14
14
  # The app calls /api/v1/native/... here.
15
15
  EXPO_PUBLIC_MW_PLATFORM_BASE_URL=https://<your-platform-base-url>
16
16
 
17
+ # Non-secret tenant UUID this native app signs into. Native password sign-in
18
+ # requires an explicit tenant context, and the browser-assisted authorize flow
19
+ # sends this value too.
20
+ EXPO_PUBLIC_MW_TENANT_ID=00000000-0000-0000-0000-000000000000
21
+
17
22
  # Optional display name shown in the UI. Defaults to "MinuteWork".
18
23
  # EXPO_PUBLIC_MW_APP_NAME=MinuteWork
@@ -37,11 +37,17 @@ flow).
37
37
  ## Status
38
38
 
39
39
  `src/mw/client.ts` and `src/mw/session.ts` are **implemented**: a real
40
- browser-assisted PKCE device flow against the platform native session endpoints
41
- (`/api/v1/native/session/*`), with tokens persisted in the device keychain via
42
- `expo-secure-store`. Wire your UI against the documented client methods
43
- (`authorize` -> `exchange`, `loadSession`, `logout`). Set `app.json` `expo.scheme`
44
- to your own unique scheme that is the OAuth-style redirect target.
40
+ native password sign-in method plus a browser-assisted PKCE device flow against
41
+ the platform native session endpoints (`/api/v1/native/session/*`), with tokens
42
+ persisted in the device keychain via `expo-secure-store`. Wire your UI against
43
+ the documented client methods (`signInWithPassword`, `authorize` -> `exchange`,
44
+ `loadSession`, `logout`). `signInWithPassword` here returns/stores a native
45
+ bearer token pair; the same method name in `@minutework/web-auth` is
46
+ cookie-backed web auth and does not return tokens. Set
47
+ `EXPO_PUBLIC_MW_TENANT_ID` to the non-secret tenant UUID; the starter passes it
48
+ as tenant context for both password sign-in and browser-assisted authorize. Set
49
+ `app.json` `expo.scheme` to your own unique scheme — that is the OAuth-style
50
+ redirect target for the browser-assisted flow.
45
51
 
46
52
  The starter is standalone npm-owned app code, even when generated beside
47
53
  `tenant-app` in a root pnpm workspace. Run mobile commands from `mobile/` with
@@ -13,7 +13,7 @@ plain screens use `StyleSheet` only so there is nothing to rip out.
13
13
 
14
14
  | Path | Owner | Notes |
15
15
  | --- | --- | --- |
16
- | `src/mw/env.ts` | **MinuteWork substrate** | Validates `EXPO_PUBLIC_MW_PLATFORM_BASE_URL` (+ optional app name) |
16
+ | `src/mw/env.ts` | **MinuteWork substrate** | Validates `EXPO_PUBLIC_MW_PLATFORM_BASE_URL`, `EXPO_PUBLIC_MW_TENANT_ID` (+ optional app name) |
17
17
  | `src/mw/endpoints.ts` | **MinuteWork substrate** | Builds platform `/api/v1/native/...` URLs |
18
18
  | `src/mw/contracts.ts` | **MinuteWork substrate** | Zod schemas for native session + token payloads |
19
19
  | `src/mw/client.ts` | **MinuteWork substrate** | Native token client — real browser-assisted PKCE device flow |
@@ -43,6 +43,12 @@ This app authenticates as a **direct platform native client**:
43
43
  `Authorization: Bearer <access>`, and on `401` mints a fresh pair at
44
44
  `POST /api/v1/native/session/refresh/` (rotating — persist the new pair) before
45
45
  retrying. `POST /api/v1/native/session/logout/` revokes the token family.
46
+ - `mwClient.signInWithPassword({ email, password })` is also available for
47
+ native/member password sign-in. The starter injects
48
+ `EXPO_PUBLIC_MW_TENANT_ID` as `tenantId` because the native password-grant
49
+ endpoint requires explicit tenant context. It stores the returned
50
+ access/refresh pair through `src/mw/session.ts` and returns that native token
51
+ pair.
46
52
 
47
53
  This is **NOT** the Next.js `tenant-app` model. The tenant-app uses a
48
54
  server-owned **BFF session cookie** (`platform_session_bff`) because a browser
@@ -52,6 +58,11 @@ token instead of a cookie. **Do not** try to reuse the BFF cookie path here, and
52
58
  **do not** build a parallel/local auth stack (no JWT minting, no local user
53
59
  table) — the platform owns identity.
54
60
 
61
+ The web and native SDKs intentionally share the `signInWithPassword` name, but
62
+ the mechanism is different: `@minutework/web-auth` sets/uses a same-origin web
63
+ cookie and returns a tenant customer session; `@minutework/native-auth` stores
64
+ and returns a bearer-token pair for the native platform API.
65
+
55
66
  The token is bound to a single tenant + membership, and that binding **is**
56
67
  enforced. The optional `audience` and `device_id` fields are **informational
57
68
  only**: the platform carries them through the flow but does **not** validate or
@@ -61,9 +72,13 @@ compare them, so do not rely on them as a security boundary.
61
72
 
62
73
  `src/mw/client.ts` and `src/mw/session.ts` are **implemented**. The client:
63
74
 
64
- - generates a PKCE `code_verifier` + S256 `code_challenge` (`expo-crypto`),
75
+ - signs in with member credentials through `signInWithPassword(...)` when you
76
+ choose the native password flow, sending the configured tenant UUID,
77
+ - generates a PKCE `code_verifier` + S256 `code_challenge` (`expo-crypto`) when
78
+ you choose the browser-assisted flow,
65
79
  - opens the platform `authorize` URL in a system browser
66
- (`expo-web-browser`'s `openAuthSessionAsync`) with an anti-forgery `state`,
80
+ (`expo-web-browser`'s `openAuthSessionAsync`) with an anti-forgery `state`
81
+ and the configured tenant UUID,
67
82
  - captures the returned one-time `code` from the deep-link redirect, exchanges
68
83
  it (plus the `code_verifier`) for a token pair, and stores it in the device
69
84
  keychain (`expo-secure-store`),
@@ -84,18 +99,20 @@ The full flow is documented in the doc-comment at the top of `src/mw/client.ts`.
84
99
 
85
100
  ## Environment
86
101
 
87
- Fresh local development works without a `.env`: `src/mw/env.ts` defaults
88
- `EXPO_PUBLIC_MW_PLATFORM_BASE_URL` to `http://127.0.0.1:8000` so the app can
89
- boot immediately after install. For a physical device, a deployed platform, or
90
- anything you plan to ship, copy `.env.example` to `.env` and set:
102
+ `src/mw/env.ts` defaults `EXPO_PUBLIC_MW_PLATFORM_BASE_URL` to
103
+ `http://127.0.0.1:8000` when omitted, but native auth still requires a tenant
104
+ UUID. Copy `.env.example` to `.env` and set:
91
105
 
92
106
  ```
93
107
  EXPO_PUBLIC_MW_PLATFORM_BASE_URL=https://<your-platform-base-url>
108
+ EXPO_PUBLIC_MW_TENANT_ID=<tenant-uuid>
94
109
  ```
95
110
 
96
111
  Only `EXPO_PUBLIC_*` variables are bundled into the app, and they are **not
97
- secret** — never put keys/tokens in them. `src/mw/env.ts` validates this value
98
- at startup with zod.
112
+ secret** — never put keys/tokens in them. The tenant UUID is not a secret; it
113
+ only selects which tenant the platform should authenticate against. Platform
114
+ membership checks still decide whether the user can receive a native token.
115
+ `src/mw/env.ts` validates these values at startup with zod.
99
116
 
100
117
  ## Running locally
101
118
 
@@ -1,14 +1,21 @@
1
1
  // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
2
  //
3
3
  // This screen is intentionally plain and is meant to be REWRITTEN. It exists to
4
- // show the one integration seam you care about: kicking off MinuteWork's
4
+ // show the two native auth seams you care about: password sign-in and
5
5
  // browser-assisted native sign-in through `src/mw/client.ts`.
6
6
  //
7
- // Pressing "Sign in" runs the real device flow: authorize in a system browser ->
8
- // exchange the returned code for a platform bearer token pair -> route into the
9
- // authed stack. Wire your own UI/UX around this call.
7
+ // Both paths return/store a platform bearer token pair through the native auth
8
+ // client, then route into the authed stack. Wire your own UI/UX around these
9
+ // calls.
10
10
  import { useState } from "react";
11
- import { Alert, Pressable, StyleSheet, Text, View } from "react-native";
11
+ import {
12
+ Alert,
13
+ Pressable,
14
+ StyleSheet,
15
+ Text,
16
+ TextInput,
17
+ View,
18
+ } from "react-native";
12
19
  import { router } from "expo-router";
13
20
 
14
21
  import { mwClient } from "@/mw/client";
@@ -16,14 +23,13 @@ import { mwEnv } from "@/mw/env";
16
23
 
17
24
  export default function LoginScreen() {
18
25
  const [busy, setBusy] = useState(false);
26
+ const [email, setEmail] = useState("");
27
+ const [password, setPassword] = useState("");
19
28
 
20
- async function onSignIn() {
29
+ async function finishSignIn(action: () => Promise<unknown>) {
21
30
  setBusy(true);
22
31
  try {
23
- // Device flow: authorize (browser) -> exchange code+verifier for tokens
24
- // (stored in the keychain by the client), then route into the authed stack.
25
- const { code, codeVerifier, redirectUri } = await mwClient.authorize();
26
- await mwClient.exchange(code, codeVerifier, redirectUri);
32
+ await action();
27
33
  router.replace("/(app)");
28
34
  } catch (error) {
29
35
  Alert.alert(
@@ -35,22 +41,82 @@ export default function LoginScreen() {
35
41
  }
36
42
  }
37
43
 
44
+ async function onPasswordSignIn() {
45
+ await finishSignIn(() =>
46
+ mwClient.signInWithPassword({
47
+ email,
48
+ password,
49
+ tenantId: mwEnv.tenantId,
50
+ }),
51
+ );
52
+ }
53
+
54
+ async function onBrowserSignIn() {
55
+ await finishSignIn(async () => {
56
+ // Device flow: authorize (browser) -> exchange code+verifier for tokens
57
+ // (stored in the keychain by the client), then route into the authed stack.
58
+ const { code, codeVerifier, redirectUri } = await mwClient.authorize({
59
+ tenantId: mwEnv.tenantId,
60
+ });
61
+ await mwClient.exchange(code, codeVerifier, redirectUri);
62
+ });
63
+ }
64
+
38
65
  return (
39
66
  <View style={styles.container}>
40
67
  <Text style={styles.title}>{mwEnv.appName}</Text>
41
68
  <Text style={styles.subtitle}>Sign in to continue</Text>
42
69
 
70
+ <View style={styles.form}>
71
+ <TextInput
72
+ autoCapitalize="none"
73
+ autoComplete="email"
74
+ autoCorrect={false}
75
+ editable={!busy}
76
+ inputMode="email"
77
+ onChangeText={setEmail}
78
+ placeholder="Email"
79
+ style={styles.input}
80
+ textContentType="username"
81
+ value={email}
82
+ />
83
+ <TextInput
84
+ autoCapitalize="none"
85
+ editable={!busy}
86
+ onChangeText={setPassword}
87
+ placeholder="Password"
88
+ secureTextEntry
89
+ style={styles.input}
90
+ textContentType="password"
91
+ value={password}
92
+ />
93
+ </View>
94
+
43
95
  <Pressable
44
96
  accessibilityRole="button"
45
- disabled={busy}
46
- onPress={onSignIn}
97
+ disabled={busy || !email || !password}
98
+ onPress={onPasswordSignIn}
47
99
  style={({ pressed }) => [
48
100
  styles.button,
49
- (pressed || busy) && styles.buttonPressed,
101
+ (pressed || busy || !email || !password) && styles.buttonPressed,
50
102
  ]}
51
103
  >
52
104
  <Text style={styles.buttonText}>
53
- {busy ? "Opening sign in" : "Sign in with MinuteWork"}
105
+ {busy ? "Signing in..." : "Sign in with password"}
106
+ </Text>
107
+ </Pressable>
108
+
109
+ <Pressable
110
+ accessibilityRole="button"
111
+ disabled={busy}
112
+ onPress={onBrowserSignIn}
113
+ style={({ pressed }) => [
114
+ styles.secondaryButton,
115
+ (pressed || busy) && styles.buttonPressed,
116
+ ]}
117
+ >
118
+ <Text style={styles.secondaryButtonText}>
119
+ {busy ? "Opening..." : "Continue in browser"}
54
120
  </Text>
55
121
  </Pressable>
56
122
  </View>
@@ -74,11 +140,25 @@ const styles = StyleSheet.create({
74
140
  opacity: 0.7,
75
141
  marginBottom: 12,
76
142
  },
143
+ form: {
144
+ width: "100%",
145
+ gap: 10,
146
+ },
147
+ input: {
148
+ borderWidth: StyleSheet.hairlineWidth,
149
+ borderColor: "#d1d5db",
150
+ borderRadius: 10,
151
+ fontSize: 16,
152
+ paddingHorizontal: 14,
153
+ paddingVertical: 12,
154
+ },
77
155
  button: {
78
156
  backgroundColor: "#111827",
79
157
  paddingVertical: 14,
80
158
  paddingHorizontal: 24,
81
159
  borderRadius: 10,
160
+ width: "100%",
161
+ alignItems: "center",
82
162
  },
83
163
  buttonPressed: {
84
164
  opacity: 0.6,
@@ -88,4 +168,18 @@ const styles = StyleSheet.create({
88
168
  fontSize: 16,
89
169
  fontWeight: "600",
90
170
  },
171
+ secondaryButton: {
172
+ borderColor: "#111827",
173
+ borderRadius: 10,
174
+ borderWidth: StyleSheet.hairlineWidth,
175
+ paddingHorizontal: 24,
176
+ paddingVertical: 14,
177
+ width: "100%",
178
+ alignItems: "center",
179
+ },
180
+ secondaryButtonText: {
181
+ color: "#111827",
182
+ fontSize: 16,
183
+ fontWeight: "600",
184
+ },
91
185
  });
@@ -3,8 +3,10 @@ import * as Linking from "expo-linking";
3
3
  import * as WebBrowser from "expo-web-browser";
4
4
  import {
5
5
  createNativeAuthClient,
6
+ type NativeAuthorizeOptions,
6
7
  type NativeBrowser,
7
8
  type NativeCrypto,
9
+ type NativePasswordGrantOptions,
8
10
  } from "@minutework/native-auth";
9
11
 
10
12
  import { mwEnv } from "@/mw/env";
@@ -38,11 +40,27 @@ const nativeBrowser: NativeBrowser = {
38
40
  },
39
41
  };
40
42
 
41
- export const mwClient = createNativeAuthClient({
43
+ const nativeAuthClient = createNativeAuthClient({
42
44
  platformBaseUrl: mwEnv.platformBaseUrl,
43
45
  storage: mwSession,
44
46
  crypto: nativeCrypto,
45
47
  browser: nativeBrowser,
46
48
  });
47
49
 
50
+ export const mwClient = {
51
+ ...nativeAuthClient,
52
+ authorize(options: NativeAuthorizeOptions = {}) {
53
+ return nativeAuthClient.authorize({
54
+ ...options,
55
+ tenantId: mwEnv.tenantId,
56
+ });
57
+ },
58
+ signInWithPassword(options: NativePasswordGrantOptions) {
59
+ return nativeAuthClient.signInWithPassword({
60
+ ...options,
61
+ tenantId: mwEnv.tenantId,
62
+ });
63
+ },
64
+ } satisfies typeof nativeAuthClient;
65
+
48
66
  export type MwClient = typeof mwClient;
@@ -9,7 +9,8 @@
9
9
  //
10
10
  // SECURITY: EXPO_PUBLIC_* values ship inside the app bundle and are readable by
11
11
  // anyone with the binary. Only put non-secret configuration here (a base URL,
12
- // a display name). Never put API keys, client secrets, or tokens in EXPO_PUBLIC_*.
12
+ // tenant UUID, a display name). Never put API keys, client secrets, or tokens
13
+ // in EXPO_PUBLIC_*.
13
14
 
14
15
  import { z } from "zod";
15
16
 
@@ -33,6 +34,7 @@ const optionalNameSchema = z.preprocess((value) => {
33
34
 
34
35
  const envSchema = z.object({
35
36
  platformBaseUrl: baseUrlSchema,
37
+ tenantId: z.string().uuid(),
36
38
  appName: optionalNameSchema,
37
39
  });
38
40
 
@@ -45,6 +47,7 @@ const parsed = envSchema.safeParse({
45
47
  configuredPlatformBaseUrl.trim().length > 0
46
48
  ? configuredPlatformBaseUrl
47
49
  : DEFAULT_LOCAL_PLATFORM_BASE_URL,
50
+ tenantId: process.env.EXPO_PUBLIC_MW_TENANT_ID,
48
51
  appName: process.env.EXPO_PUBLIC_MW_APP_NAME,
49
52
  });
50
53
 
@@ -58,7 +61,8 @@ if (!parsed.success) {
58
61
 
59
62
  throw new Error(
60
63
  `Invalid Expo public environment for mobile-app: ${issues}. ` +
61
- "Set EXPO_PUBLIC_MW_PLATFORM_BASE_URL to the MinuteWork platform URL.",
64
+ "Set EXPO_PUBLIC_MW_PLATFORM_BASE_URL to the MinuteWork platform URL " +
65
+ "and EXPO_PUBLIC_MW_TENANT_ID to this native app's tenant UUID.",
62
66
  );
63
67
  }
64
68
 
@@ -14,5 +14,5 @@
14
14
  "reference/mwv3-dj6-docs/auth_and_credential_contract.md"
15
15
  ],
16
16
  "deployable": false,
17
- "notes": "Bring-your-own-UI Expo (React Native + Expo Router) starter. Direct platform native API client (bearer token to /api/v1/native/...), NOT a tenant-app BFF cookie client. Distribution is the developer's EAS pipeline, not `minutework deploy`. Only src/mw/ is MinuteWork substrate; everything else is developer-owned. src/mw/client.ts implements the real browser-assisted PKCE device flow against the platform native session endpoints; the OAuth-style redirect target is the app.json expo.scheme. This manifest is validated by tools/template/validate-template.mjs, not the strict shared template.schema.json (mobile is not a web/sidecar deployable kind)."
17
+ "notes": "Bring-your-own-UI Expo (React Native + Expo Router) starter. Direct platform native API client (bearer token to /api/v1/native/...), NOT a tenant-app BFF cookie client. Distribution is the developer's EAS pipeline, not `minutework deploy`. Only src/mw/ is MinuteWork substrate; everything else is developer-owned. src/mw/client.ts implements native password sign-in plus the real browser-assisted PKCE device flow against the platform native session endpoints; set EXPO_PUBLIC_MW_TENANT_ID to the non-secret tenant UUID so both flows send tenant context. The OAuth-style redirect target is the app.json expo.scheme. This manifest is validated by tools/template/validate-template.mjs, not the strict shared template.schema.json (mobile is not a web/sidecar deployable kind)."
18
18
  }