@clovnet/plugin-cli 0.2.2

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/LICENSE +21 -0
  3. package/README.md +92 -0
  4. package/bin/cwe-plugin.mjs +14 -0
  5. package/dist/cli.js +10400 -0
  6. package/package.json +62 -0
  7. package/templates/blank/README.md +60 -0
  8. package/templates/blank/_gitignore +3 -0
  9. package/templates/blank/cwe-plugin.json +9 -0
  10. package/templates/blank/package.json +31 -0
  11. package/templates/blank/settings.dev.json +8 -0
  12. package/templates/blank/src/handlers/hooks.ts +29 -0
  13. package/templates/blank/src/handlers/jobs.ts +9 -0
  14. package/templates/blank/src/handlers/routes.ts +44 -0
  15. package/templates/blank/src/index.ts +68 -0
  16. package/templates/blank/src/settings.ts +23 -0
  17. package/templates/blank/test/hooks.test.ts +43 -0
  18. package/templates/blank/tsconfig.json +14 -0
  19. package/templates/cashback/README.md +61 -0
  20. package/templates/cashback/_gitignore +3 -0
  21. package/templates/cashback/cwe-plugin.json +9 -0
  22. package/templates/cashback/package.json +31 -0
  23. package/templates/cashback/settings.dev.json +9 -0
  24. package/templates/cashback/src/handlers/hooks.ts +19 -0
  25. package/templates/cashback/src/handlers/jobs.ts +23 -0
  26. package/templates/cashback/src/handlers/routes.ts +188 -0
  27. package/templates/cashback/src/index.ts +128 -0
  28. package/templates/cashback/src/settings.ts +29 -0
  29. package/templates/cashback/test/hooks.test.ts +83 -0
  30. package/templates/cashback/tsconfig.json +14 -0
  31. package/templates/provider-skeleton/README.md +63 -0
  32. package/templates/provider-skeleton/_gitignore +3 -0
  33. package/templates/provider-skeleton/cwe-plugin.json +9 -0
  34. package/templates/provider-skeleton/package.json +31 -0
  35. package/templates/provider-skeleton/settings.dev.json +10 -0
  36. package/templates/provider-skeleton/src/adapter.ts +164 -0
  37. package/templates/provider-skeleton/src/fake-backend.ts +56 -0
  38. package/templates/provider-skeleton/src/handlers/hooks.ts +30 -0
  39. package/templates/provider-skeleton/src/handlers/jobs.ts +16 -0
  40. package/templates/provider-skeleton/src/handlers/routes.ts +128 -0
  41. package/templates/provider-skeleton/src/index.ts +83 -0
  42. package/templates/provider-skeleton/src/settings.ts +37 -0
  43. package/templates/provider-skeleton/test/hooks.test.ts +83 -0
  44. package/templates/provider-skeleton/tsconfig.json +14 -0
