@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.
@@ -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).