minutework 0.1.20 → 0.1.22
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 +1 -1
- package/README.md +18 -5
- package/assets/claude-local/skills/README.md +1 -0
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -2
- package/assets/claude-local/skills/shell-architecture/SKILL.md +3 -0
- package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +105 -0
- package/assets/templates/mobile-app/.env.example +16 -0
- package/assets/templates/mobile-app/AGENTS.md +44 -0
- package/assets/templates/mobile-app/README.md +123 -0
- package/assets/templates/mobile-app/app/(app)/_layout.tsx +10 -0
- package/assets/templates/mobile-app/app/(app)/index.tsx +72 -0
- package/assets/templates/mobile-app/app/(auth)/login.tsx +91 -0
- package/assets/templates/mobile-app/app/_layout.tsx +15 -0
- package/assets/templates/mobile-app/app.json +31 -0
- package/assets/templates/mobile-app/babel.config.js +7 -0
- package/assets/templates/mobile-app/eas.json +24 -0
- package/assets/templates/mobile-app/expo-env.d.ts +5 -0
- package/assets/templates/mobile-app/metro.config.js +7 -0
- package/assets/templates/mobile-app/package.json +32 -0
- package/assets/templates/mobile-app/src/mw/client.ts +251 -0
- package/assets/templates/mobile-app/src/mw/contracts.ts +79 -0
- package/assets/templates/mobile-app/src/mw/endpoints.ts +42 -0
- package/assets/templates/mobile-app/src/mw/env.ts +59 -0
- package/assets/templates/mobile-app/src/mw/session.ts +50 -0
- package/assets/templates/mobile-app/template.json +18 -0
- package/assets/templates/mobile-app/tools/template/validate-template.mjs +69 -0
- package/assets/templates/mobile-app/tsconfig.json +16 -0
- package/assets/templates/next-tenant-app/README.md +4 -1
- package/dist/deploy.js +43 -5
- package/dist/deploy.js.map +1 -1
- package/dist/developer-client.d.ts +22 -0
- package/dist/developer-client.js +9 -0
- package/dist/developer-client.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/init-prompt.js +10 -2
- package/dist/init-prompt.js.map +1 -1
- package/dist/init.d.ts +2 -1
- package/dist/init.js +59 -2
- package/dist/init.js.map +1 -1
- package/dist/publish.d.ts +22 -6
- package/dist/publish.js +222 -24
- package/dist/publish.js.map +1 -1
- package/dist/workspace-bootstrap.d.ts +1 -0
- package/dist/workspace-bootstrap.js.map +1 -1
- package/package.json +3 -3
- package/vendor/workspace-mcp/types.d.ts +2 -0
package/EXTERNAL_ALPHA.md
CHANGED
|
@@ -140,7 +140,7 @@ The CLI does not fabricate success. If the backend cannot materialize a hosted p
|
|
|
140
140
|
|
|
141
141
|
## Machine-readable output (`--json`)
|
|
142
142
|
|
|
143
|
-
`validate`, `compile`, `codegen`,
|
|
143
|
+
`validate`, `compile`, `codegen`, `deploy --preview`, and `publish` accept `--json` for unattended, agent-driven use. The command prints a single result envelope to stdout (`{ cliJsonVersion, command, ok, status, result, error }`) and nothing else; `ok` mirrors the exit code (fail-closed). `deploy --preview --json` and `publish --json` never prompt — pair them with `--yes` to authorize, otherwise they return `status: "confirmation_required"` and exit non-zero. For `publish`, a rejected publication-review attestation is reported as `ok: false` with the gate findings in `result.findings` (a real review outcome, not a transport error). The automatic bug report is suppressed in `--json` mode.
|
|
144
144
|
|
|
145
145
|
## Failure semantics
|
|
146
146
|
|
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# `minutework`
|
|
2
2
|
|
|
3
|
-
MinuteWork CLI for initializing a workspace, authenticating against a MinuteWork platform, linking a repo to a tenant-scoped public-site property, launching developer-local session broker flows, running local preview/test loops,
|
|
3
|
+
MinuteWork CLI for initializing a workspace, authenticating against a MinuteWork platform, linking a repo to a tenant-scoped public-site property, launching developer-local session broker flows, running local preview/test loops, submitting hosted preview deploys, and publishing app packs to the marketplace through the publication-review gate.
|
|
4
4
|
|
|
5
5
|
## External alpha scope
|
|
6
6
|
|
|
7
7
|
The current external alpha is intentionally narrow:
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- Starters: `tenant-app` (the deployable web surface) and `mobile` (init-only)
|
|
10
10
|
- Developer-local broker surface: `minutework session start|resume|status`
|
|
11
11
|
- Hosted release class: `ssr_container`
|
|
12
12
|
- Deploy surface: `minutework deploy --preview`
|
|
13
|
-
- Deferred: `--live`, additional local coding engines, sidecar/runtime-backed deploys, marketplace
|
|
14
|
-
-
|
|
13
|
+
- Deferred: `--live`, additional local coding engines, sidecar/runtime-backed deploys, and the commercial marketplace listing/pricing layer
|
|
14
|
+
- 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
|
|
15
15
|
|
|
16
16
|
The shipped `tenant-app` starter is the combined web surface: public routes at
|
|
17
17
|
the root plus a private `/app` workspace. Public-site content follows the
|
|
@@ -44,7 +44,20 @@ npx minutework login
|
|
|
44
44
|
npx minutework link
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
After `link`, the current alpha has two supported lanes.
|
|
47
|
+
After `link`, the current alpha has two supported lanes (both `tenant-app`).
|
|
48
|
+
|
|
49
|
+
For a native mobile client, scaffold the **`mobile`** starter instead:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx minutework init my-app --starter mobile
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It is **init-only** and bring-your-own-UI: a minimal Expo (React Native) client
|
|
56
|
+
that authenticates **directly** against the platform with a native device-flow
|
|
57
|
+
session token (PKCE authorize -> exchange -> bearer + refresh in the device
|
|
58
|
+
keychain), not the `tenant-app` BFF cookie path. There is no `link`/`deploy`
|
|
59
|
+
lane for it — distribution is your own EAS pipeline (`eas build`/`eas submit`,
|
|
60
|
+
EAS Update), not `minutework deploy`.
|
|
48
61
|
|
|
49
62
|
### Developer-local broker lane
|
|
50
63
|
|
|
@@ -32,6 +32,7 @@ Generated-workspace-first guidance should live here, especially:
|
|
|
32
32
|
- `vuilder-public-site-authoring/SKILL.md`
|
|
33
33
|
- `workspace-guidance-refresh/SKILL.md`
|
|
34
34
|
- `shell-architecture/SKILL.md`
|
|
35
|
+
- `standalone-mobile-client/SKILL.md`
|
|
35
36
|
- `runtime-capability-inventory/SKILL.md`
|
|
36
37
|
- `layering-and-import-modes/SKILL.md`
|
|
37
38
|
- `capability-gap-reporting/SKILL.md`
|
|
@@ -19,9 +19,17 @@ the monorepo or a live tenant runtime.
|
|
|
19
19
|
- app packs
|
|
20
20
|
- skills
|
|
21
21
|
- tenant overlays
|
|
22
|
-
- `tenant-app` and optional `
|
|
23
|
-
tenant product, not the whole architecture.
|
|
22
|
+
- `tenant-app`, optional `sidecar`, and the optional `mobile` starter are
|
|
23
|
+
implementation surfaces inside the tenant product, not the whole architecture.
|
|
24
24
|
- `tenant-app` is the combined public plus private web starter.
|
|
25
|
+
- A mobile / native client (Expo, React Native, native iOS/Android) is a
|
|
26
|
+
standalone-client exception authored in the `mobile` starter, not in
|
|
27
|
+
`tenant-app` or `sidecar`. It is a direct platform API client that consumes
|
|
28
|
+
the platform native-token API (`/api/v1/native/...`) directly with a
|
|
29
|
+
platform-issued native session token, rather than the `tenant-app` BFF cookie
|
|
30
|
+
session.
|
|
31
|
+
- If the `mobile` starter is enabled (`starters.mobile.enabled`), read
|
|
32
|
+
`standalone-mobile-client/SKILL.md` before proposing the mobile surface.
|
|
25
33
|
- `vuilder-public-site` is a Vuilder-owned public-site authoring workspace for
|
|
26
34
|
landing, blog, and onboarding routes backed by public-dj CMS manifests.
|
|
27
35
|
- If `template_kind` is `vuilder-public-site`, or the workspace profile says
|
|
@@ -44,6 +44,9 @@ threads, channels, inboxes, guest collaboration, approvals, dashboards, or
|
|
|
44
44
|
- the surface is public marketing
|
|
45
45
|
- the surface is public intake for non-members
|
|
46
46
|
- the surface is an embeddable widget
|
|
47
|
+
- the surface is a mobile app (Expo / React Native / native iOS/Android) ->
|
|
48
|
+
author in the `mobile` starter, not `tenant-app`; see
|
|
49
|
+
`standalone-mobile-client/SKILL.md`
|
|
47
50
|
- If shell fit is correct but this generated workspace cannot directly author
|
|
48
51
|
the shell extension, keep the shell-first recommendation and record a
|
|
49
52
|
capability gap instead of silently converting the request into standalone
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: standalone-mobile-client
|
|
3
|
+
description: "Building a native mobile app (Expo / React Native / native iOS/Android). It is a standalone-client exception authored in the mobile starter as a direct platform API client using a native session token, not in tenant-app or sidecar."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Standalone Mobile Client
|
|
7
|
+
|
|
8
|
+
Use this skill when the request is "can I build a mobile app?", or anything
|
|
9
|
+
that involves a native iOS/Android surface (Expo, React Native, or fully native).
|
|
10
|
+
The answer is yes, and the shape is fixed. Do not flip-flop on it.
|
|
11
|
+
|
|
12
|
+
## Where it lives
|
|
13
|
+
|
|
14
|
+
- A native mobile app is a **standalone-client exception**. It is authored in
|
|
15
|
+
the `mobile` starter (`destinationDirectory: mobile`), enabled via the
|
|
16
|
+
workspace config key `starters.mobile = { enabled: true, path: "mobile" }`.
|
|
17
|
+
- It is **not** part of `tenant-app`, and **not** part of `sidecar`. Do not put
|
|
18
|
+
native code, Expo config, or React Native screens inside either of those
|
|
19
|
+
surfaces.
|
|
20
|
+
- `tenant-app` stays web-only. The mobile app is a sibling client, not a tab or
|
|
21
|
+
route inside the web BFF.
|
|
22
|
+
|
|
23
|
+
## What it is
|
|
24
|
+
|
|
25
|
+
- The mobile app is a **direct platform API client**. It talks to the platform
|
|
26
|
+
HTTP API at `/api/v1/native/session/...` and nothing else. It does not go
|
|
27
|
+
through the `tenant-app` BFF.
|
|
28
|
+
- Authentication uses a platform-issued **native session token** -- a distinct
|
|
29
|
+
principal class, the "native app session token holder". This is exactly what
|
|
30
|
+
the auth contract anticipates: non-browser clients must mint explicit scoped
|
|
31
|
+
tokens after browser-assisted login or a device flow
|
|
32
|
+
(`reference/mwv3-dj6-docs/auth_and_credential_contract.md`, Rule 7).
|
|
33
|
+
- The app reads the platform base URL from `EXPO_PUBLIC_MW_PLATFORM_BASE_URL`.
|
|
34
|
+
|
|
35
|
+
## Endpoints (shipped)
|
|
36
|
+
|
|
37
|
+
All native endpoints live under `/api/v1/native/session/`:
|
|
38
|
+
|
|
39
|
+
- `authorize/` -- browser-assisted consent (GET renders the login/consent page
|
|
40
|
+
or redirects to the onboarding host; POST confirms and 302-redirects to the
|
|
41
|
+
app `redirect_uri` with `?code=...&state=...`).
|
|
42
|
+
- `token-exchange/` -- POST `{code, code_verifier, redirect_uri}` -> `{access,
|
|
43
|
+
refresh, expires_at, ...}`. No bearer required.
|
|
44
|
+
- `refresh/` -- POST `{refresh_token}` -> a new `{access, refresh}` pair. No
|
|
45
|
+
bearer required.
|
|
46
|
+
- `me/` and `context/` -- GET (bearer) -> the tenant session payload for the
|
|
47
|
+
bound user/tenant. `context/` is currently identical to `me/`.
|
|
48
|
+
- `logout/` -- POST (bearer) -> revokes the presented token's access/refresh
|
|
49
|
+
family.
|
|
50
|
+
|
|
51
|
+
## Auth flow
|
|
52
|
+
|
|
53
|
+
The native token flow mirrors the existing CLI developer-token flow
|
|
54
|
+
(PKCE authorize -> exchange -> opaque scoped tokens):
|
|
55
|
+
|
|
56
|
+
1. **Authorize (browser-assisted device flow):** the app generates a PKCE
|
|
57
|
+
verifier and opens `GET /api/v1/native/session/authorize/` (with
|
|
58
|
+
`code_challenge`, `state`, `redirect_uri`) in a system browser. The user logs
|
|
59
|
+
in and confirms on the platform; the platform POSTs the confirmation and
|
|
60
|
+
302-redirects back to the app's `redirect_uri` with a one-time `code` and the
|
|
61
|
+
`state`.
|
|
62
|
+
2. **Exchange:** the app POSTs that one-time `code` plus the matching PKCE
|
|
63
|
+
`code_verifier` and the same `redirect_uri` to
|
|
64
|
+
`/api/v1/native/session/token-exchange/`, receiving an **access token +
|
|
65
|
+
refresh token** (opaque, scoped; access ~15 min, refresh ~30 days).
|
|
66
|
+
3. **Store:** both tokens are persisted in `expo-secure-store` (the device
|
|
67
|
+
keystore), never in plain React/web state.
|
|
68
|
+
4. **Call:** every request to `/api/v1/native/session/...` carries
|
|
69
|
+
`Authorization: Bearer <access>`.
|
|
70
|
+
5. **Refresh:** on access-token expiry (`401`), POST the refresh token to
|
|
71
|
+
`/api/v1/native/session/refresh/` to mint a new pair, then retry. Refresh is
|
|
72
|
+
**rotating** -- the old access/refresh family is revoked, so persist the
|
|
73
|
+
freshly returned pair. Re-run the browser-assisted authorize step only when
|
|
74
|
+
the refresh token itself is invalid or revoked.
|
|
75
|
+
|
|
76
|
+
The browser/BFF cookie path belongs to `tenant-app` and is **web-only**. The
|
|
77
|
+
mobile app does not ride the `tenant-app` cookie jar, and it does not read or
|
|
78
|
+
forward CSRF -- native uses the bearer token directly against the platform.
|
|
79
|
+
|
|
80
|
+
### Token binding (what is and isn't enforced)
|
|
81
|
+
|
|
82
|
+
The token is bound to one tenant + membership, and that binding **is** enforced:
|
|
83
|
+
a request that names a different tenant is rejected, and a token whose membership
|
|
84
|
+
has been deactivated stops authenticating. The optional `audience` and
|
|
85
|
+
`device_id` fields are **informational only** -- the platform carries them
|
|
86
|
+
through authorize -> exchange (and preserves them across refresh) but does **not**
|
|
87
|
+
validate or compare them. Do not treat `audience` or `device_id` as a security
|
|
88
|
+
boundary.
|
|
89
|
+
|
|
90
|
+
## What not to do
|
|
91
|
+
|
|
92
|
+
- Do not build a parallel auth stack.
|
|
93
|
+
- Do not mint a JWT inside the app or stand up a local user table / token issuer.
|
|
94
|
+
- Do not expose access or refresh tokens to web pages or generic React state;
|
|
95
|
+
keep them in `expo-secure-store`.
|
|
96
|
+
- Do not route the mobile app through the `tenant-app` BFF cookie session.
|
|
97
|
+
- Do not read or forward CSRF from native; bearer-token auth does not use it.
|
|
98
|
+
- Do not put native code inside `tenant-app` or `sidecar`.
|
|
99
|
+
|
|
100
|
+
## Distribution
|
|
101
|
+
|
|
102
|
+
- The `mobile` starter is **init-only**. Scaffolding it lands the app in the
|
|
103
|
+
workspace; it does not deploy it.
|
|
104
|
+
- Distribution is the developer's own pipeline: EAS Build, TestFlight, and the
|
|
105
|
+
Play Store. It is **not** `minutework deploy`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to `.env` and set the values for your environment.
|
|
4
|
+
#
|
|
5
|
+
# Only EXPO_PUBLIC_* variables are exposed to the app bundle. They are NOT
|
|
6
|
+
# secret — anyone with the app binary can read them. Put only non-secret config
|
|
7
|
+
# here. Never add API keys, client secrets, or tokens as EXPO_PUBLIC_* values;
|
|
8
|
+
# the platform issues short-lived bearer tokens at runtime via the native
|
|
9
|
+
# device flow.
|
|
10
|
+
|
|
11
|
+
# Base URL of the MinuteWork platform this app talks to directly
|
|
12
|
+
# (e.g. https://platform.minutework.dev). The app calls /api/v1/native/... here.
|
|
13
|
+
EXPO_PUBLIC_MW_PLATFORM_BASE_URL=https://<your-platform-base-url>
|
|
14
|
+
|
|
15
|
+
# Optional display name shown in the UI. Defaults to "MinuteWork".
|
|
16
|
+
# EXPO_PUBLIC_MW_APP_NAME=MinuteWork
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AGENTS.md — mobile-app starter
|
|
2
|
+
|
|
3
|
+
You are working in a **bring-your-own-UI Expo (React Native + Expo Router)**
|
|
4
|
+
app. **You own all UI/UX.** The only MinuteWork-managed code is the thin
|
|
5
|
+
substrate under `src/mw/`.
|
|
6
|
+
|
|
7
|
+
## The one rule
|
|
8
|
+
|
|
9
|
+
**Only `src/mw/` is MinuteWork substrate.** Build your product in `app/` and your
|
|
10
|
+
own components/modules. Do not move product logic into `src/mw/`, and do not
|
|
11
|
+
build a parallel/local auth stack — the platform owns identity. The mobile app
|
|
12
|
+
is a **direct platform native API client** (bearer token to `/api/v1/native/...`),
|
|
13
|
+
**not** a `tenant-app` BFF cookie client and **not** a `sidecar`.
|
|
14
|
+
|
|
15
|
+
## Use your IDE's Expo skills
|
|
16
|
+
|
|
17
|
+
For the native craft (UI, data, build/release), lean on your local IDE skills:
|
|
18
|
+
|
|
19
|
+
- `building-native-ui` — Expo Router UI, styling, navigation, animations
|
|
20
|
+
- `native-data-fetching` — fetch/React Query/SWR, caching, offline, loaders
|
|
21
|
+
- `expo-deployment` — App Store / Play Store / EAS distribution
|
|
22
|
+
- `expo-dev-client` — custom dev client (needed for the browser-assisted auth flow)
|
|
23
|
+
- `expo-tailwind-setup` — NativeWind/Tailwind if you want utility styling
|
|
24
|
+
- `push-notification-setup` — APNs/FCM push
|
|
25
|
+
- `upgrading-expo` — SDK upgrades and dependency fixes
|
|
26
|
+
- `expo-cicd-workflows` — EAS build/deploy pipelines (`.eas/workflows/`)
|
|
27
|
+
|
|
28
|
+
## MinuteWork integration shape
|
|
29
|
+
|
|
30
|
+
For how this app fits the platform (the standalone-client exception, the native
|
|
31
|
+
session token, and the device flow), read the Builder skill
|
|
32
|
+
**`standalone-mobile-client`** (in the Builder bundle) and the auth contract
|
|
33
|
+
`reference/mwv3-dj6-docs/auth_and_credential_contract.md` (Rule 7: non-browser
|
|
34
|
+
clients mint explicit scoped tokens after browser-assisted login / a device
|
|
35
|
+
flow).
|
|
36
|
+
|
|
37
|
+
## Status
|
|
38
|
+
|
|
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.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Mobile App Starter (Expo, bring-your-own-UI)
|
|
2
|
+
|
|
3
|
+
A minimal **Expo (React Native + Expo Router)** starter for building a native
|
|
4
|
+
MinuteWork client. **You own all of the UI/UX.** The only MinuteWork-managed
|
|
5
|
+
code is the thin substrate under `src/mw/`. Everything else in this template is
|
|
6
|
+
a deliberately plain placeholder meant to be replaced.
|
|
7
|
+
|
|
8
|
+
There is **no MinuteWork design system here** — bring your own. Style with
|
|
9
|
+
whatever you like (plain `StyleSheet`, NativeWind/Tailwind, Tamagui, etc.). The
|
|
10
|
+
plain screens use `StyleSheet` only so there is nothing to rip out.
|
|
11
|
+
|
|
12
|
+
## What's substrate vs. yours
|
|
13
|
+
|
|
14
|
+
| Path | Owner | Notes |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `src/mw/env.ts` | **MinuteWork substrate** | Validates `EXPO_PUBLIC_MW_PLATFORM_BASE_URL` (+ optional app name) |
|
|
17
|
+
| `src/mw/endpoints.ts` | **MinuteWork substrate** | Builds platform `/api/v1/native/...` URLs |
|
|
18
|
+
| `src/mw/contracts.ts` | **MinuteWork substrate** | Zod schemas for native session + token payloads |
|
|
19
|
+
| `src/mw/client.ts` | **MinuteWork substrate** | Native token client — real browser-assisted PKCE device flow |
|
|
20
|
+
| `src/mw/session.ts` | **MinuteWork substrate** | Secure-store token wrapper (`expo-secure-store`) |
|
|
21
|
+
| `tools/template/` | **MinuteWork substrate** | Template-governance tooling, not shipped app code |
|
|
22
|
+
| `app/**` | **You** | Expo Router screens — replace freely |
|
|
23
|
+
| `package.json`, `app.json`, `eas.json` | **You** | App config — replace freely |
|
|
24
|
+
| `tsconfig.json`, `babel.config.js`, `metro.config.js` | **You** | Tooling config — replace freely |
|
|
25
|
+
|
|
26
|
+
Rule of thumb: **only `src/mw/` is MinuteWork substrate.** If you find yourself
|
|
27
|
+
editing `src/mw/` to add product behavior, that behavior probably belongs in
|
|
28
|
+
your own `app/` / components instead.
|
|
29
|
+
|
|
30
|
+
## Auth model (read this)
|
|
31
|
+
|
|
32
|
+
This app authenticates as a **direct platform native client**:
|
|
33
|
+
|
|
34
|
+
- The platform issues a **bearer token** to the device through a
|
|
35
|
+
**browser-assisted PKCE device flow**: `GET /api/v1/native/session/authorize/`
|
|
36
|
+
(with a PKCE `code_challenge`) opens in a system browser; after login/consent
|
|
37
|
+
the platform redirects back with a one-time `code`, which you exchange at
|
|
38
|
+
`POST /api/v1/native/session/token-exchange/` (with the PKCE `code_verifier`)
|
|
39
|
+
for an `{access, refresh, expires_at}` pair, then store in the device keychain
|
|
40
|
+
via `expo-secure-store`.
|
|
41
|
+
- The app then calls the platform **directly** (e.g.
|
|
42
|
+
`GET /api/v1/native/session/me/` or `.../context/`) with
|
|
43
|
+
`Authorization: Bearer <access>`, and on `401` mints a fresh pair at
|
|
44
|
+
`POST /api/v1/native/session/refresh/` (rotating — persist the new pair) before
|
|
45
|
+
retrying. `POST /api/v1/native/session/logout/` revokes the token family.
|
|
46
|
+
|
|
47
|
+
This is **NOT** the Next.js `tenant-app` model. The tenant-app uses a
|
|
48
|
+
server-owned **BFF session cookie** (`platform_session_bff`) because a browser
|
|
49
|
+
can't safely hold tokens. A native app has secure device storage and no
|
|
50
|
+
per-app server in front of it, so it talks to the platform API directly with a
|
|
51
|
+
token instead of a cookie. **Do not** try to reuse the BFF cookie path here, and
|
|
52
|
+
**do not** build a parallel/local auth stack (no JWT minting, no local user
|
|
53
|
+
table) — the platform owns identity.
|
|
54
|
+
|
|
55
|
+
The token is bound to a single tenant + membership, and that binding **is**
|
|
56
|
+
enforced. The optional `audience` and `device_id` fields are **informational
|
|
57
|
+
only**: the platform carries them through the flow but does **not** validate or
|
|
58
|
+
compare them, so do not rely on them as a security boundary.
|
|
59
|
+
|
|
60
|
+
### The auth client is real — configure your redirect scheme
|
|
61
|
+
|
|
62
|
+
`src/mw/client.ts` and `src/mw/session.ts` are **implemented**. The client:
|
|
63
|
+
|
|
64
|
+
- generates a PKCE `code_verifier` + S256 `code_challenge` (`expo-crypto`),
|
|
65
|
+
- opens the platform `authorize` URL in a system browser
|
|
66
|
+
(`expo-web-browser`'s `openAuthSessionAsync`) with an anti-forgery `state`,
|
|
67
|
+
- captures the returned one-time `code` from the deep-link redirect, exchanges
|
|
68
|
+
it (plus the `code_verifier`) for a token pair, and stores it in the device
|
|
69
|
+
keychain (`expo-secure-store`),
|
|
70
|
+
- sends `Authorization: Bearer <access>` on every platform call and
|
|
71
|
+
auto-refreshes once on a `401`, and
|
|
72
|
+
- revokes + clears local storage on `logout()`.
|
|
73
|
+
|
|
74
|
+
The redirect target is your app's deep-link scheme. The starter ships
|
|
75
|
+
`"scheme": "mobileapp"` in `app.json`, so the redirect is
|
|
76
|
+
`mobileapp://auth/native-callback` (built at runtime via `expo-linking`'s
|
|
77
|
+
`createURL`). **Set `app.json` `expo.scheme` to your own unique scheme** before
|
|
78
|
+
shipping; the client follows whatever you configure, and a unique scheme avoids
|
|
79
|
+
collisions with other apps that could intercept the callback. The browser-based
|
|
80
|
+
flow needs a custom dev client (not stock Expo Go) — see the `expo-dev-client`
|
|
81
|
+
IDE skill.
|
|
82
|
+
|
|
83
|
+
The full flow is documented in the doc-comment at the top of `src/mw/client.ts`.
|
|
84
|
+
|
|
85
|
+
## Environment
|
|
86
|
+
|
|
87
|
+
Copy `.env.example` to `.env` and set:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
EXPO_PUBLIC_MW_PLATFORM_BASE_URL=https://<your-platform-base-url>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Only `EXPO_PUBLIC_*` variables are bundled into the app, and they are **not
|
|
94
|
+
secret** — never put keys/tokens in them. `src/mw/env.ts` validates this value
|
|
95
|
+
at startup with zod and fails fast if it's missing or not a URL.
|
|
96
|
+
|
|
97
|
+
## Running locally
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
npm install # or pnpm/yarn/bun
|
|
101
|
+
npm run start # Expo dev server (then press i / a, or scan with Expo Go)
|
|
102
|
+
npm run ios
|
|
103
|
+
npm run android
|
|
104
|
+
npm run typecheck # tsc --noEmit
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The native auth flow opens a system browser, which needs a custom dev client
|
|
108
|
+
rather than stock Expo Go; see the `expo-dev-client` IDE skill.
|
|
109
|
+
|
|
110
|
+
## Distribution
|
|
111
|
+
|
|
112
|
+
**Distribution is your EAS pipeline, not `minutework deploy`.** This starter is
|
|
113
|
+
not a hosted web/sidecar deployable; it produces native binaries. Build and ship
|
|
114
|
+
with EAS (`eas build`, `eas submit`, EAS Update). `eas.json` ships minimal
|
|
115
|
+
`preview` and `production` profiles to start from. See the `expo-deployment` and
|
|
116
|
+
`expo-cicd-workflows` IDE skills.
|
|
117
|
+
|
|
118
|
+
## Template manifest
|
|
119
|
+
|
|
120
|
+
`template.json` declares `template_kind: "mobile-app"` and `deployable: false`.
|
|
121
|
+
Because mobile is not a web/sidecar deployable, it is validated by
|
|
122
|
+
`tools/template/validate-template.mjs` (run `node tools/template/validate-template.mjs`),
|
|
123
|
+
**not** the strict shared `runtime/builder/templates/template.schema.json`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
|
|
2
|
+
//
|
|
3
|
+
// Authed area layout. This is where you'd guard on a loaded MinuteWork session
|
|
4
|
+
// (via `mwClient.loadSession()`) and redirect to /(auth)/login when there is no
|
|
5
|
+
// valid token. Left as a plain Stack so you can build your own gating/UX.
|
|
6
|
+
import { Stack } from "expo-router";
|
|
7
|
+
|
|
8
|
+
export default function AppLayout() {
|
|
9
|
+
return <Stack screenOptions={{ headerShown: false }} />;
|
|
10
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
|
|
2
|
+
//
|
|
3
|
+
// Trivial authed screen. Replace with your real product home. Build your data
|
|
4
|
+
// fetching against the platform native API using the bearer token from
|
|
5
|
+
// `src/mw/` (see the `native-data-fetching` IDE skill).
|
|
6
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
7
|
+
import { router } from "expo-router";
|
|
8
|
+
|
|
9
|
+
import { mwClient } from "@/mw/client";
|
|
10
|
+
|
|
11
|
+
export default function HomeScreen() {
|
|
12
|
+
async function onSignOut() {
|
|
13
|
+
try {
|
|
14
|
+
await mwClient.logout();
|
|
15
|
+
} finally {
|
|
16
|
+
router.replace("/(auth)/login");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<View style={styles.container}>
|
|
22
|
+
<Text style={styles.title}>You are in.</Text>
|
|
23
|
+
<Text style={styles.body}>
|
|
24
|
+
Replace this screen with your product. Only `src/mw/` is MinuteWork
|
|
25
|
+
substrate — everything else is yours.
|
|
26
|
+
</Text>
|
|
27
|
+
|
|
28
|
+
<Pressable
|
|
29
|
+
accessibilityRole="button"
|
|
30
|
+
onPress={onSignOut}
|
|
31
|
+
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
|
|
32
|
+
>
|
|
33
|
+
<Text style={styles.buttonText}>Sign out</Text>
|
|
34
|
+
</Pressable>
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create({
|
|
40
|
+
container: {
|
|
41
|
+
flex: 1,
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
justifyContent: "center",
|
|
44
|
+
padding: 24,
|
|
45
|
+
gap: 12,
|
|
46
|
+
},
|
|
47
|
+
title: {
|
|
48
|
+
fontSize: 24,
|
|
49
|
+
fontWeight: "700",
|
|
50
|
+
},
|
|
51
|
+
body: {
|
|
52
|
+
fontSize: 15,
|
|
53
|
+
opacity: 0.7,
|
|
54
|
+
textAlign: "center",
|
|
55
|
+
},
|
|
56
|
+
button: {
|
|
57
|
+
marginTop: 16,
|
|
58
|
+
paddingVertical: 12,
|
|
59
|
+
paddingHorizontal: 20,
|
|
60
|
+
borderRadius: 10,
|
|
61
|
+
borderWidth: 1,
|
|
62
|
+
borderColor: "#111827",
|
|
63
|
+
},
|
|
64
|
+
buttonPressed: {
|
|
65
|
+
opacity: 0.6,
|
|
66
|
+
},
|
|
67
|
+
buttonText: {
|
|
68
|
+
fontSize: 15,
|
|
69
|
+
fontWeight: "600",
|
|
70
|
+
color: "#111827",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
|
|
2
|
+
//
|
|
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
|
|
5
|
+
// browser-assisted native sign-in through `src/mw/client.ts`.
|
|
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.
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { Alert, Pressable, StyleSheet, Text, View } from "react-native";
|
|
12
|
+
import { router } from "expo-router";
|
|
13
|
+
|
|
14
|
+
import { mwClient } from "@/mw/client";
|
|
15
|
+
import { mwEnv } from "@/mw/env";
|
|
16
|
+
|
|
17
|
+
export default function LoginScreen() {
|
|
18
|
+
const [busy, setBusy] = useState(false);
|
|
19
|
+
|
|
20
|
+
async function onSignIn() {
|
|
21
|
+
setBusy(true);
|
|
22
|
+
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);
|
|
27
|
+
router.replace("/(app)");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
Alert.alert(
|
|
30
|
+
"Sign in unavailable",
|
|
31
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
32
|
+
);
|
|
33
|
+
} finally {
|
|
34
|
+
setBusy(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={styles.container}>
|
|
40
|
+
<Text style={styles.title}>{mwEnv.appName}</Text>
|
|
41
|
+
<Text style={styles.subtitle}>Sign in to continue</Text>
|
|
42
|
+
|
|
43
|
+
<Pressable
|
|
44
|
+
accessibilityRole="button"
|
|
45
|
+
disabled={busy}
|
|
46
|
+
onPress={onSignIn}
|
|
47
|
+
style={({ pressed }) => [
|
|
48
|
+
styles.button,
|
|
49
|
+
(pressed || busy) && styles.buttonPressed,
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
<Text style={styles.buttonText}>
|
|
53
|
+
{busy ? "Opening sign in…" : "Sign in with MinuteWork"}
|
|
54
|
+
</Text>
|
|
55
|
+
</Pressable>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const styles = StyleSheet.create({
|
|
61
|
+
container: {
|
|
62
|
+
flex: 1,
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
padding: 24,
|
|
66
|
+
gap: 12,
|
|
67
|
+
},
|
|
68
|
+
title: {
|
|
69
|
+
fontSize: 28,
|
|
70
|
+
fontWeight: "700",
|
|
71
|
+
},
|
|
72
|
+
subtitle: {
|
|
73
|
+
fontSize: 16,
|
|
74
|
+
opacity: 0.7,
|
|
75
|
+
marginBottom: 12,
|
|
76
|
+
},
|
|
77
|
+
button: {
|
|
78
|
+
backgroundColor: "#111827",
|
|
79
|
+
paddingVertical: 14,
|
|
80
|
+
paddingHorizontal: 24,
|
|
81
|
+
borderRadius: 10,
|
|
82
|
+
},
|
|
83
|
+
buttonPressed: {
|
|
84
|
+
opacity: 0.6,
|
|
85
|
+
},
|
|
86
|
+
buttonText: {
|
|
87
|
+
color: "#ffffff",
|
|
88
|
+
fontSize: 16,
|
|
89
|
+
fontWeight: "600",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
|
|
2
|
+
import { Stack } from "expo-router";
|
|
3
|
+
import { StatusBar } from "expo-status-bar";
|
|
4
|
+
|
|
5
|
+
export default function RootLayout() {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
9
|
+
<Stack.Screen name="(auth)/login" />
|
|
10
|
+
<Stack.Screen name="(app)" />
|
|
11
|
+
</Stack>
|
|
12
|
+
<StatusBar style="auto" />
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.",
|
|
3
|
+
"//scheme": "expo.scheme is the OAuth-style redirect target for the native auth device flow (src/mw/client.ts builds <scheme>://auth/native-callback). Set a unique scheme before shipping.",
|
|
4
|
+
"expo": {
|
|
5
|
+
"name": "mobile-app",
|
|
6
|
+
"slug": "mobile-app",
|
|
7
|
+
"scheme": "mobileapp",
|
|
8
|
+
"version": "0.1.0",
|
|
9
|
+
"orientation": "portrait",
|
|
10
|
+
"userInterfaceStyle": "automatic",
|
|
11
|
+
"newArchEnabled": true,
|
|
12
|
+
"ios": {
|
|
13
|
+
"supportsTablet": true,
|
|
14
|
+
"bundleIdentifier": "com.example.mobileapp"
|
|
15
|
+
},
|
|
16
|
+
"android": {
|
|
17
|
+
"package": "com.example.mobileapp"
|
|
18
|
+
},
|
|
19
|
+
"web": {
|
|
20
|
+
"bundler": "metro",
|
|
21
|
+
"output": "static"
|
|
22
|
+
},
|
|
23
|
+
"plugins": [
|
|
24
|
+
"expo-router",
|
|
25
|
+
"expo-secure-store"
|
|
26
|
+
],
|
|
27
|
+
"experiments": {
|
|
28
|
+
"typedRoutes": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate. Distribution is YOUR EAS pipeline, not `minutework deploy`.",
|
|
3
|
+
"cli": {
|
|
4
|
+
"version": ">= 12.0.0",
|
|
5
|
+
"appVersionSource": "remote"
|
|
6
|
+
},
|
|
7
|
+
"build": {
|
|
8
|
+
"development": {
|
|
9
|
+
"developmentClient": true,
|
|
10
|
+
"distribution": "internal"
|
|
11
|
+
},
|
|
12
|
+
"preview": {
|
|
13
|
+
"distribution": "internal",
|
|
14
|
+
"channel": "preview"
|
|
15
|
+
},
|
|
16
|
+
"production": {
|
|
17
|
+
"channel": "production",
|
|
18
|
+
"autoIncrement": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"submit": {
|
|
22
|
+
"production": {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/// <reference types="expo/types" />
|
|
2
|
+
|
|
3
|
+
// DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
|
|
4
|
+
// NOTE: This file should not be edited and should be committed; Expo regenerates
|
|
5
|
+
// it. It gives `process.env.EXPO_PUBLIC_*` string typings for `src/mw/env.ts`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
|
|
2
|
+
// Default Expo Metro config. Customize bundling/resolver here if you need to.
|
|
3
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
4
|
+
|
|
5
|
+
const config = getDefaultConfig(__dirname);
|
|
6
|
+
|
|
7
|
+
module.exports = config;
|