@pplethai/cli 0.7.0 → 0.8.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/dist/bin.js +171 -22
- package/package.json +3 -2
- package/templates/golden-path/.dev.vars.example +19 -1
- package/templates/golden-path/CLAUDE.md +51 -13
- package/templates/golden-path/README.md +27 -2
- package/templates/golden-path/gitignore +3 -0
- package/templates/golden-path/overlay-mtime.mjs +30 -0
- package/templates/golden-path/package-lock.json +19 -1
- package/templates/golden-path/package.json +3 -2
- package/templates/golden-path/scripts/assert-dev-compiled-out.mjs +42 -0
- package/templates/golden-path/shared/api.ts +25 -2
- package/templates/golden-path/src/auth.ts +72 -7
- package/templates/golden-path/src/call-service.ts +140 -0
- package/templates/golden-path/src/dataset-query.ts +470 -0
- package/templates/golden-path/src/dataset-schema.ts +398 -0
- package/templates/golden-path/src/dev-call-service.ts +391 -0
- package/templates/golden-path/src/dev-personas.ts +49 -15
- package/templates/golden-path/src/dev-read-dataset.ts +429 -0
- package/templates/golden-path/src/dev-registry.ts +276 -0
- package/templates/golden-path/src/env.ts +13 -2
- package/templates/golden-path/src/identity-header.ts +17 -0
- package/templates/golden-path/src/index.ts +32 -1
- package/templates/golden-path/src/legal-identity.ts +47 -28
- package/templates/golden-path/src/levels.ts +132 -0
- package/templates/golden-path/src/read-dataset.ts +376 -0
- package/templates/golden-path/src/registry.ts +122 -0
- package/templates/golden-path/src/vite-env.d.ts +13 -0
- package/templates/golden-path/test/call-service.test.ts +178 -0
- package/templates/golden-path/test/dev-call-service.test.ts +279 -0
- package/templates/golden-path/test/dev-read-dataset.test.ts +270 -0
- package/templates/golden-path/test/dev-registry.test.ts +375 -0
- package/templates/golden-path/test/effective-sub.test.ts +101 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/entry.md +85 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/audience.json +3 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/named.json +3 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/offices.json +6 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/roster.json +3 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/summary.json +1 -0
- package/templates/golden-path/test/fixtures/overlay/platform/legal-identity/entry.md +8 -0
- package/templates/golden-path/test/fixtures/overlay/platform/legal-identity/mocks/responses.json +13 -0
- package/templates/golden-path/test/fixtures/overlay/shop/catalog/entry.md +8 -0
- package/templates/golden-path/test/fixtures/overlay/shop/catalog/mocks/responses.json +7 -0
- package/templates/golden-path/test/fixtures/overlay/shop/orders/entry.md +12 -0
- package/templates/golden-path/test/fixtures/overlay/shop/orders/mocks/responses.json +36 -0
- package/templates/golden-path/test/harness.ts +101 -16
- package/templates/golden-path/test/legal-identity.test.ts +91 -24
- package/templates/golden-path/test/membership.test.ts +105 -0
- package/templates/golden-path/test/read-dataset.test.ts +214 -0
- package/templates/golden-path/test/service-call.test.ts +166 -0
- package/templates/golden-path/vite.config.ts +2 -1
- package/templates/golden-path/vitest.config.ts +2 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// serviceCall (ADR-0024). The Identity header is base64url of the UTF-8 JSON,
|
|
2
|
+
// so a Thai name round-trips. encodeIdentityHeader is the one encoder the
|
|
3
|
+
// dev wrapper and callRegistry share; serviceCall undoes it.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import type { Identity } from "../shared/api";
|
|
7
|
+
import { encodeIdentityHeader } from "../src/identity-header";
|
|
8
|
+
import worker from "../src/index";
|
|
9
|
+
import { serviceCall } from "../src/registry";
|
|
10
|
+
|
|
11
|
+
const IDENTITY: Identity = {
|
|
12
|
+
actor: {
|
|
13
|
+
sub: "u-delegate",
|
|
14
|
+
name: "ชื่อนกทดสอบ",
|
|
15
|
+
mobile: "0810000014",
|
|
16
|
+
kyc: {
|
|
17
|
+
status: "verified",
|
|
18
|
+
verified: true,
|
|
19
|
+
verifiedAt: "2024-04-01T00:00:00.000Z",
|
|
20
|
+
source: "membership",
|
|
21
|
+
},
|
|
22
|
+
membership: { memberSince: "2020-05-01" },
|
|
23
|
+
},
|
|
24
|
+
effective: {
|
|
25
|
+
isDelegated: true,
|
|
26
|
+
sub: "347513243977122153",
|
|
27
|
+
activeRole: "province",
|
|
28
|
+
eligibleRoles: ["province"],
|
|
29
|
+
provinces: ["เชียงใหม่"],
|
|
30
|
+
extraRoles: ["exco", "province_co"],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("serviceCall", () => {
|
|
35
|
+
it("round-trips the shared encoder, including a Thai name", () => {
|
|
36
|
+
const encoded = encodeIdentityHeader(IDENTITY);
|
|
37
|
+
expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
38
|
+
expect(encoded).not.toContain("ชื่อ");
|
|
39
|
+
|
|
40
|
+
const call = serviceCall(
|
|
41
|
+
new Request("https://provider.invalid/offices", {
|
|
42
|
+
headers: {
|
|
43
|
+
"x-pple-consumer": "brave-purple-lion",
|
|
44
|
+
"x-pple-entry": "calm-amber-fox/branch-list",
|
|
45
|
+
"x-pple-identity": encoded,
|
|
46
|
+
"x-pple-user-token": "raw-token-must-not-surface",
|
|
47
|
+
authorization: "Bearer raw-token-must-not-surface",
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(call).toEqual({
|
|
53
|
+
consumer: "brave-purple-lion",
|
|
54
|
+
entry: "calm-amber-fox/branch-list",
|
|
55
|
+
identity: IDENTITY,
|
|
56
|
+
});
|
|
57
|
+
expect(JSON.stringify(call)).not.toContain("raw-token-must-not-surface");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("round-trips a null effective sub", () => {
|
|
61
|
+
const identity: Identity = {
|
|
62
|
+
...IDENTITY,
|
|
63
|
+
effective: { ...IDENTITY.effective, sub: null },
|
|
64
|
+
};
|
|
65
|
+
const call = serviceCall(
|
|
66
|
+
new Request("https://provider.invalid/", {
|
|
67
|
+
headers: {
|
|
68
|
+
"x-pple-consumer": "brave-purple-lion",
|
|
69
|
+
"x-pple-entry": "calm-amber-fox/branch-list",
|
|
70
|
+
"x-pple-identity": encodeIdentityHeader(identity),
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
expect(call?.identity).toEqual(identity);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("treats an Identity header with no effective sub as no Identity", () => {
|
|
78
|
+
const effective = {
|
|
79
|
+
isDelegated: IDENTITY.effective.isDelegated,
|
|
80
|
+
activeRole: IDENTITY.effective.activeRole,
|
|
81
|
+
eligibleRoles: IDENTITY.effective.eligibleRoles,
|
|
82
|
+
provinces: IDENTITY.effective.provinces,
|
|
83
|
+
extraRoles: IDENTITY.effective.extraRoles,
|
|
84
|
+
};
|
|
85
|
+
const call = serviceCall(
|
|
86
|
+
new Request("https://provider.invalid/", {
|
|
87
|
+
headers: {
|
|
88
|
+
"x-pple-consumer": "brave-purple-lion",
|
|
89
|
+
"x-pple-entry": "calm-amber-fox/branch-list",
|
|
90
|
+
"x-pple-identity": encodeIdentityHeader({ actor: IDENTITY.actor, effective } as Identity),
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
expect(call?.identity).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("treats an Identity header with no membership as no Identity", () => {
|
|
98
|
+
const stale = {
|
|
99
|
+
actor: {
|
|
100
|
+
sub: IDENTITY.actor.sub,
|
|
101
|
+
name: IDENTITY.actor.name,
|
|
102
|
+
mobile: IDENTITY.actor.mobile,
|
|
103
|
+
kyc: IDENTITY.actor.kyc,
|
|
104
|
+
},
|
|
105
|
+
effective: IDENTITY.effective,
|
|
106
|
+
};
|
|
107
|
+
const call = serviceCall(
|
|
108
|
+
new Request("https://provider.invalid/", {
|
|
109
|
+
headers: {
|
|
110
|
+
"x-pple-consumer": "brave-purple-lion",
|
|
111
|
+
"x-pple-entry": "calm-amber-fox/branch-list",
|
|
112
|
+
"x-pple-identity": encodeIdentityHeader(stale as Identity),
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
expect(call?.identity).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns the call with no Identity when the entry declares userToken none", () => {
|
|
120
|
+
expect(
|
|
121
|
+
serviceCall(
|
|
122
|
+
new Request("https://provider.invalid/", {
|
|
123
|
+
headers: {
|
|
124
|
+
"x-pple-consumer": "brave-purple-lion",
|
|
125
|
+
"x-pple-entry": "calm-amber-fox/branch-list",
|
|
126
|
+
"x-pple-user-token": "still-not-a-fact",
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
),
|
|
130
|
+
).toEqual({
|
|
131
|
+
consumer: "brave-purple-lion",
|
|
132
|
+
entry: "calm-amber-fox/branch-list",
|
|
133
|
+
identity: null,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns null when the request is not a platform registry call", () => {
|
|
138
|
+
expect(serviceCall(new Request("https://app.example/api/notes"))).toBeNull();
|
|
139
|
+
expect(
|
|
140
|
+
serviceCall(new Request("https://app.example/api/notes", { headers: { "x-pple-consumer": "only-one" } })),
|
|
141
|
+
).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("treats an identity header that is not the encoding as no Identity", () => {
|
|
145
|
+
expect(
|
|
146
|
+
serviceCall(
|
|
147
|
+
new Request("https://provider.invalid/", {
|
|
148
|
+
headers: {
|
|
149
|
+
"x-pple-consumer": "brave-purple-lion",
|
|
150
|
+
"x-pple-entry": "calm-amber-fox/branch-list",
|
|
151
|
+
"x-pple-identity": "not-json",
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
).toEqual({
|
|
156
|
+
consumer: "brave-purple-lion",
|
|
157
|
+
entry: "calm-amber-fox/branch-list",
|
|
158
|
+
identity: null,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("puts a registry slot on the Worker default export", () => {
|
|
163
|
+
expect(worker.registry).toEqual({});
|
|
164
|
+
expect(typeof worker.fetch).toBe("function");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -2,13 +2,14 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
3
3
|
import react from "@vitejs/plugin-react";
|
|
4
4
|
import { defineConfig } from "vite";
|
|
5
|
+
import { overlayMtimePlugin } from "./overlay-mtime.mjs";
|
|
5
6
|
|
|
6
7
|
// The Cloudflare plugin reads wrangler.jsonc, runs the Worker in the real
|
|
7
8
|
// Workers runtime during `vite dev`, and builds a deployable bundle with
|
|
8
9
|
// `vite build`. `vite dev` serves the React app and the /api/* Worker from a
|
|
9
10
|
// single origin, so no CORS or proxy config is needed.
|
|
10
11
|
export default defineConfig({
|
|
11
|
-
plugins: [react(), cloudflare()],
|
|
12
|
+
plugins: [overlayMtimePlugin(), react(), cloudflare()],
|
|
12
13
|
resolve: {
|
|
13
14
|
alias: [
|
|
14
15
|
// @pplethai/components ships a "development" export condition pointing at
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
readD1Migrations,
|
|
5
5
|
} from "@cloudflare/vitest-pool-workers";
|
|
6
6
|
import { defineConfig } from "vitest/config";
|
|
7
|
+
import { overlayMtimePlugin } from "./overlay-mtime.mjs";
|
|
7
8
|
|
|
8
9
|
// Runs the smoke tests inside the real Workers runtime (workerd) with local
|
|
9
10
|
// D1 and R2, so the tests exercise the same code paths as production. The D1
|
|
@@ -12,6 +13,7 @@ export default defineConfig(async () => {
|
|
|
12
13
|
const migrations = await readD1Migrations(path.join(__dirname, "migrations"));
|
|
13
14
|
return {
|
|
14
15
|
plugins: [
|
|
16
|
+
overlayMtimePlugin(),
|
|
15
17
|
cloudflareTest({
|
|
16
18
|
wrangler: { configPath: "./wrangler.jsonc" },
|
|
17
19
|
miniflare: {
|