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.
- package/EXTERNAL_ALPHA.md +5 -0
- package/README.md +12 -4
- package/assets/claude-local/CLAUDE.md.template +21 -0
- package/assets/claude-local/skills/README.md +7 -0
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -0
- package/assets/claude-local/skills/capability-gap-reporting/SKILL.md +44 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +14 -0
- package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +16 -0
- package/assets/templates/mobile-app/.env.example +5 -0
- package/assets/templates/mobile-app/AGENTS.md +11 -5
- package/assets/templates/mobile-app/README.md +26 -9
- package/assets/templates/mobile-app/app/(auth)/login.tsx +108 -14
- package/assets/templates/mobile-app/src/mw/client.ts +19 -1
- package/assets/templates/mobile-app/src/mw/env.ts +6 -2
- package/assets/templates/mobile-app/template.json +1 -1
- package/assets/templates/next-tenant-app/README.md +7 -0
- package/assets/templates/next-tenant-app/package.json +1 -1
- package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +3 -3
- package/assets/templates/next-tenant-app/src/features/shell/components/authenticated-app-layout-shell.tsx +4 -9
- package/dist/cli-json.d.ts +1 -1
- package/dist/cli-json.js.map +1 -1
- package/dist/developer-client.d.ts +40 -0
- package/dist/developer-client.js +27 -0
- package/dist/developer-client.js.map +1 -1
- package/dist/gaps.d.ts +13 -0
- package/dist/gaps.js +340 -0
- package/dist/gaps.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- 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`,
|
|
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`),
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
41
|
-
(`/api/v1/native/session/*`), with tokens
|
|
42
|
-
`expo-secure-store`. Wire your UI against
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
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.
|
|
98
|
-
|
|
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
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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 {
|
|
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
|
|
29
|
+
async function finishSignIn(action: () => Promise<unknown>) {
|
|
21
30
|
setBusy(true);
|
|
22
31
|
try {
|
|
23
|
-
|
|
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={
|
|
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 ? "
|
|
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
|
-
|
|
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
|
|
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
|
}
|