@ratio-app/cli 0.1.1
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 +504 -0
- package/bin/run.mjs +5 -0
- package/dist/commands/__test__/throw.d.ts +25 -0
- package/dist/commands/__test__/throw.js +238 -0
- package/dist/commands/__test__/throw.js.map +1 -0
- package/dist/commands/app/create.d.ts +55 -0
- package/dist/commands/app/create.js +609 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/deploy.d.ts +14 -0
- package/dist/commands/app/deploy.js +179 -0
- package/dist/commands/app/deploy.js.map +1 -0
- package/dist/commands/app/dev.d.ts +28 -0
- package/dist/commands/app/dev.js +701 -0
- package/dist/commands/app/dev.js.map +1 -0
- package/dist/commands/app/generate.d.ts +14 -0
- package/dist/commands/app/generate.js +179 -0
- package/dist/commands/app/generate.js.map +1 -0
- package/dist/commands/app/info.d.ts +14 -0
- package/dist/commands/app/info.js +179 -0
- package/dist/commands/app/info.js.map +1 -0
- package/dist/commands/app/link.d.ts +118 -0
- package/dist/commands/app/link.js +1120 -0
- package/dist/commands/app/link.js.map +1 -0
- package/dist/commands/auth/login.d.ts +43 -0
- package/dist/commands/auth/login.js +987 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.d.ts +15 -0
- package/dist/commands/auth/logout.js +486 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/auth/whoami.d.ts +16 -0
- package/dist/commands/auth/whoami.js +531 -0
- package/dist/commands/auth/whoami.js.map +1 -0
- package/dist/commands/dev/listen.d.ts +84 -0
- package/dist/commands/dev/listen.js +1187 -0
- package/dist/commands/dev/listen.js.map +1 -0
- package/dist/commands/dev/trigger.d.ts +88 -0
- package/dist/commands/dev/trigger.js +729 -0
- package/dist/commands/dev/trigger.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +617 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth/authorize-code-exchange.d.ts +50 -0
- package/dist/lib/auth/authorize-code-exchange.js +114 -0
- package/dist/lib/auth/authorize-code-exchange.js.map +1 -0
- package/dist/lib/auth/authorize-url.d.ts +42 -0
- package/dist/lib/auth/authorize-url.js +35 -0
- package/dist/lib/auth/authorize-url.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +16 -0
- package/dist/lib/auth/browser.js +48 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/config.d.ts +41 -0
- package/dist/lib/auth/config.js +21 -0
- package/dist/lib/auth/config.js.map +1 -0
- package/dist/lib/auth/http-refresher.d.ts +30 -0
- package/dist/lib/auth/http-refresher.js +103 -0
- package/dist/lib/auth/http-refresher.js.map +1 -0
- package/dist/lib/auth/identity.d.ts +26 -0
- package/dist/lib/auth/identity.js +10 -0
- package/dist/lib/auth/identity.js.map +1 -0
- package/dist/lib/auth/index.d.ts +13 -0
- package/dist/lib/auth/index.js +527 -0
- package/dist/lib/auth/index.js.map +1 -0
- package/dist/lib/auth/loopback-server.d.ts +65 -0
- package/dist/lib/auth/loopback-server.js +245 -0
- package/dist/lib/auth/loopback-server.js.map +1 -0
- package/dist/lib/auth/manual-prompt.d.ts +17 -0
- package/dist/lib/auth/manual-prompt.js +20 -0
- package/dist/lib/auth/manual-prompt.js.map +1 -0
- package/dist/lib/auth/pkce.d.ts +24 -0
- package/dist/lib/auth/pkce.js +15 -0
- package/dist/lib/auth/pkce.js.map +1 -0
- package/dist/lib/auth/state.d.ts +20 -0
- package/dist/lib/auth/state.js +19 -0
- package/dist/lib/auth/state.js.map +1 -0
- package/dist/lib/auth/verbose-trace.d.ts +25 -0
- package/dist/lib/auth/verbose-trace.js +17 -0
- package/dist/lib/auth/verbose-trace.js.map +1 -0
- package/dist/lib/credentials/clock.d.ts +18 -0
- package/dist/lib/credentials/clock.js +10 -0
- package/dist/lib/credentials/clock.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.js +340 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/path.d.ts +26 -0
- package/dist/lib/credentials/path.js +32 -0
- package/dist/lib/credentials/path.js.map +1 -0
- package/dist/lib/credentials/refresher.d.ts +26 -0
- package/dist/lib/credentials/refresher.js +34 -0
- package/dist/lib/credentials/refresher.js.map +1 -0
- package/dist/lib/credentials/schema.d.ts +138 -0
- package/dist/lib/credentials/schema.js +85 -0
- package/dist/lib/credentials/schema.js.map +1 -0
- package/dist/lib/credentials/store.d.ts +102 -0
- package/dist/lib/credentials/store.js +337 -0
- package/dist/lib/credentials/store.js.map +1 -0
- package/dist/lib/credentials/types.d.ts +65 -0
- package/dist/lib/credentials/types.js +1 -0
- package/dist/lib/credentials/types.js.map +1 -0
- package/dist/lib/dev-store-gate.d.ts +66 -0
- package/dist/lib/dev-store-gate.js +15 -0
- package/dist/lib/dev-store-gate.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.js +101 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/manifest/index.d.ts +2 -0
- package/dist/lib/manifest/index.js +144 -0
- package/dist/lib/manifest/index.js.map +1 -0
- package/dist/lib/manifest/read-manifest.d.ts +32 -0
- package/dist/lib/manifest/read-manifest.js +87 -0
- package/dist/lib/manifest/read-manifest.js.map +1 -0
- package/dist/lib/manifest/write-client-id.d.ts +33 -0
- package/dist/lib/manifest/write-client-id.js +94 -0
- package/dist/lib/manifest/write-client-id.js.map +1 -0
- package/dist/lib/messages.d.ts +15 -0
- package/dist/lib/messages.js +16 -0
- package/dist/lib/messages.js.map +1 -0
- package/dist/lib/orchestrator/child-runner.d.ts +140 -0
- package/dist/lib/orchestrator/child-runner.js +471 -0
- package/dist/lib/orchestrator/child-runner.js.map +1 -0
- package/dist/lib/preview/index.d.ts +11 -0
- package/dist/lib/preview/index.js +17 -0
- package/dist/lib/preview/index.js.map +1 -0
- package/dist/lib/preview/types.d.ts +16 -0
- package/dist/lib/preview/types.js +11 -0
- package/dist/lib/preview/types.js.map +1 -0
- package/dist/lib/ratio-command.d.ts +52 -0
- package/dist/lib/ratio-command.js +141 -0
- package/dist/lib/ratio-command.js.map +1 -0
- package/dist/lib/relay/index.d.ts +7 -0
- package/dist/lib/relay/index.js +362 -0
- package/dist/lib/relay/index.js.map +1 -0
- package/dist/lib/relay/relay-client.d.ts +91 -0
- package/dist/lib/relay/relay-client.js +362 -0
- package/dist/lib/relay/relay-client.js.map +1 -0
- package/dist/lib/relay/types.d.ts +71 -0
- package/dist/lib/relay/types.js +1 -0
- package/dist/lib/relay/types.js.map +1 -0
- package/dist/lib/render-error.d.ts +35 -0
- package/dist/lib/render-error.js +115 -0
- package/dist/lib/render-error.js.map +1 -0
- package/dist/lib/verbose-flag.d.ts +12 -0
- package/dist/lib/verbose-flag.js +17 -0
- package/dist/lib/verbose-flag.js.map +1 -0
- package/dist/render-CrHGqTPH.d.ts +115 -0
- package/dist/templates/.gitkeep +0 -0
- package/dist/templates/minimal/.env.example.tmpl +4 -0
- package/dist/templates/minimal/README.md.tmpl +26 -0
- package/dist/templates/minimal/gitignore +5 -0
- package/dist/templates/minimal/manifest.json +5 -0
- package/dist/templates/minimal/package.json.tmpl +25 -0
- package/dist/templates/minimal/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/minimal/src/index.ts.tmpl +26 -0
- package/dist/templates/minimal/src/install.ts.tmpl +37 -0
- package/dist/templates/minimal/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/minimal/tsconfig.json +17 -0
- package/dist/templates/serverless/README.md.tmpl +28 -0
- package/dist/templates/serverless/gitignore +6 -0
- package/dist/templates/serverless/manifest.json +5 -0
- package/dist/templates/serverless/package.json.tmpl +22 -0
- package/dist/templates/serverless/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/serverless/src/index.ts.tmpl +20 -0
- package/dist/templates/serverless/src/install.ts.tmpl +20 -0
- package/dist/templates/serverless/src/webhooks.ts.tmpl +36 -0
- package/dist/templates/serverless/tsconfig.json +18 -0
- package/dist/templates/serverless/wrangler.toml.tmpl +9 -0
- package/dist/templates/with-admin-ui/.env.example.tmpl +4 -0
- package/dist/templates/with-admin-ui/README.md.tmpl +28 -0
- package/dist/templates/with-admin-ui/gitignore +6 -0
- package/dist/templates/with-admin-ui/manifest.json +5 -0
- package/dist/templates/with-admin-ui/package.json.tmpl +31 -0
- package/dist/templates/with-admin-ui/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/with-admin-ui/src/admin/app/globals.css +19 -0
- package/dist/templates/with-admin-ui/src/admin/app/layout.tsx.tmpl +13 -0
- package/dist/templates/with-admin-ui/src/admin/app/page.tsx.tmpl +8 -0
- package/dist/templates/with-admin-ui/src/admin/next-env.d.ts.tmpl +5 -0
- package/dist/templates/with-admin-ui/src/admin/next.config.mjs +3 -0
- package/dist/templates/with-admin-ui/src/admin/tsconfig.json +20 -0
- package/dist/templates/with-admin-ui/src/index.ts.tmpl +26 -0
- package/dist/templates/with-admin-ui/src/install.ts.tmpl +37 -0
- package/dist/templates/with-admin-ui/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/with-admin-ui/tsconfig.json +17 -0
- package/package.json +78 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { verify } from '@ratio-app/sdk/webhooks';
|
|
2
|
+
import type { Env } from './index.js';
|
|
3
|
+
|
|
4
|
+
export async function handleWebhook(request: Request, env: Env): Promise<Response> {
|
|
5
|
+
// MUST read the raw body; do not JSON-parse before verify (SDK requires raw body for HMAC).
|
|
6
|
+
// The SDK accepts `string | Buffer`; the string path hashes the utf-8 payload bytes.
|
|
7
|
+
const rawBody = await request.text();
|
|
8
|
+
const signature = request.headers.get('x-ratio-signature') ?? '';
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
verify({
|
|
12
|
+
rawBody,
|
|
13
|
+
signature,
|
|
14
|
+
secret: env.RATIO_WEBHOOK_SECRET,
|
|
15
|
+
});
|
|
16
|
+
} catch (err) {
|
|
17
|
+
const code = (err as { code?: string }).code ?? 'unknown';
|
|
18
|
+
return new Response(JSON.stringify({ ok: false, code }), {
|
|
19
|
+
status: 400,
|
|
20
|
+
headers: { 'content-type': 'application/json' },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const event = JSON.parse(rawBody) as { type?: string };
|
|
25
|
+
switch (event.type) {
|
|
26
|
+
case 'orders.created':
|
|
27
|
+
// TODO: implement handler
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
33
|
+
status: 200,
|
|
34
|
+
headers: { 'content-type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["@cloudflare/workers-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
name = "{{appName}}"
|
|
2
|
+
main = "src/index.ts"
|
|
3
|
+
compatibility_date = "2026-01-15"
|
|
4
|
+
compatibility_flags = ["nodejs_compat"]
|
|
5
|
+
|
|
6
|
+
# Secrets — set via `wrangler secret put RATIO_CLIENT_SECRET` and
|
|
7
|
+
# `wrangler secret put RATIO_WEBHOOK_SECRET`. Do NOT commit real values.
|
|
8
|
+
[vars]
|
|
9
|
+
# Non-secret vars go here.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{appName}}
|
|
2
|
+
|
|
3
|
+
Scaffolded from the Ratio `{{templateName}}` template — an Express-based
|
|
4
|
+
Ratio app with OAuth install, verified webhooks, and a Next.js admin UI
|
|
5
|
+
under `src/admin`.
|
|
6
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
1. Install dependencies: `npm install`.
|
|
10
|
+
2. Create your Ratio app manually in the Ratio developer dashboard and copy the client id.
|
|
11
|
+
3. Open `ratio.config.jsonc` and paste the client id into the `clientId` field (or run `ratio app link` to auto-fill it).
|
|
12
|
+
4. Copy `.env.example` to `.env` and fill in `RATIO_CLIENT_SECRET` and `RATIO_WEBHOOK_SECRET` from the dashboard.
|
|
13
|
+
5. Run `npm run dev` to start the API server on port 3000, and `npm run admin:dev` to start the admin UI on `http://localhost:3001`.
|
|
14
|
+
6. Point your merchant test store at `https://<merchant>.<store>/oauth/callback` and `https://<merchant>.<store>/webhooks` (use a public tunnel such as ngrok or `ratio dev listen` for local testing).
|
|
15
|
+
|
|
16
|
+
## Project layout
|
|
17
|
+
|
|
18
|
+
- `src/index.ts` — Express bootstrap; mounts the install and webhook routes.
|
|
19
|
+
- `src/install.ts` — OAuth install callback (`GET /oauth/callback`); exchanges the authorization code for tokens.
|
|
20
|
+
- `src/webhooks.ts` — webhook receiver (`POST /webhooks`); verifies the HMAC signature over the raw body, then dispatches by topic.
|
|
21
|
+
- `src/admin/` — Next.js admin UI (own `tsconfig.json`; excluded from the root build).
|
|
22
|
+
- `ratio.config.jsonc` — the app manifest committed to this repo.
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
|
|
26
|
+
- The webhook route uses `express.raw({ type: 'application/json' })` on purpose: signature verification needs the exact raw bytes. Do not add a global JSON body parser in front of it.
|
|
27
|
+
- The admin UI is a starter stub — wire it to your own API routes as your app grows.
|
|
28
|
+
- You own the dependency pins in `package.json` (including `next`) — update them as your app evolves.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{appName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Ratio app scaffolded from the with-admin-ui template.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/index.ts",
|
|
8
|
+
"admin:dev": "next dev src/admin --port 3001",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"start": "node dist/index.js",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@ratio-app/sdk": "{{sdkVersion}}",
|
|
15
|
+
"express": "^4.19.0",
|
|
16
|
+
"next": "^14.2.0",
|
|
17
|
+
"react": "^18.3.0",
|
|
18
|
+
"react-dom": "^18.3.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/express": "^4.17.21",
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"@types/react": "^18.3.0",
|
|
24
|
+
"@types/react-dom": "^18.3.0",
|
|
25
|
+
"tsx": "^4.7.0",
|
|
26
|
+
"typescript": "^5.6.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Canonical Ratio app manifest. Committed to the app repo.
|
|
3
|
+
// Written by scaffolder; `clientId` filled by `ratio app link`.
|
|
4
|
+
"$schema": "https://ratio.app/schema/app-manifest.v1.json",
|
|
5
|
+
"appName": "{{appName}}",
|
|
6
|
+
"clientId": "{{clientId}}",
|
|
7
|
+
"sdkVersion": "{{sdkVersion}}",
|
|
8
|
+
"template": "{{templateName}}",
|
|
9
|
+
"webhooks": {
|
|
10
|
+
"path": "/webhooks",
|
|
11
|
+
"topics": []
|
|
12
|
+
},
|
|
13
|
+
"oauth": {
|
|
14
|
+
"redirectPath": "/oauth/callback",
|
|
15
|
+
"scopes": []
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
13
|
+
line-height: 1.5;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
h1 {
|
|
18
|
+
margin-bottom: 0.5rem;
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import './globals.css';
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: '{{appName}} — admin',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<body>{children}</body>
|
|
11
|
+
</html>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "preserve",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }]
|
|
17
|
+
},
|
|
18
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{appName}} — server entry point.
|
|
3
|
+
*
|
|
4
|
+
* Mounts the OAuth install callback and the verified webhook receiver.
|
|
5
|
+
*/
|
|
6
|
+
import express from 'express';
|
|
7
|
+
|
|
8
|
+
import { installRouter } from './install.js';
|
|
9
|
+
import { webhooksRouter } from './webhooks.js';
|
|
10
|
+
|
|
11
|
+
const app = express();
|
|
12
|
+
|
|
13
|
+
// Order matters: the webhook route registers its own raw-body parser;
|
|
14
|
+
// keep any global JSON parsing away from it.
|
|
15
|
+
app.use(installRouter);
|
|
16
|
+
app.use(webhooksRouter);
|
|
17
|
+
|
|
18
|
+
app.get('/', (_req, res) => {
|
|
19
|
+
res.json({ ok: true, app: '{{appName}}', template: '{{templateName}}' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const port = Number(process.env['PORT'] ?? 3000);
|
|
23
|
+
|
|
24
|
+
app.listen(port, () => {
|
|
25
|
+
console.log(JSON.stringify({ msg: 'server started', app: '{{appName}}', port }));
|
|
26
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth install callback.
|
|
3
|
+
*
|
|
4
|
+
* When a merchant approves the app, Ratio redirects the browser to
|
|
5
|
+
* `GET /oauth/callback?code=...`. This handler exchanges the code for
|
|
6
|
+
* tokens using the app's client secret.
|
|
7
|
+
*/
|
|
8
|
+
import { Router } from 'express';
|
|
9
|
+
import { exchangeToken, type ExchangeTokenArgs } from '@ratio-app/sdk/oauth';
|
|
10
|
+
|
|
11
|
+
export const installRouter: Router = Router();
|
|
12
|
+
|
|
13
|
+
installRouter.get('/oauth/callback', async (req, res) => {
|
|
14
|
+
const code = req.query['code'];
|
|
15
|
+
if (typeof code !== 'string' || code === '') {
|
|
16
|
+
res.status(400).json({ ok: false, error: 'missing_code' });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const args: ExchangeTokenArgs = {
|
|
21
|
+
code,
|
|
22
|
+
clientId: '{{clientId}}',
|
|
23
|
+
clientSecret: process.env['RATIO_CLIENT_SECRET'] ?? '',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// exchangeToken returns `unknown` — narrow before reading fields
|
|
28
|
+
// (e.g. with a type guard or a schema parser).
|
|
29
|
+
const tokens: unknown = await exchangeToken(args);
|
|
30
|
+
void tokens; // TODO: persist tokens for this merchant.
|
|
31
|
+
res.status(200).json({ ok: true });
|
|
32
|
+
} catch {
|
|
33
|
+
// Never log the raw error object here — it can carry secret material.
|
|
34
|
+
console.error(JSON.stringify({ msg: 'token exchange failed' }));
|
|
35
|
+
res.status(502).json({ ok: false, error: 'token_exchange_failed' });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook receiver.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the HMAC signature over the raw request bytes, then
|
|
5
|
+
* dispatches the event by topic.
|
|
6
|
+
*/
|
|
7
|
+
import express, { Router } from 'express';
|
|
8
|
+
import { verify, type VerifyInput } from '@ratio-app/sdk/webhooks';
|
|
9
|
+
|
|
10
|
+
export const webhooksRouter: Router = Router();
|
|
11
|
+
|
|
12
|
+
webhooksRouter.post(
|
|
13
|
+
'/webhooks',
|
|
14
|
+
// Raw body on purpose: the SDK requires the exact raw body bytes for
|
|
15
|
+
// HMAC verification. Parsing JSON first changes the byte sequence and
|
|
16
|
+
// the signature will not match.
|
|
17
|
+
express.raw({ type: 'application/json' }),
|
|
18
|
+
(req, res) => {
|
|
19
|
+
const rawBody = req.body as Buffer;
|
|
20
|
+
|
|
21
|
+
const input: VerifyInput = {
|
|
22
|
+
rawBody,
|
|
23
|
+
signature: String(req.headers['x-ratio-signature'] ?? ''),
|
|
24
|
+
secret: process.env['RATIO_WEBHOOK_SECRET'] ?? '',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
verify(input);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// The SDK error carries a stable `code`. Do not log the raw body
|
|
31
|
+
// or the full error object.
|
|
32
|
+
const code = (err as { code?: string }).code ?? 'unknown';
|
|
33
|
+
console.error(JSON.stringify({ msg: 'webhook verification failed', code }));
|
|
34
|
+
res.status(400).json({ ok: false, code });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Safe to parse now — the signature matched.
|
|
39
|
+
const event = JSON.parse(rawBody.toString('utf8')) as { type?: string };
|
|
40
|
+
|
|
41
|
+
switch (event.type) {
|
|
42
|
+
case 'orders.created':
|
|
43
|
+
// TODO: handle new orders.
|
|
44
|
+
break;
|
|
45
|
+
case 'products.updated':
|
|
46
|
+
// TODO: handle product updates.
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
console.log(
|
|
50
|
+
JSON.stringify({ msg: 'unhandled webhook topic', topic: event.type ?? 'unknown' }),
|
|
51
|
+
);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
res.status(200).json({ ok: true });
|
|
56
|
+
},
|
|
57
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"rootDir": "./src"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "src/admin"]
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ratio-app/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Official Ratio developer CLI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ratio",
|
|
7
|
+
"cli",
|
|
8
|
+
"oclif",
|
|
9
|
+
"developer-tools",
|
|
10
|
+
"ecommerce",
|
|
11
|
+
"webhooks",
|
|
12
|
+
"oauth",
|
|
13
|
+
"scaffold",
|
|
14
|
+
"app-platform"
|
|
15
|
+
],
|
|
16
|
+
"author": "Ratio",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"ratio": "bin/run.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin",
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"oclif": {
|
|
31
|
+
"bin": "ratio",
|
|
32
|
+
"commands": "./dist/commands",
|
|
33
|
+
"topicSeparator": " ",
|
|
34
|
+
"plugins": [
|
|
35
|
+
"@oclif/plugin-help",
|
|
36
|
+
"@oclif/plugin-version"
|
|
37
|
+
],
|
|
38
|
+
"topics": {
|
|
39
|
+
"auth": {
|
|
40
|
+
"description": "Sign in, sign out, and inspect the current Ratio user."
|
|
41
|
+
},
|
|
42
|
+
"app": {
|
|
43
|
+
"description": "Create, link, run, and deploy Ratio apps."
|
|
44
|
+
},
|
|
45
|
+
"dev": {
|
|
46
|
+
"description": "Local development utilities: forward and trigger webhook events."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"prepack": "npm run build"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@oclif/core": "^4.0.0",
|
|
58
|
+
"@oclif/plugin-help": "^6.2.53",
|
|
59
|
+
"@oclif/plugin-version": "^2.2.50",
|
|
60
|
+
"@ratio-app/sdk": "0.1.0-alpha.2",
|
|
61
|
+
"chalk": "^5.3.0",
|
|
62
|
+
"open": "^10.1.0",
|
|
63
|
+
"ws": "^8.18.0",
|
|
64
|
+
"zod": "^3.23.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/node": "^20.0.0",
|
|
68
|
+
"@types/ws": "^8.5.10",
|
|
69
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
70
|
+
"oclif": "^4.14.0",
|
|
71
|
+
"tsup": "^8.5.1",
|
|
72
|
+
"typescript": "^5.6.0",
|
|
73
|
+
"vitest": "^4.1.9"
|
|
74
|
+
},
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
77
|
+
}
|
|
78
|
+
}
|