@pplethai/cli 0.5.1 → 0.7.0
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/README.md +117 -16
- package/dist/bin.js +32 -48
- package/package.json +2 -4
- package/templates/golden-path/AGENTS.md +1 -0
- package/templates/golden-path/CLAUDE.md +44 -24
- package/templates/golden-path/gitignore +5 -0
- package/templates/golden-path/shared/api.ts +17 -0
- package/templates/golden-path/src/auth.ts +27 -0
- package/templates/golden-path/src/dev-personas.ts +67 -3
- package/templates/golden-path/src/env.ts +6 -0
- package/templates/golden-path/src/legal-identity.ts +161 -0
- package/templates/golden-path/test/legal-identity.test.ts +256 -0
- package/templates/golden-path/wrangler.jsonc +14 -1
- package/skills/pple-platform/SKILL.md +0 -114
- package/skills/pple-platform/evals/evals.json +0 -29
- package/skills/pple-platform/references/deployment.md +0 -129
- package/skills/pple-platform/references/design-system.md +0 -55
- package/skills/pple-platform/references/mini-app.md +0 -111
- package/skills/pple-platform/references/pple-id-auth.md +0 -99
- package/skills/pple-platform/references/project-scaffold.md +0 -75
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Mini-app integration & notifications
|
|
2
|
-
|
|
3
|
-
## Running inside PPLE Today
|
|
4
|
-
|
|
5
|
-
Builder Apps ship as **mini-apps**: web apps opened in PPLE Today's WebView.
|
|
6
|
-
`@pplethai/pple-today-miniapp-sdk` captures the access token — see
|
|
7
|
-
[pple-id-auth.md](pple-id-auth.md). Facts that shape your design:
|
|
8
|
-
|
|
9
|
-
- `sdk.init()` must complete before any other SDK call.
|
|
10
|
-
- Outside PPLE Today (plain browser), the same SDK runs the PPLE ID browser
|
|
11
|
-
login — that is expected, not an error. Locally, `DEV_AUTH` covers
|
|
12
|
-
development.
|
|
13
|
-
- The OAuth client and mini-app URL are provisioned when the app is created in
|
|
14
|
-
the Console; Builders do not register them manually with the Today team.
|
|
15
|
-
- Design mobile-first; test at ~375 px width.
|
|
16
|
-
|
|
17
|
-
## Push notifications (Broadcast and Direct)
|
|
18
|
-
|
|
19
|
-
Builder Apps send push notifications through `sendNotification` in
|
|
20
|
-
`src/notify.ts`. Every send **states its audience** — the two kinds follow
|
|
21
|
-
from that:
|
|
22
|
-
|
|
23
|
-
- **Broadcast** (การแจ้งเตือนทุกคน) — `{ kind: "all" }`, to everyone this app
|
|
24
|
-
can reach: its App Users ∩ its current publication-tier audience.
|
|
25
|
-
- **Direct** (การแจ้งเตือนรายบุคคล) — `{ kind: "direct", recipients }`, to
|
|
26
|
-
the people this app names, by PPLE ID `sub` or mobile number.
|
|
27
|
-
|
|
28
|
-
The audience is a **required discriminated union**. Omitting it is a compile
|
|
29
|
-
error, not a silent broadcast — that is the dangerous bug this shape exists
|
|
30
|
-
to prevent. Do not default a missing audience to `all`.
|
|
31
|
-
|
|
32
|
-
Naming **narrows** the reachable set and never widens it. A named person who
|
|
33
|
-
has never opened this app receives nothing and comes back `not_reachable`.
|
|
34
|
-
That status is **one collapsed reason** covering "no PPLE ID at all", "never
|
|
35
|
-
opened this app", "outside the tier", and "opted out". Do not decompose it,
|
|
36
|
-
surface it as a guess, or infer *why*. Use it to prune stale names — a send
|
|
37
|
-
debits the reach it requested (every named recipient, or the Broadcast
|
|
38
|
-
audience size), so leftover entries cost quota until removed.
|
|
39
|
-
|
|
40
|
-
`NOTIFY_API_BASE` and `NOTIFY_API_KEY` are provisioned bindings. The key *is*
|
|
41
|
-
the address — no app id on the wire. Recipients belong only on a Direct send,
|
|
42
|
-
on this same route.
|
|
43
|
-
|
|
44
|
-
### App-facing shape (`shared/api.ts`)
|
|
45
|
-
|
|
46
|
-
Call `sendNotification` with a `Notification` from `shared/api.ts`. Do not
|
|
47
|
-
invent a parallel payload or POST the wire shape yourself.
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
import { sendNotification } from "./notify";
|
|
51
|
-
|
|
52
|
-
await sendNotification(env, {
|
|
53
|
-
audience: {
|
|
54
|
-
kind: "direct",
|
|
55
|
-
recipients: [{ sub: "…" }, { phone: "0812345678" }],
|
|
56
|
-
},
|
|
57
|
-
// or: { kind: "all" },
|
|
58
|
-
title: "Reimbursement approved",
|
|
59
|
-
body: "Your claim was approved",
|
|
60
|
-
linkPath: "/claims/123", // optional path-only self-link
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
A recipient entry names **exactly one** of `sub` or `phone`. Cap is 200 per
|
|
65
|
-
call — over it is a refusal, never a silent truncation, so chunk a long list
|
|
66
|
-
deliberately. Optional `idempotencyKey` makes a retry after a timeout safe.
|
|
67
|
-
|
|
68
|
-
Optional `linkPath` is a deep link into **this** app only. The platform joins
|
|
69
|
-
it to the app's known entry and rejects absolute URLs, schemes, `..`, or
|
|
70
|
-
anything that would leave this app. Free-form destinations that could address
|
|
71
|
-
another mini app (e.g. `content.link`) are not available on this route.
|
|
72
|
-
Omit `linkPath` when the tap should open the notification centre.
|
|
73
|
-
|
|
74
|
-
A Direct send returns each named entry as `delivered` or `not_reachable`,
|
|
75
|
-
echoed as the caller named it. A Broadcast names no one, so there is nothing
|
|
76
|
-
to enumerate.
|
|
77
|
-
|
|
78
|
-
### Wire shape (`src/notify.ts`)
|
|
79
|
-
|
|
80
|
-
Keep the platform vocabulary translation in `notify.ts` only. Your routes
|
|
81
|
-
call `sendNotification`; they do not POST this themselves:
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
body: JSON.stringify({
|
|
85
|
-
audience: input.audience,
|
|
86
|
-
content: { header: input.title, message: input.body },
|
|
87
|
-
...(input.linkPath !== undefined ? { linkPath: input.linkPath } : {}),
|
|
88
|
-
...(input.idempotencyKey !== undefined
|
|
89
|
-
? { idempotencyKey: input.idempotencyKey }
|
|
90
|
-
: {}),
|
|
91
|
-
})
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
- Missing config → fail loud (`notifications_not_configured` / 503), not silent.
|
|
95
|
-
- HTTP 429 → `quota_exceeded` (**nothing was delivered**). The helper also
|
|
96
|
-
maps the other refusals (`invalid_request`, `invalid_recipients`,
|
|
97
|
-
`idempotency_key_conflict`, `not_permitted`).
|
|
98
|
-
- Prefer fire-and-forget from route handlers with error logging — do not fail
|
|
99
|
-
the user's primary action solely because notify is slow.
|
|
100
|
-
|
|
101
|
-
### What not to do
|
|
102
|
-
|
|
103
|
-
Do **not** call the legacy central-team endpoint that accepts a
|
|
104
|
-
`PHONE_NUMBER` audience (`/external/notifications/send` with raw numbers).
|
|
105
|
-
Naming a phone on `sendNotification` is the Direct contract; that unbound
|
|
106
|
-
path is not. If you see older docs or `mini-app-example` code addressing
|
|
107
|
-
phones on `/external/notifications/send`, ignore them for new apps.
|
|
108
|
-
Do **not** invent free-form link / absolute URL fields — self-links use
|
|
109
|
-
`linkPath` only.
|
|
110
|
-
Do **not** treat `not_reachable` as "not a party member" versus "not a user
|
|
111
|
-
of this app". That distinction is the one fact this status exists to hide.
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# PPLE ID authentication (Builder Apps)
|
|
2
|
-
|
|
3
|
-
PPLE ID is the party's OIDC identity provider. For a **mini-app**, the flow is:
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
PPLE Today ──opens mini-app in WebView──▶ web (React)
|
|
7
|
-
web: PPLEMiniApp.init() → getAccessToken() ── bearer token
|
|
8
|
-
web ──Authorization: Bearer <token>──▶ Worker
|
|
9
|
-
Worker ──GET {SSO_BASE_URL}/oidc/v1/userinfo──▶ PPLE ID
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
In a plain browser the same SDK runs the PPLE ID authorization-code login
|
|
13
|
-
(redirect / callback) instead of reading tokens from the launch URL; the
|
|
14
|
-
Worker still validates the bearer token the same way.
|
|
15
|
-
|
|
16
|
-
The Worker never sees a password and holds no session; the bearer token is
|
|
17
|
-
the credential on every request (`src/auth.ts`).
|
|
18
|
-
|
|
19
|
-
Tokens also exist outside mini-apps for **Builders** (Console browser login,
|
|
20
|
-
`pple auth login` device flow). End users of a Builder App authenticate
|
|
21
|
-
through the mini-app SDK — either inside PPLE Today or via that SDK's
|
|
22
|
-
browser PPLE ID flow. Do not invent a second custom login page.
|
|
23
|
-
|
|
24
|
-
## Three ways the mini-app can start
|
|
25
|
-
|
|
26
|
-
Canonical wiring: `web/pple.ts` + `web/main.tsx` in the golden-path template.
|
|
27
|
-
|
|
28
|
-
1. **Local `vite dev`** — set `token` to the `DEV_AUTH` value from `.dev.vars`
|
|
29
|
-
and treat the session as signed-in. No PPLE Today required.
|
|
30
|
-
2. **Inside PPLE Today** — init `@pplethai/pple-today-miniapp-sdk`, capture
|
|
31
|
-
the access token from the launch URL, attach it on every `api()` call.
|
|
32
|
-
3. **Plain browser (production)** — the same SDK completes the OIDC sign-in
|
|
33
|
-
callback or redirects to PPLE ID; on return it stores the access token
|
|
34
|
-
and the app continues as signed-in.
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
import { PPLEMiniApp } from "@pplethai/pple-today-miniapp-sdk";
|
|
38
|
-
|
|
39
|
-
// Local: import.meta.env.DEV → DEV_AUTH token ("dev-token" by default)
|
|
40
|
-
// Production (Today or browser): sdk.init() → getAccessToken() → { kind: "signed-in" }
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
OAuth client values are provisioned per app — Builders do not register
|
|
44
|
-
clients by hand. The SPA reads them at **build time** via Vite
|
|
45
|
-
(`VITE_OAUTH_URL`, `VITE_OAUTH_CLIENT_ID`), so the platform bakes them into
|
|
46
|
-
the `env:` block of the deploy workflow it owns in each app repo
|
|
47
|
-
(`.github/workflows/deploy.yml`), where `npm run build` can see them. Because
|
|
48
|
-
that workflow is reconciled on every deploy (ADR-0004), an app self-heals
|
|
49
|
-
these values on its next deploy. `VITE_OAUTH_REDIRECT_URI` is not baked in:
|
|
50
|
-
it defaults to the app's own origin (`window.location.origin`), which matches
|
|
51
|
-
the mini-app URL the platform registers as an allowed redirect on that OAuth
|
|
52
|
-
client (`docs/integration/today-sso-changes.md` §5).
|
|
53
|
-
|
|
54
|
-
A build that runs without `VITE_OAUTH_URL` set produces a bundle with an
|
|
55
|
-
empty `oauthUrl`; the template's `bootstrap()` catches that and shows an
|
|
56
|
-
"app is misconfigured" error instead of the raw `oidc-client-ts` failure.
|
|
57
|
-
|
|
58
|
-
## Worker: validating the token
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
// src/auth.ts — pattern from the template
|
|
62
|
-
if (env.DEV_AUTH && token === env.DEV_AUTH) {
|
|
63
|
-
return { ok: true, identity: { sub: "dev-user", name: "Local Dev" } };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const res = await fetch(`${env.SSO_BASE_URL}/oidc/v1/userinfo`, {
|
|
67
|
-
headers: { authorization: `Bearer ${token}` },
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
- Missing / invalid token → 401.
|
|
72
|
-
- PPLE ID unreachable / malformed profile → 502 (so the client can tell
|
|
73
|
-
"sign in again" from "try again later").
|
|
74
|
-
- Attach `{ sub, name }` to the request context; keep route handlers thin.
|
|
75
|
-
|
|
76
|
-
## Local DEV_AUTH only
|
|
77
|
-
|
|
78
|
-
```env
|
|
79
|
-
# .dev.vars (gitignored) — copy from .dev.vars.example
|
|
80
|
-
DEV_AUTH="dev-token"
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Rules:
|
|
84
|
-
|
|
85
|
-
- **Never** set `DEV_AUTH` in production. The platform never provisions it;
|
|
86
|
-
absence means real auth only.
|
|
87
|
-
- The React app's local token must match `DEV_AUTH` exactly (template default:
|
|
88
|
-
`dev-token`).
|
|
89
|
-
- Do not resurrect header-based impersonation (`x-dev-phone`,
|
|
90
|
-
`ALLOW_DEV_AUTH`) — that was the Express-era bypass.
|
|
91
|
-
|
|
92
|
-
## What not to tell the Builder
|
|
93
|
-
|
|
94
|
-
- Do not claim "tokens only exist inside PPLE Today" as a platform-wide fact
|
|
95
|
-
(Console and CLI also hold Builder tokens; end users can also sign in via
|
|
96
|
-
the SDK's browser PPLE ID flow).
|
|
97
|
-
- Do not add a custom OAuth redirect login page, or a NotInApp / "open in
|
|
98
|
-
PPLE Today only" guard — the SDK already handles the browser case.
|
|
99
|
-
- Do not ask for GitHub or Cloudflare credentials to "get a token."
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# Project scaffold & local development
|
|
2
|
-
|
|
3
|
-
New Builder Apps start from the golden-path template in
|
|
4
|
-
`pple-platform/template`, copied by `pple init` (create-and-scaffold, or
|
|
5
|
-
`pple init <slug>` after Console create). Do not scaffold Express/Prisma
|
|
6
|
-
monorepos or copy `pple-erp` — that layout is the old path.
|
|
7
|
-
|
|
8
|
-
## Layout (single package)
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
my-app/ # folder name matches the App Slug
|
|
12
|
-
├── package.json # scripts: dev, build, test, deploy, db:migrate:*
|
|
13
|
-
├── wrangler.jsonc # Worker + D1 + R2 bindings (platform-owned names)
|
|
14
|
-
├── .dev.vars.example # copy to .dev.vars for local DEV_AUTH
|
|
15
|
-
├── .pple/app.json # App Link — written by pple init; do not invent
|
|
16
|
-
├── src/ # Worker (API) — Hono routes under /api/*
|
|
17
|
-
│ ├── index.ts # thin route handlers
|
|
18
|
-
│ ├── auth.ts # PPLE ID userinfo + DEV_AUTH bypass
|
|
19
|
-
│ ├── db.ts # D1 helpers
|
|
20
|
-
│ ├── files.ts # R2 helpers
|
|
21
|
-
│ ├── notify.ts # Broadcast and Direct notifications
|
|
22
|
-
│ └── env.ts # Env bindings type
|
|
23
|
-
├── web/ # React SPA (Vite)
|
|
24
|
-
│ ├── main.tsx # bootstrap → App | error
|
|
25
|
-
│ ├── pple.ts # SDK / DEV_AUTH / api() helpers
|
|
26
|
-
│ ├── App.tsx
|
|
27
|
-
│ └── app.css # mobile-first layout only
|
|
28
|
-
├── shared/ # Zod schemas + types used by both sides
|
|
29
|
-
│ └── api.ts
|
|
30
|
-
├── migrations/ # D1 SQL migrations (0001_*.sql, …)
|
|
31
|
-
└── test/ # smoke tests in the Workers runtime
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**`shared/api.ts` is the contract** between Worker and SPA. Change a shape
|
|
35
|
-
there once; both sides import the Zod schema / inferred type.
|
|
36
|
-
|
|
37
|
-
## Conventions
|
|
38
|
-
|
|
39
|
-
- **One origin.** The Worker serves `/api/*` and the SPA for everything else.
|
|
40
|
-
Frontend calls use relative paths (`/api/notes`) via `api()` / `apiJson()`
|
|
41
|
-
in `web/pple.ts`.
|
|
42
|
-
- **Keep handlers thin.** Put logic in `auth` / `db` / `files` / `notify`.
|
|
43
|
-
- **Validate with Zod** from `shared/api.ts`.
|
|
44
|
-
- **Bind D1 parameters** (`?` + `.bind(...)`) — never string-concatenate SQL.
|
|
45
|
-
- **Add a table** with a new `migrations/000N_*.sql` (never edit an applied
|
|
46
|
-
migration), then `npm run db:migrate:local`.
|
|
47
|
-
- **Mobile-first.** The app runs in a phone-sized WebView.
|
|
48
|
-
- **User-facing strings are Thai** where the template already is; code and
|
|
49
|
-
comments stay English.
|
|
50
|
-
|
|
51
|
-
## Local development
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
# After pple auth login + create/scaffold (pple init / --name, or Console
|
|
55
|
-
# then pple init <slug>):
|
|
56
|
-
cd <slug>
|
|
57
|
-
npm install
|
|
58
|
-
cp .dev.vars.example .dev.vars # enables DEV_AUTH for plain-browser work
|
|
59
|
-
npm run db:migrate:local
|
|
60
|
-
npm run dev # http://localhost:5173 — SPA + Worker
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Everyday commands:
|
|
64
|
-
|
|
65
|
-
| Command | Does |
|
|
66
|
-
| ------- | ---- |
|
|
67
|
-
| `npm run dev` | Full-stack Vite + Worker with hot reload |
|
|
68
|
-
| `npm run typecheck` | Type-check app and Worker |
|
|
69
|
-
| `npm test` | Smoke tests in the Workers runtime |
|
|
70
|
-
| `npm run build` | Deployable bundle |
|
|
71
|
-
| `npm run db:migrate:local` | Apply D1 migrations locally |
|
|
72
|
-
| `pple deploy` | Ship via the platform CLI |
|
|
73
|
-
|
|
74
|
-
In local `vite dev`, `web/pple.ts` uses the `DEV_AUTH` token automatically so
|
|
75
|
-
you never need PPLE Today open. See [pple-id-auth.md](pple-id-auth.md).
|