minutework 0.1.37 → 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.
@@ -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
 
@@ -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 +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.
@@ -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
  }
@@ -26,6 +26,13 @@ The local `/login` route is product UI that calls SDK hooks. The SDK also
26
26
  provides hosted UI helpers for same-origin `/_mw/login`, `/_mw/signup`, and
27
27
  `/_mw/verify-email` URLs; both paths stay on the same SDK contract.
28
28
 
29
+ For Supabase-like naming, the template uses `signUp` and
30
+ `signInWithPassword` from `@minutework/web-auth/react`. These aliases still use
31
+ the same cookie-backed web customer session and do not return access or refresh
32
+ tokens. App guards should read `useMinuteWorkSession().status`, whose stable
33
+ precedence is `loading` -> `verification_required` -> `authenticated` ->
34
+ `unauthenticated`; keep `error` as a separate field.
35
+
29
36
  Client-side `/app` session checks are only UX gating. Authorization is enforced
30
37
  by the platform `/_mw` routes and runtime dispatch: active customer membership,
31
38
  email verification, app publication, and manifest `web_customer_exposed`
@@ -27,7 +27,7 @@
27
27
  "design-system:visual": "playwright test -c tools/design-system/playwright.config.mjs"
28
28
  },
29
29
  "dependencies": {
30
- "@minutework/web-auth": "^0.1.0",
30
+ "@minutework/web-auth": "^0.1.1",
31
31
  "@radix-ui/react-slot": "^1.2.4",
32
32
  "class-variance-authority": "^0.7.1",
33
33
  "clsx": "^2.1.1",
@@ -17,7 +17,7 @@ type AuthMode = "login" | "signup";
17
17
  export function LoginScreen({ appName }: { appName: string }) {
18
18
  const router = useRouter();
19
19
  const searchParams = useSearchParams();
20
- const { login, signup } = useMinuteWorkAuth();
20
+ const { signInWithPassword, signUp } = useMinuteWorkAuth();
21
21
  const [mode, setMode] = useState<AuthMode>("login");
22
22
  const [email, setEmail] = useState("");
23
23
  const [displayName, setDisplayName] = useState("");
@@ -34,7 +34,7 @@ export function LoginScreen({ appName }: { appName: string }) {
34
34
 
35
35
  try {
36
36
  if (mode === "signup") {
37
- await signup({
37
+ await signUp({
38
38
  email,
39
39
  password,
40
40
  displayName,
@@ -45,7 +45,7 @@ export function LoginScreen({ appName }: { appName: string }) {
45
45
  return;
46
46
  }
47
47
 
48
- await login({ email, password });
48
+ await signInWithPassword({ email, password });
49
49
  startTransition(() => {
50
50
  router.replace(appRoutes.appHome);
51
51
  router.refresh();
@@ -44,8 +44,7 @@ export function AuthenticatedAppLayoutShell({
44
44
  }) {
45
45
  const router = useRouter();
46
46
  const { logout } = useMinuteWorkAuth();
47
- const { session, loading, authenticated, emailVerificationRequired } =
48
- useMinuteWorkSession();
47
+ const { session, status } = useMinuteWorkSession();
49
48
 
50
49
  function redirectToLogin() {
51
50
  startTransition(() => {
@@ -59,7 +58,7 @@ export function AuthenticatedAppLayoutShell({
59
58
  redirectToLogin();
60
59
  }
61
60
 
62
- if (loading) {
61
+ if (status === "loading") {
63
62
  return (
64
63
  <main className="min-h-screen bg-background text-foreground">
65
64
  <div className="mx-auto flex min-h-screen max-w-5xl items-center px-6 py-10">
@@ -71,11 +70,7 @@ export function AuthenticatedAppLayoutShell({
71
70
  );
72
71
  }
73
72
 
74
- if (
75
- emailVerificationRequired ||
76
- session?.email_verification_required ||
77
- session?.customer_membership?.status === "pending_verification"
78
- ) {
73
+ if (status === "verification_required") {
79
74
  return (
80
75
  <main className="min-h-screen bg-background text-foreground">
81
76
  <div className="mx-auto flex min-h-screen max-w-5xl items-center px-6 py-10">
@@ -102,7 +97,7 @@ export function AuthenticatedAppLayoutShell({
102
97
  );
103
98
  }
104
99
 
105
- if (!authenticated || !session?.customer_membership) {
100
+ if (status !== "authenticated" || !session?.customer_membership) {
106
101
  return (
107
102
  <main className="min-h-screen bg-background text-foreground">
108
103
  <div className="mx-auto flex min-h-screen max-w-5xl items-center px-6 py-10">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minutework",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "MinuteWork CLI for workspace scaffolding, local preview workflows, and hosted preview deploys.",
5
5
  "type": "module",
6
6
  "bin": {