@@ -0,0 +1,188 @@
1
+ import { z } from "zod";
2
+ import type { PluginContext, PluginRequest } from "@clovnet/plugin-sdk";
3
+
4
+ /**
5
+ * Player-facing cashback surface (mirrors the platform's reference cashback
6
+ * plugin): a dataset of per-player accrual periods, two query actions and an
7
+ * idempotent `claim` mutation that credits the BONUS bucket via the
8
+ * allowlisted `wallet.bonus_award` command — the only way a plugin may award
9
+ * money, with every wallet invariant (idempotency, immutable ledger, audit,
10
+ * outbox) applied by the platform.
11
+ */
12
+
13
+ // `wallet.bonus_award` — referenced by string: an external plugin depends
14
+ // only on the SDK + zod, never on platform packages.
15
+ export const AWARD_BONUS_COMMAND = "wallet.bonus_award";
16
+
17
+ // ── Dataset shape: one accrual period per player, per cycle ──────────────────
18
+ export const accrualPeriodSchema = z.object({
19
+ periodId: z.string(),
20
+ playerId: z.string(),
21
+ /** accruing → claimable → claimed. */
22
+ status: z.enum(["accruing", "claimable", "claimed"]),
23
+ /** Decimal major units (e.g. 5 = €5.00); the wallet normalizes to 4dp. */
24
+ amount: z.number(),
25
+ currency: z.string(),
26
+ accruedAt: z.string(),
27
+ claimedAt: z.string().optional(),
28
+ });
29
+ export type AccrualPeriod = z.infer<typeof accrualPeriodSchema>;
30
+
31
+ const periods = (ctx: PluginContext) => ctx.datasets.collection<AccrualPeriod>("accrual_periods");
32
+
33
+ async function playerPeriods(ctx: PluginContext, playerId: string): Promise<AccrualPeriod[]> {
34
+ const result = await periods(ctx).query({ where: { playerId }, limit: 200 });
35
+ return result.records.map((r) => r.value);
36
+ }
37
+
38
+ // ── Action I/O schemas (drive the catalog descriptors + generated client) ────
39
+ export const summaryOutputSchema = z.object({
40
+ currency: z.string(),
41
+ claimableAmount: z.number(),
42
+ claimedAmount: z.number(),
43
+ accruingAmount: z.number(),
44
+ periodCount: z.number(),
45
+ });
46
+
47
+ export const listPeriodsQuerySchema = z.object({
48
+ status: z.enum(["accruing", "claimable", "claimed"]).optional(),
49
+ });
50
+ export const listPeriodsOutputSchema = z.object({
51
+ periods: z.array(
52
+ z.object({
53
+ periodId: z.string(),
54
+ status: z.enum(["accruing", "claimable", "claimed"]),
55
+ amount: z.number(),
56
+ currency: z.string(),
57
+ accruedAt: z.string(),
58
+ claimedAt: z.string().optional(),
59
+ }),
60
+ ),
61
+ });
62
+
63
+ export const claimBodySchema = z.object({ periodId: z.string().min(1).max(191) }).strict();
64
+ export const claimOutputSchema = z.object({
65
+ claimed: z.literal(true),
66
+ periodId: z.string(),
67
+ amount: z.number(),
68
+ currency: z.string(),
69
+ });
70
+
71
+ export const accrueBodySchema = z
72
+ .object({
73
+ playerId: z.string().min(1).max(191),
74
+ amount: z.number().positive(),
75
+ currency: z.string().length(3).optional(),
76
+ })
77
+ .strict();
78
+
79
+ // ── Handlers ──────────────────────────────────────────────────────────────────
80
+ export async function getSummary(req: PluginRequest, ctx: PluginContext) {
81
+ const playerId = req.player?.id;
82
+ if (!playerId) return { status: 401, body: { error: { code: "UNAUTHORIZED" } } };
83
+ const { payoutCurrency } = ctx.settings.get<{ payoutCurrency?: string }>();
84
+ const all = await playerPeriods(ctx, playerId);
85
+ const sum = (status: AccrualPeriod["status"]): number =>
86
+ all.filter((p) => p.status === status).reduce((n, p) => n + p.amount, 0);
87
+ return {
88
+ body: {
89
+ currency: payoutCurrency ?? "EUR",
90
+ claimableAmount: sum("claimable"),
91
+ claimedAmount: sum("claimed"),
92
+ accruingAmount: sum("accruing"),
93
+ periodCount: all.length,
94
+ },
95
+ };
96
+ }
97
+
98
+ export async function listPeriods(req: PluginRequest, ctx: PluginContext) {
99
+ const playerId = req.player?.id;
100
+ if (!playerId) return { status: 401, body: { error: { code: "UNAUTHORIZED" } } };
101
+ const query = req.query as z.infer<typeof listPeriodsQuerySchema>;
102
+ let all = await playerPeriods(ctx, playerId);
103
+ if (query.status) all = all.filter((p) => p.status === query.status);
104
+ all.sort((a, b) => a.accruedAt.localeCompare(b.accruedAt));
105
+ return {
106
+ body: {
107
+ periods: all.map((p) => ({
108
+ periodId: p.periodId,
109
+ status: p.status,
110
+ amount: p.amount,
111
+ currency: p.currency,
112
+ accruedAt: p.accruedAt,
113
+ ...(p.claimedAt ? { claimedAt: p.claimedAt } : {}),
114
+ })),
115
+ },
116
+ };
117
+ }
118
+
119
+ export async function claim(req: PluginRequest, ctx: PluginContext) {
120
+ const playerId = req.player?.id;
121
+ if (!playerId) return { status: 401, body: { error: { code: "UNAUTHORIZED" } } };
122
+ const body = req.body as z.infer<typeof claimBodySchema>;
123
+ const period = await periods(ctx).get(body.periodId);
124
+ // Not-found and not-owned look identical — never leak another player's data.
125
+ if (!period || period.playerId !== playerId) {
126
+ return { status: 404, body: { error: { code: "CASHBACK_PERIOD_NOT_FOUND" } } };
127
+ }
128
+ if (period.status !== "claimable") {
129
+ return {
130
+ status: 409,
131
+ body: { error: { code: "CASHBACK_NOT_CLAIMABLE", details: { status: period.status } } },
132
+ };
133
+ }
134
+
135
+ // The wallet idempotency key is PERIOD-SCOPED and deterministic, so
136
+ // concurrent/replayed claims of one period credit exactly once;
137
+ // `externalTransactionId = periodId` is the cross-key backstop.
138
+ const award = await ctx.commands.execute<{ idempotent: boolean }>({
139
+ name: AWARD_BONUS_COMMAND,
140
+ input: {
141
+ playerId,
142
+ amount: period.amount,
143
+ currency: period.currency,
144
+ idempotencyKey: `__PLUGIN_KEY__:claim:${period.periodId}`,
145
+ source: "__PLUGIN_KEY__",
146
+ externalTransactionId: period.periodId,
147
+ },
148
+ });
149
+
150
+ await periods(ctx).put(period.periodId, {
151
+ ...period,
152
+ status: "claimed",
153
+ claimedAt: new Date().toISOString(),
154
+ });
155
+ if (!award.idempotent) {
156
+ await ctx.events.emit({
157
+ name: "plugin.__PLUGIN_KEY__.claimed",
158
+ playerId,
159
+ payload: { periodId: period.periodId, amount: period.amount, currency: period.currency },
160
+ });
161
+ }
162
+ return {
163
+ body: { claimed: true as const, periodId: period.periodId, amount: period.amount, currency: period.currency },
164
+ };
165
+ }
166
+
167
+ /** Backoffice/ops route: mint a claimable cashback period for a player. */
168
+ export async function accrue(req: PluginRequest, ctx: PluginContext) {
169
+ const body = req.body as z.infer<typeof accrueBodySchema>;
170
+ const { payoutCurrency } = ctx.settings.get<{ payoutCurrency?: string }>();
171
+ const now = new Date();
172
+ const periodId = `cb_${body.playerId}_${now.getTime().toString(36)}`;
173
+ const currency = body.currency ?? payoutCurrency ?? "EUR";
174
+ await periods(ctx).put(periodId, {
175
+ periodId,
176
+ playerId: body.playerId,
177
+ status: "claimable",
178
+ amount: body.amount,
179
+ currency,
180
+ accruedAt: now.toISOString(),
181
+ });
182
+ await ctx.events.emit({
183
+ name: "plugin.__PLUGIN_KEY__.accrued",
184
+ playerId: body.playerId,
185
+ payload: { periodId, amount: body.amount, currency },
186
+ });
187
+ return { status: 201, body: { periodId } };
188
+ }
@@ -0,0 +1,128 @@
1
+ import { definePlugin } from "@clovnet/plugin-sdk";
2
+ import { settings } from "./settings.js";
3
+ import { onConfigure, onInstall, onUninstall } from "./handlers/hooks.js";
4
+ import {
5
+ AWARD_BONUS_COMMAND,
6
+ accrualPeriodSchema,
7
+ accrue,
8
+ accrueBodySchema,
9
+ claim,
10
+ claimBodySchema,
11
+ claimOutputSchema,
12
+ getSummary,
13
+ listPeriods,
14
+ listPeriodsOutputSchema,
15
+ listPeriodsQuerySchema,
16
+ summaryOutputSchema,
17
+ } from "./handlers/routes.js";
18
+ import { nightlyAccrual } from "./handlers/jobs.js";
19
+
20
+ /**
21
+ * __PLUGIN_NAME__ — scaffolded from the `cashback` template (the platform's
22
+ * reference plugin): a plugin-private dataset, three player actions
23
+ * (`getSummary`/`listPeriods` queries + the idempotent `claim` mutation), an
24
+ * admin accrue route, a nightly cron and an account-slot frontend widget.
25
+ */
26
+ export default definePlugin({
27
+ manifest: {
28
+ key: "__PLUGIN_KEY__",
29
+ name: "__PLUGIN_NAME__",
30
+ author: "you",
31
+ version: "0.1.0",
32
+ kind: "integration",
33
+ runtimeCompat: ">=0.1.0 <0.2.0",
34
+ description: "Player cashback accrual periods, claimable to the bonus bucket.",
35
+ permissions: {
36
+ // The ONLY wallet command a plugin may hold for awards — wallet.credit
37
+ // is hard-denied to plugins by the platform.
38
+ commands: [AWARD_BONUS_COMMAND],
39
+ events: {
40
+ subscribe: [],
41
+ emit: ["plugin.__PLUGIN_KEY__.accrued", "plugin.__PLUGIN_KEY__.claimed"],
42
+ },
43
+ // Short event types allowed to fan out to player sockets.
44
+ frontendEvents: ["__PLUGIN_KEY__.accrued", "__PLUGIN_KEY__.claimed"],
45
+ },
46
+ settings,
47
+ datasets: {
48
+ accrual_periods: {
49
+ schema: accrualPeriodSchema,
50
+ keyField: "periodId",
51
+ indexes: ["playerId", "status"],
52
+ maxRecords: 200_000,
53
+ },
54
+ },
55
+ routes: [
56
+ {
57
+ method: "GET",
58
+ path: "/summary",
59
+ surface: "player",
60
+ handler: "getSummary",
61
+ output: summaryOutputSchema,
62
+ rateLimit: { windowSec: 60, max: 120 },
63
+ },
64
+ {
65
+ method: "GET",
66
+ path: "/periods",
67
+ surface: "player",
68
+ handler: "listPeriods",
69
+ input: { query: listPeriodsQuerySchema },
70
+ output: listPeriodsOutputSchema,
71
+ },
72
+ {
73
+ method: "POST",
74
+ path: "/claim",
75
+ surface: "player",
76
+ handler: "claim",
77
+ input: { body: claimBodySchema },
78
+ output: claimOutputSchema,
79
+ idempotent: true,
80
+ },
81
+ {
82
+ method: "POST",
83
+ path: "/accrue",
84
+ surface: "admin",
85
+ handler: "accrue",
86
+ input: { body: accrueBodySchema },
87
+ },
88
+ ],
89
+ actions: [
90
+ {
91
+ key: "getSummary",
92
+ route: "GET /summary",
93
+ title: "Cashback summary",
94
+ description: "Claimable, claimed and accruing cashback totals for the player.",
95
+ kind: "query",
96
+ },
97
+ { key: "listPeriods", route: "GET /periods", title: "List cashback periods", kind: "query" },
98
+ {
99
+ key: "claim",
100
+ route: "POST /claim",
101
+ title: "Claim cashback",
102
+ description: "Credit a claimable period's cashback to the bonus balance.",
103
+ kind: "mutation",
104
+ idempotent: true,
105
+ emits: ["__PLUGIN_KEY__.claimed"],
106
+ },
107
+ ],
108
+ frontend: {
109
+ widgets: [
110
+ {
111
+ key: "__PLUGIN_KEY__-card",
112
+ title: "__PLUGIN_NAME__",
113
+ slot: "account",
114
+ dataAction: "getSummary",
115
+ actions: ["claim"],
116
+ },
117
+ ],
118
+ },
119
+ jobs: {
120
+ "nightly-accrual": { schedule: "0 3 * * *", handler: "nightly-accrual", timeoutSec: 300 },
121
+ },
122
+ },
123
+ hooks: { onInstall, onConfigure, onUninstall },
124
+ handlers: {
125
+ routes: { getSummary, listPeriods, claim, accrue },
126
+ jobs: { "nightly-accrual": nightlyAccrual },
127
+ },
128
+ });
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import { settingsField, type PluginSettingsSchema } from "@clovnet/plugin-sdk";
3
+
4
+ /**
5
+ * Tenant settings for the cashback program. `cashbackPercent` drives the
6
+ * nightly accrual sweep; the secret is an example write-only field (stored
7
+ * encrypted, never returned, never logged).
8
+ */
9
+ export const settings: PluginSettingsSchema = {
10
+ fields: {
11
+ cashbackPercent: settingsField.number({
12
+ label: "Cashback percent",
13
+ description: "Percent of tracked losses accrued as claimable cashback.",
14
+ default: 5,
15
+ zod: z.number().min(0).max(25),
16
+ }),
17
+ payoutCurrency: settingsField.string({
18
+ label: "Payout currency",
19
+ description: "ISO currency the cashback is credited in.",
20
+ default: "EUR",
21
+ zod: z.string().length(3),
22
+ }),
23
+ reportingApiKey: settingsField.string({
24
+ label: "Reporting API key",
25
+ description: "Example write-only secret — read with ctx.secrets.get('reportingApiKey').",
26
+ secret: true,
27
+ }),
28
+ },
29
+ };
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestContext } from "@clovnet/plugin-sdk/testing";
3
+ import type { PluginRequest } from "@clovnet/plugin-sdk";
4
+ import plugin from "../src/index.js";
5
+ import { accrue, claim, getSummary } from "../src/handlers/routes.js";
6
+ import { nightlyAccrual } from "../src/handlers/jobs.js";
7
+
8
+ /**
9
+ * Unit tests on the in-memory test context: real permission enforcement,
10
+ * real dataset semantics, canned wallet results — no runtime, no docker,
11
+ * breakpoints work under `vitest --inspect`.
12
+ */
13
+ const asRequest = (partial: Partial<PluginRequest>): PluginRequest =>
14
+ ({ params: {}, query: {}, headers: {}, ...partial }) as PluginRequest;
15
+
16
+ function makeContext() {
17
+ return createTestContext(plugin, {
18
+ tenantId: "t-test",
19
+ settings: { cashbackPercent: 10, payoutCurrency: "EUR" },
20
+ secrets: { reportingApiKey: "test-key" },
21
+ commandResults: {
22
+ // Money commands NEVER execute in tests — canned result + recording.
23
+ "wallet.bonus_award": { idempotent: false, balances: { bonus: "5.0000" } },
24
+ },
25
+ });
26
+ }
27
+
28
+ describe("__PLUGIN_KEY__", () => {
29
+ it("accrues then claims exactly once through wallet.bonus_award", async () => {
30
+ const t = makeContext();
31
+ await t.runHook("onInstall");
32
+
33
+ const accrued = await accrue(
34
+ asRequest({ body: { playerId: "p1", amount: 5 }, actor: { type: "staff", id: "admin" } as never }),
35
+ t.ctx,
36
+ );
37
+ expect(accrued.status).toBe(201);
38
+ const periodId = (accrued.body as { periodId: string }).periodId;
39
+
40
+ const claimed = await claim(asRequest({ player: { id: "p1" }, body: { periodId } }), t.ctx);
41
+ expect(claimed.body).toMatchObject({ claimed: true, amount: 5, currency: "EUR" });
42
+
43
+ // The wallet command was recorded with a deterministic, period-scoped key.
44
+ expect(t.recorder.commands).toHaveLength(1);
45
+ expect(t.recorder.commands[0]).toMatchObject({ name: "wallet.bonus_award" });
46
+ expect(t.recorder.events.map((e) => e.name)).toEqual([
47
+ "plugin.__PLUGIN_KEY__.accrued",
48
+ "plugin.__PLUGIN_KEY__.claimed",
49
+ ]);
50
+
51
+ // A second claim of the SAME period is a 409, not a second credit.
52
+ const again = await claim(asRequest({ player: { id: "p1" }, body: { periodId } }), t.ctx);
53
+ expect(again.status).toBe(409);
54
+ expect(t.recorder.commands).toHaveLength(1);
55
+ });
56
+
57
+ it("never leaks another player's period", async () => {
58
+ const t = makeContext();
59
+ await accrue(asRequest({ body: { playerId: "p1", amount: 5 } }), t.ctx);
60
+ const summary = await getSummary(asRequest({ player: { id: "p2" } }), t.ctx);
61
+ expect(summary.body).toMatchObject({ claimableAmount: 0, periodCount: 0 });
62
+
63
+ const stolen = await claim(asRequest({ player: { id: "p2" }, body: { periodId: "cb_p1_x" } }), t.ctx);
64
+ expect(stolen.status).toBe(404);
65
+ });
66
+
67
+ it("nightly sweep flips accruing periods to claimable, idempotently", async () => {
68
+ const t = makeContext();
69
+ const periods = t.ctx.datasets.collection("accrual_periods");
70
+ await periods.put("cb_1", {
71
+ periodId: "cb_1",
72
+ playerId: "p1",
73
+ status: "accruing",
74
+ amount: 2.5,
75
+ currency: "EUR",
76
+ accruedAt: new Date().toISOString(),
77
+ });
78
+ await nightlyAccrual(t.ctx);
79
+ await nightlyAccrual(t.ctx); // idempotent
80
+ const record = await periods.get("cb_1");
81
+ expect(record).toMatchObject({ status: "claimable" });
82
+ });
83
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "verbatimModuleSyntax": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src", "test"]
14
+ }
@@ -0,0 +1,63 @@
1
+ # __PLUGIN_NAME__
2
+
3
+ A CasinoWebEngine game-provider plugin skeleton, scaffolded with `cwe-plugin create`.
4
+
5
+ ## The golden path
6
+
7
+ ```bash
8
+ pnpm create cwe-plugin my-cashback # scaffold: manifest, handlers, tests, config
9
+ cd my-cashback && pnpm install
10
+
11
+ pnpm dev # = cwe-plugin dev
12
+ # → first contact with the scaffold's preset `local` environment: the handshake verifies
13
+ # class=local + devHarness=true and pins its fingerprint → green banner
14
+ # → doctor passes → sandbox tenant resolved → plugin installed+enabled (prompted)
15
+ # → esbuild watch starts, bundle hot-loaded, log stream attached
16
+ # → edit src/handlers/routes.ts, save → rebuilt + hot-reloaded in <2s, logs streaming live
17
+
18
+ # in the dev session (keyboard): i install · e enable · c configure · x disable · u uninstall
19
+ # r reload · d doctor · l clear · q quit
20
+ # each lifecycle key prints a full hook trace: capability calls, events, audit refs, state
21
+
22
+ pnpm test # unit tests on hooks/handlers via the SDK test kit
23
+
24
+ cwe-plugin lifecycle cycle # conformance sweep: install→enable→configure→disable→
25
+ # uninstall→reinstall, asserting idempotency + clean teardown
26
+
27
+ cwe-plugin login # only needed for shared dev clusters / publishing
28
+ cwe-plugin env add dev-eu https://dev-eu.cwe.dev && cwe-plugin env use dev-eu
29
+ pnpm dev # same loop against the shared cluster, per-dev sandbox
30
+
31
+ pnpm publish:dev # = cwe-plugin publish — always the dev channel:
32
+ # doctor + codegen-diff gate + publish
33
+ ```
34
+
35
+ ## What's in this template
36
+
37
+ | File | Purpose |
38
+ |---|---|
39
+ | `cwe-plugin.json` | project config: plugin key, entry, environments (committed — no secrets, ever) |
40
+ | `src/index.ts` | `definePlugin({ manifest, hooks, handlers })` — the whole platform contract |
41
+ | `src/settings.ts` | Zod-typed tenant settings (one secret field included) |
42
+ | `src/adapter.ts` | the provider adapter — bets/settlements/rollbacks normalized into wallet commands |
43
+ | `src/fake-backend.ts` | in-memory provider API stand-in (swap for real `ctx.http` calls) |
44
+ | `src/handlers/hooks.ts` | lifecycle hooks (`onEnable` demands the apiKey secret) |
45
+ | `src/handlers/routes.ts` | signature-verified `/tx` callback + public `/status` |
46
+ | `src/handlers/jobs.ts` | nightly reconcile placeholder (dry-run with `cwe-plugin jobs run reconcile`) |
47
+ | `settings.dev.json` | sandbox settings applied by `cwe-plugin lifecycle configure` / the `c` key |
48
+ | `test/hooks.test.ts` | in-memory test-context examples — breakpoints work under `vitest --inspect` |
49
+
50
+ ## Useful commands
51
+
52
+ ```bash
53
+ cwe-plugin env status # am I pointed at a dev environment? auth? sandbox state?
54
+ cwe-plugin invoke POST /tx --surface callback \
55
+ --body '{"type":"wager","playerId":"p1","roundId":"r1","amount":"5.0000","currency":"EUR","providerTxId":"ptx1"}'
56
+ cwe-plugin settings set greeting=hi # validated locally against the schema before it is sent
57
+ cwe-plugin logs --level warn # standalone log tail for a second terminal
58
+ cwe-plugin doctor --remote # full server-side validation
59
+ ```
60
+
61
+ Machine-local state (pinned environment fingerprints, log cursors) lives in `.cwe/` — gitignored
62
+ and always safe to delete. Credentials never touch the repo: `cwe-plugin login` stores them in the
63
+ OS keychain.
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ .cwe/
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://cwe.dev/schemas/cwe-plugin.json",
3
+ "plugin": "__PLUGIN_KEY__",
4
+ "entry": "src/index.ts",
5
+ "environments": {
6
+ "local": { "runtimeUrl": "http://localhost:3000" }
7
+ },
8
+ "defaultEnvironment": "local"
9
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "cwe-plugin-__PLUGIN_KEY__",
3
+ "version": "0.1.0",
4
+ "description": "__PLUGIN_NAME__ \u2014 a CasinoWebEngine plugin",
5
+ "private": true,
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "scripts": {
11
+ "dev": "cwe-plugin dev",
12
+ "doctor": "cwe-plugin doctor",
13
+ "test": "vitest run",
14
+ "build": "cwe-plugin build",
15
+ "codegen": "cwe-plugin codegen client",
16
+ "publish:dev": "cwe-plugin publish"
17
+ },
18
+ "dependencies": {
19
+ "@clovnet/plugin-sdk": "^0.1.4"
20
+ },
21
+ "peerDependencies": {
22
+ "zod": "^3.23.0"
23
+ },
24
+ "devDependencies": {
25
+ "@clovnet/plugin-cli": "^0.2.0",
26
+ "@types/node": "^20.16.5",
27
+ "typescript": "^5.6.2",
28
+ "vitest": "^2.1.1",
29
+ "zod": "^3.23.8"
30
+ }
31
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "values": {
3
+ "baseUrl": "https://api.__PLUGIN_KEY__.example",
4
+ "operatorId": "op-demo"
5
+ },
6
+ "secrets": {
7
+ "apiKey": "dev-api-key",
8
+ "callbackSigningSecret": "dev-signing-secret"
9
+ }
10
+ }