minutework 0.1.19 → 0.1.21
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 +15 -4
- package/README.md +48 -6
- package/assets/claude-local/CLAUDE.md.template +56 -3
- package/assets/claude-local/bundle.json +4 -1
- package/assets/claude-local/skills/README.md +5 -1
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +57 -0
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +22 -0
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +16 -2
- package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +12 -0
- package/assets/claude-local/skills/runtime-capability-inventory/SKILL.md +17 -0
- package/assets/claude-local/skills/schema-engine/SKILL.md +18 -0
- 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/claude-local/skills/vuilder-discovery-output-contract/SKILL.md +136 -0
- package/assets/claude-local/skills/vuilder-public-site-authoring/SKILL.md +76 -0
- package/assets/templates/fastapi-sidecar/README.md +7 -1
- package/assets/templates/fastapi-sidecar/template.schema.json +4 -2
- 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 +15 -4
- package/assets/templates/next-tenant-app/template.schema.json +4 -2
- package/dist/cli-json.d.ts +28 -0
- package/dist/cli-json.js +20 -0
- package/dist/cli-json.js.map +1 -0
- package/dist/compile.js +71 -7
- package/dist/compile.js.map +1 -1
- package/dist/deploy.js +147 -14
- 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 +43 -14
- 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 +29 -0
- package/dist/publish.js +254 -0
- package/dist/publish.js.map +1 -0
- package/dist/runtime-package.d.ts +3 -1
- package/dist/runtime-package.js +13 -0
- package/dist/runtime-package.js.map +1 -1
- package/dist/workspace-assets.js +37 -5
- package/dist/workspace-assets.js.map +1 -1
- package/dist/workspace-bootstrap.d.ts +1 -0
- package/dist/workspace-bootstrap.js.map +1 -1
- package/dist/workspace.js +2 -0
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
- package/vendor/workspace-mcp/types.d.ts +10 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vuilder-discovery-output-contract
|
|
3
|
+
description: "Constrain Vuilder discovery to AI-native service operations and MinuteWork-buildable delivery systems instead of generic startup ideas."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vuilder Discovery Output Contract
|
|
7
|
+
|
|
8
|
+
Use this skill when Vuilder discovery is deciding what to propose from a user's
|
|
9
|
+
profile, resume, pasted context, or refinement request.
|
|
10
|
+
|
|
11
|
+
Discovery is pre-workspace. It must produce a buildable service direction that
|
|
12
|
+
can flow into the paid Planning Blueprint and later Builder handoff. It must not
|
|
13
|
+
pretend to create projects, runtimes, releases, packages, secrets, or live
|
|
14
|
+
published surfaces.
|
|
15
|
+
|
|
16
|
+
## Core Output
|
|
17
|
+
|
|
18
|
+
Discovery cards are build cards, not founder idea cards.
|
|
19
|
+
|
|
20
|
+
Each card must describe:
|
|
21
|
+
|
|
22
|
+
- an AI-native service outcome the operator can credibly deliver
|
|
23
|
+
- the buyer or internal team receiving that service outcome
|
|
24
|
+
- evidence from the user's profile that makes the operator credible
|
|
25
|
+
- the MinuteWork delivery system Builder can prepare
|
|
26
|
+
- the first workflow the system should support
|
|
27
|
+
- the runtime records, AI workers, Builder surfaces, and artifacts involved
|
|
28
|
+
- capability gap candidates for unsupported substrate
|
|
29
|
+
|
|
30
|
+
Discovery should ask:
|
|
31
|
+
|
|
32
|
+
> What service outcome can this operator credibly deliver, and what
|
|
33
|
+
> MinuteWork-supported system can Builder create to help deliver it?
|
|
34
|
+
|
|
35
|
+
Do not ask:
|
|
36
|
+
|
|
37
|
+
> What SaaS startup should this person found?
|
|
38
|
+
|
|
39
|
+
## Builder Constraints
|
|
40
|
+
|
|
41
|
+
- Compose shared MinuteWork substrate before bespoke code.
|
|
42
|
+
- Treat the app pack as the shipped product unit.
|
|
43
|
+
- Check shell fit first for member-facing collaboration and operator work.
|
|
44
|
+
- Use `tenant-app` only for public surfaces or explicit standalone UI
|
|
45
|
+
exceptions after shell fit has been checked.
|
|
46
|
+
- Use `sidecar` only for concrete backend/runtime responsibilities such as
|
|
47
|
+
webhooks, workers, schedulers, external sync, or Python-heavy compute.
|
|
48
|
+
- Reuse runtime AI for drafting/generation before proposing bespoke provider
|
|
49
|
+
calls in `tenant-app` or `sidecar`.
|
|
50
|
+
- Per-customer deliverables should come from runtime data/content/workflows, not
|
|
51
|
+
fresh Builder/codegen work for each customer.
|
|
52
|
+
- Unsupported substrate becomes a capability gap candidate, not a polished
|
|
53
|
+
promise.
|
|
54
|
+
|
|
55
|
+
## Banned Framing
|
|
56
|
+
|
|
57
|
+
Do not make these the primary discovery output:
|
|
58
|
+
|
|
59
|
+
- generic SaaS ideas
|
|
60
|
+
- founder coaching
|
|
61
|
+
- TAM-first or ARR-first startup cards
|
|
62
|
+
- broad knowledge-management products without a specific service workflow
|
|
63
|
+
- unsupported "Builder can build anything" claims
|
|
64
|
+
|
|
65
|
+
Service packaging, pricing, market notes, or revenue hypotheses may appear as
|
|
66
|
+
secondary context only after the service outcome and buildable system are clear.
|
|
67
|
+
|
|
68
|
+
## Discovery Tool Use
|
|
69
|
+
|
|
70
|
+
The discovery agent may use read-only evaluation and research tools to sharpen
|
|
71
|
+
cards before it saves proposal fields:
|
|
72
|
+
|
|
73
|
+
- `extract_workflows_from_context` for repeatable work, bottlenecks, buyers, and
|
|
74
|
+
AI workforce jobs already implied by the user's context.
|
|
75
|
+
- `search_industry_examples` for curated MinuteWork service-business patterns.
|
|
76
|
+
This is not open web search.
|
|
77
|
+
- `estimate_market_pricing` for bounded pilot and retainer ranges with explicit
|
|
78
|
+
assumptions and uncertainty.
|
|
79
|
+
- `score_service_card_fit` for use-it-first fit, sell-it-back potential, buyer
|
|
80
|
+
urgency, founder advantage, implementation difficulty, and jargon risk.
|
|
81
|
+
- `rewrite_card_for_plain_language` for advisory rewrites of user-facing fields.
|
|
82
|
+
- `validate_discovery_proposal` before saving or marking a proposal ready.
|
|
83
|
+
- `research_unknown_entity` only as a confidence-gated fallback for an obscure
|
|
84
|
+
company, product, acronym, vendor, or niche workflow already present in the
|
|
85
|
+
resume, pasted context, or user message.
|
|
86
|
+
|
|
87
|
+
Do not use web research for well-known entities or general concepts the model
|
|
88
|
+
already understands, such as ADP, Workday, Fidelity, COBRA, or 401k, unless the
|
|
89
|
+
user explicitly asks for current/recent information. Web results are cited
|
|
90
|
+
context evidence, not authority for market-size or revenue claims.
|
|
91
|
+
|
|
92
|
+
## Required Build Card Fields
|
|
93
|
+
|
|
94
|
+
Use the `DiscoveryBuildCardV1` shape:
|
|
95
|
+
|
|
96
|
+
- `id`
|
|
97
|
+
- `plain_title`
|
|
98
|
+
- `plain_summary`
|
|
99
|
+
- `work_you_already_do`
|
|
100
|
+
- `ai_workforce_summary`
|
|
101
|
+
- `use_it_first`
|
|
102
|
+
- `sell_it_back`
|
|
103
|
+
- `first_pilot`
|
|
104
|
+
- `market_size_estimate`
|
|
105
|
+
- `yearly_revenue_potential`
|
|
106
|
+
- `pilot_pricing`
|
|
107
|
+
- `why_you_can_win`
|
|
108
|
+
- `who_it_helps`
|
|
109
|
+
- `what_minutework_helps_with`
|
|
110
|
+
- `service_outcome`
|
|
111
|
+
- `buyer`
|
|
112
|
+
- `operator_fit_summary`
|
|
113
|
+
- `evidence_from_profile`
|
|
114
|
+
- `minute_work_build`
|
|
115
|
+
- `builder_surfaces`
|
|
116
|
+
- `shell_fit`
|
|
117
|
+
- `shell_surface`
|
|
118
|
+
- `standalone_ui_exception_reason`
|
|
119
|
+
- `first_workflow`
|
|
120
|
+
- `runtime_records`
|
|
121
|
+
- `ai_workforce`
|
|
122
|
+
- `expected_artifact_graph`
|
|
123
|
+
- `required_capabilities`
|
|
124
|
+
- `capability_gap_candidates`
|
|
125
|
+
- `paid_blueprint_will_create`
|
|
126
|
+
- optional `service_packaging`
|
|
127
|
+
|
|
128
|
+
Plain-language fields are user-facing. They must express MinuteWork's loop:
|
|
129
|
+
build the AI workforce from work the operator already does, use it first under
|
|
130
|
+
their own judgment, then sell it back to their industry. Do not expose Builder
|
|
131
|
+
jargon such as app pack, schemas, runtime records, right rail, runtime agent seed,
|
|
132
|
+
or capability gaps in those fields.
|
|
133
|
+
|
|
134
|
+
Capability gap candidates use the same layer vocabulary as
|
|
135
|
+
`MinuteWorkCapabilityGapReportV1`, but they are not the workspace gap report.
|
|
136
|
+
The actual gap report is created later inside the generated Builder workspace.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vuilder-public-site-authoring
|
|
3
|
+
description: "Vuilder-owned public websites, public-dj CMS manifests, vuilder-public-site template work, or customer onboarding from a Vuilder vertical site."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vuilder Public Site Authoring
|
|
7
|
+
|
|
8
|
+
Use this skill when the request touches a Vuilder-owned public website,
|
|
9
|
+
`vuilder-public-site`, public-dj CMS content, or customer signup through a
|
|
10
|
+
Vuilder vertical.
|
|
11
|
+
|
|
12
|
+
## System Boundary
|
|
13
|
+
|
|
14
|
+
- Vuilder public sites are authored in a Vuilder builder/operator workspace.
|
|
15
|
+
- Generated React, Next.js, route, component, and styling code belongs in the
|
|
16
|
+
workspace/template source tree, not in public-dj database rows.
|
|
17
|
+
- public-dj is the CMS and revision store for Vuilder-owned public content:
|
|
18
|
+
pages, blog posts, onboarding flow content, navigation, brand profile, media,
|
|
19
|
+
theme tokens, and approved package/install metadata.
|
|
20
|
+
- Platform owns `PublishedWebProperty`, `SiteRelease`, domains, hosted
|
|
21
|
+
deployment receipts, onboarding intents, billing disposition, and runtime
|
|
22
|
+
provisioning.
|
|
23
|
+
- Tenant/customer runtime content after customer provisioning remains
|
|
24
|
+
runtime-canonical through `mw.core.site` and app-pack data.
|
|
25
|
+
|
|
26
|
+
## Template Contract
|
|
27
|
+
|
|
28
|
+
- Use `runtime/builder/templates/vuilder-public-site` for Vuilder-owned landing,
|
|
29
|
+
blog, public onboarding, and other public marketing routes.
|
|
30
|
+
- The template reads public-dj through a server-only CMS adapter.
|
|
31
|
+
- Expected release env includes `MW_PUBLIC_DJ_BASE_URL`,
|
|
32
|
+
`MW_PUBLIC_CONTENT_REF`, `MW_PUBLIC_CONTENT_DIGEST`, and
|
|
33
|
+
`MW_PUBLIC_DJ_READ_TOKEN`.
|
|
34
|
+
- Resolve CMS data by immutable `content_ref + digest`; do not render live
|
|
35
|
+
public pages from mutable drafts.
|
|
36
|
+
- The template may be released as `static_export` or `ssr_container` behind the
|
|
37
|
+
hosted deployment provider abstraction.
|
|
38
|
+
|
|
39
|
+
## Onboarding Intent Rules
|
|
40
|
+
|
|
41
|
+
- Vuilder authoring signup uses `vuilder_authoring`: create workspace/profile,
|
|
42
|
+
no runtime VM.
|
|
43
|
+
- Vuilder upgrade uses `vuilder_operator_runtime`: provision the Vuilder's own
|
|
44
|
+
builder/operator VM and materialize the public-site template.
|
|
45
|
+
- Customer signup through the Vuilder site uses `customer_vertical_signup`:
|
|
46
|
+
provision the customer's tenant runtime and install the approved vertical
|
|
47
|
+
package.
|
|
48
|
+
- The public-site BFF creates customer onboarding intents from server-pinned
|
|
49
|
+
release context: property/release refs, manifest ref/digest, package
|
|
50
|
+
ref/digest, onboarding flow ref/digest, and Vuilder workspace context.
|
|
51
|
+
- Browser intake answers are untrusted input. Browsers must not author package
|
|
52
|
+
refs, manifest refs, provisioning kind, or workspace identity.
|
|
53
|
+
|
|
54
|
+
## Release Rules
|
|
55
|
+
|
|
56
|
+
- Platform `SiteRelease.content_source` should be `shared_public_content` for a
|
|
57
|
+
Vuilder public site.
|
|
58
|
+
- `shared_public_content_ref` and `shared_public_content_digest` point to the
|
|
59
|
+
public-dj `PublicSiteManifestRevision`.
|
|
60
|
+
- Live activation remains a platform/manual approval action using approved
|
|
61
|
+
public content and package refs.
|
|
62
|
+
- Public-dj may store markdown/content/media/theme revisions; it must not store
|
|
63
|
+
or execute generated React component code as database content.
|
|
64
|
+
|
|
65
|
+
## Hard Rules
|
|
66
|
+
|
|
67
|
+
- Do not use tenant `/w/*`, `/app`, or the universal tenant shell in
|
|
68
|
+
`vuilder-public-site`.
|
|
69
|
+
- Do not use runtime `mw.core.site` as the CMS for Vuilder-owned public
|
|
70
|
+
websites.
|
|
71
|
+
- Do not infer provisioning behavior from browser labels, hostnames, or query
|
|
72
|
+
params; use platform-owned onboarding intents.
|
|
73
|
+
- Do not expose platform credentials, public-dj tokens, package refs, or
|
|
74
|
+
OpenRouter/provider keys to browser bundles.
|
|
75
|
+
- Do not let public-dj execute tenant runtime code or import runtime Django
|
|
76
|
+
apps for public-site rendering.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# FastAPI Sidecar Template
|
|
2
2
|
|
|
3
|
-
This directory is the canonical internal FastAPI sidecar scaffold for Builder.
|
|
3
|
+
This directory is the canonical internal FastAPI sidecar scaffold for Builder.
|
|
4
|
+
Builder will later materialize this bundle into
|
|
5
|
+
`BuilderWorkspace.sandbox_root/sidecar/` and edit only that workspace copy.
|
|
6
|
+
|
|
7
|
+
On managed tenant VMs, `BuilderWorkspace.sandbox_root` normally lives under
|
|
8
|
+
`/srv/minutework/workspaces/<workspace_id>/`, so the Builder-edited sidecar copy
|
|
9
|
+
is typically `/srv/minutework/workspaces/<workspace_id>/sidecar/`.
|
|
4
10
|
|
|
5
11
|
`seed_source` intentionally points back to this directory because the canonical bundle itself is the v1 source of truth. There is no separate `apps/*` seed app for this template.
|
|
6
12
|
|
|
@@ -25,14 +25,16 @@
|
|
|
25
25
|
"enum": [
|
|
26
26
|
"sidecar_nextjs_private",
|
|
27
27
|
"sidecar_fastapi_internal",
|
|
28
|
-
"combined_web"
|
|
28
|
+
"combined_web",
|
|
29
|
+
"public_site"
|
|
29
30
|
]
|
|
30
31
|
},
|
|
31
32
|
"template_profile": {
|
|
32
33
|
"type": "string",
|
|
33
34
|
"enum": [
|
|
34
35
|
"platform_session_bff",
|
|
35
|
-
"bridge_internal"
|
|
36
|
+
"bridge_internal",
|
|
37
|
+
"public_dj_cms"
|
|
36
38
|
]
|
|
37
39
|
},
|
|
38
40
|
"template_version": {
|
|
@@ -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
|
+
}
|