@blotout/qinto-cli 0.1.20 → 0.1.21

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,133 +0,0 @@
1
- import type { AppUiContext } from '@blotout/qinto-sdk-app/server'
2
- import { defineApp } from '@blotout/qinto-sdk-app/server'
3
-
4
- type Env = { PURCHASES: D1Database }
5
-
6
- // The purchases table comes from the migrations qinto.app.jsonc declares, applied at deploy.
7
- const INSERT_PURCHASE = 'INSERT OR REPLACE INTO purchases (order_id, value, email_sha256, recorded_at) VALUES (?, ?, ?, ?)'
8
-
9
- const sha256Hex = async (value: string): Promise<string> => {
10
- const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value.trim().toLowerCase()))
11
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')
12
- }
13
-
14
- // The panel posts an install id; it is input, never authority. The handler resolves it against
15
- // ctx.installs, which the platform built from the site's own registry.
16
- const analytics = async (request: Request, ctx: AppUiContext<Env>): Promise<Response> => {
17
- const { installId } = (await request.json()) as { installId: string }
18
- const target = ctx.installs.find((entry) => entry.id === installId)
19
- if (target === undefined) {
20
- return Response.json({ error: 'unknown install' }, { status: 404 })
21
- }
22
-
23
- // That install's own secret, plus the app-scope secret you set once with `qinto app secret set`.
24
- const token = target.secrets.API_KEY
25
- const stats = await fetch(`${String(target.variables.ENDPOINT_URL)}/stats`, {
26
- headers: { authorization: `Bearer ${token}`, 'x-app-secret': ctx.appSecrets.VENDOR_APP_SECRET }
27
- })
28
-
29
- return Response.json({ install: target.label, stats: await stats.json() })
30
- }
31
-
32
- // Writing a setting back needs `ui.writesSettings: true` in the manifest, and a viewer with write
33
- // access. The value is authoritative at once but reaches ctx.variables on the next 5-minute sync —
34
- // state your own code must read back immediately belongs in your own KV or D1.
35
- const refresh = async (request: Request, ctx: AppUiContext<Env>): Promise<Response> => {
36
- const { installId } = (await request.json()) as { installId: string }
37
- if (ctx.viewer.access !== 'write') {
38
- return Response.json({ error: 'read-only viewer' }, { status: 403 })
39
- }
40
-
41
- await ctx.settings.write(installId, { LAST_SYNCED_AT: new Date().toISOString() })
42
-
43
- return Response.json({ refreshed: true })
44
- }
45
-
46
- export default defineApp<Env>({
47
- ui: async (request, ctx) => {
48
- const path = new URL(request.url).pathname
49
- if (path === '/analytics') {
50
- return analytics(request, ctx)
51
- }
52
-
53
- if (path === '/refresh') {
54
- return refresh(request, ctx)
55
- }
56
-
57
- return Response.json({ error: 'not found' }, { status: 404 })
58
- },
59
- transform: {
60
- track: async (action, ctx) => {
61
- // Reshape the payload with ctx.replace, discard the event with ctx.drop, or let it through.
62
- if (action.payload.event === 'Blocked Event') {
63
- return ctx.drop()
64
- }
65
-
66
- return ctx.replace({ ...action.payload, properties: { ...action.payload.properties, siteTag: ctx.variables.SITE_TAG } })
67
- }
68
- },
69
- onEvent: {
70
- track: async (action, ctx) => {
71
- if (action.payload.event === 'Purchase') {
72
- const email = ctx.user.properties.email
73
- const emailHash = typeof email === 'string' ? await sha256Hex(email) : null
74
- await ctx.env.PURCHASES.prepare(INSERT_PURCHASE)
75
- .bind(String(action.payload.properties.orderId), Number(action.payload.properties.value), emailHash, action.receivedAt)
76
- .run()
77
- ctx.observability.log('purchase recorded', { orderId: String(action.payload.properties.orderId) })
78
- }
79
-
80
- // A plain `fetch` here is traced for you and appears in the site's real-time console;
81
- // report a delivery that failed with ctx.observability.error so the trace marks it.
82
- const response = await fetch(String(ctx.variables.ENDPOINT_URL), {
83
- method: 'POST',
84
- headers: { 'content-type': 'application/json', authorization: `Bearer ${ctx.secrets.API_KEY}` },
85
- body: JSON.stringify({
86
- event: action.payload.event,
87
- properties: action.payload.properties,
88
- userId: action.userId,
89
- context: action.context,
90
- browser: action.browser
91
- })
92
- })
93
- if (!response.ok) {
94
- ctx.observability.error('delivery refused', undefined, { status: response.status, install: ctx.install.label })
95
-
96
- return
97
- }
98
-
99
- ctx.observability.log('track delivered', { event: action.payload.event, install: ctx.install.label })
100
- },
101
- identify: async (action, ctx) => {
102
- ctx.observability.log('identify received', { userId: action.userId, traits: Object.keys(action.payload.traits).length })
103
- }
104
- },
105
- onLifecycle: {
106
- installed: async (event, ctx) => {
107
- ctx.observability.log('install ready', { eventId: event.eventId, install: ctx.install.label })
108
- },
109
- configurationChanged: async (event, ctx) => {
110
- ctx.observability.log('configuration changed', {
111
- eventId: event.eventId,
112
- variables: event.changedVariables.length,
113
- secrets: event.changedSecrets.length
114
- })
115
- },
116
- uninstalled: async (event, ctx) => {
117
- ctx.observability.log('install removed', { eventId: event.eventId, install: ctx.install.label })
118
- }
119
- },
120
- // Keyed by the ids qinto.app.jsonc declares. Delivery is best effort — a run can be missed with no
121
- // catch-up, and one whose response was lost may still have happened, so a handler must be safe to
122
- // repeat. Work that must not be lost belongs in a workflow the handler starts.
123
- schedules: {
124
- 'daily-rollup': async (event, ctx) => {
125
- const counted = await ctx.env.PURCHASES.prepare('SELECT count(*) AS total FROM purchases').first<{ total: number }>()
126
- ctx.observability.log('rollup complete', {
127
- schedule: event.id,
128
- install: ctx.install.label,
129
- purchases: counted?.total ?? 0
130
- })
131
- }
132
- }
133
- })
@@ -1,53 +0,0 @@
1
- <html lang='en'>
2
- <head>
3
- <meta charset='utf-8' />
4
- <meta name='viewport' content='width=device-width, initial-scale=1' />
5
- <title>__APP_NAME__</title>
6
- </head>
7
- <body>
8
- <main>
9
- <h1>__APP_NAME__</h1>
10
- <p>The whole loop: pick an install, the panel calls this app's own <code>ui</code> server handler, and the handler reads that install's secret and calls out with the app's own credential.</p>
11
-
12
- <label for='install'>Install</label>
13
- <select id='install'></select>
14
- <button id='load' type='button'>Load stats</button>
15
- <button id='refresh' type='button'>Record a sync</button>
16
-
17
- <pre id='out'>Pick an install.</pre>
18
- </main>
19
- <script>
20
- // Resolved from the app's own dependency one directory up; the panel declares only its build tooling.
21
- import { createAppUi } from '@blotout/qinto-sdk-app/ui'
22
-
23
- const ui = createAppUi()
24
- const context = await ui.getContext()
25
- const out = document.getElementById('out')
26
- const select = document.getElementById('install')
27
-
28
- select.replaceChildren(
29
- ...context.installs.map((install) => {
30
- const option = document.createElement('option')
31
- option.value = install.id
32
- option.textContent = install.enabled ? install.label : `${install.label} (disabled)`
33
- return option
34
- })
35
- )
36
- select.value = context.appInstallId
37
-
38
- // ui.fetch resolves the app's own handler and attaches the panel token. It returns the raw
39
- // Response: parse it, and decide yourself what to do with a 429 or a 5xx.
40
- const call = async (path) => {
41
- const response = await ui.fetch(path, {
42
- method: 'POST',
43
- headers: { 'content-type': 'application/json' },
44
- body: JSON.stringify({ installId: select.value })
45
- })
46
- out.textContent = `${response.status} ${JSON.stringify(await response.json(), null, 2)}`
47
- }
48
-
49
- document.getElementById('load').addEventListener('click', () => call('/analytics'))
50
- document.getElementById('refresh').addEventListener('click', () => call('/refresh'))
51
- </script>
52
- </body>
53
- </html>
@@ -1,20 +0,0 @@
1
- {
2
- "$schema": "./node_modules/@blotout/qinto-sdk-app/app.schema.json",
3
- // The immutable identifier of the app on the Qinto platform.
4
- "slug": "__APP_SLUG__",
5
- "name": "__APP_NAME__",
6
- // Where the app plugs into the event pipeline: "transform" and/or "destination".
7
- "flows": ["destination"],
8
- // Events reach the app only when the visitor granted this consent category.
9
- "consent": { "category": "analytics" },
10
- // Replace these with your own hosted documents before publishing.
11
- "legal": {
12
- "termsUrl": "https://example.com/terms",
13
- "privacyUrl": "https://example.com/privacy"
14
- },
15
- "server": {
16
- "entry": "src/server.ts",
17
- // Must match the __hooks the server entry exports.
18
- "hooks": { "transform": [], "onEvent": ["track"], "onConnect": false }
19
- }
20
- }
@@ -1,9 +0,0 @@
1
- import { defineApp } from '@blotout/qinto-sdk-app/server'
2
-
3
- export default defineApp({
4
- onEvent: {
5
- track: async (action, ctx) => {
6
- ctx.observability.log('track received', { event: action.payload.event, install: ctx.install.label })
7
- }
8
- }
9
- })
@@ -1,3 +0,0 @@
1
- node_modules/
2
- .qinto/
3
- ui/dist/
@@ -1,13 +0,0 @@
1
- {
2
- "name": "__APP_SLUG__",
3
- "private": true,
4
- "type": "module",
5
- "dependencies": {
6
- "@blotout/qinto-sdk-app": "__APP_SDK_SPECIFIER__",
7
- "zod": "4.5.4"
8
- },
9
- "devDependencies": {
10
- "@cloudflare/workers-types": "5.20260819.1",
11
- "typescript": "7.0.2"
12
- }
13
- }
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2022",
4
- "module": "esnext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "noEmit": true,
8
- "skipLibCheck": true,
9
- "types": ["@cloudflare/workers-types"]
10
- },
11
- "include": ["src", "mocks"]
12
- }
@@ -1,6 +0,0 @@
1
- import { defineConfig } from 'astro/config'
2
-
3
- // The Qinto CLI injects BASE_PATH at publish time: the panel is served from /<appId>/<version>/.
4
- export default defineConfig({
5
- base: process.env.BASE_PATH ?? '/'
6
- })
@@ -1,12 +0,0 @@
1
- {
2
- "name": "__APP_SLUG__-ui",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "dev": "astro dev",
7
- "build": "astro build"
8
- },
9
- "dependencies": {
10
- "astro": "7.2.2"
11
- }
12
- }
@@ -1,50 +0,0 @@
1
- <html lang='en'>
2
- <head>
3
- <meta charset='utf-8' />
4
- <meta name='viewport' content='width=device-width, initial-scale=1' />
5
- <title>App panel</title>
6
- </head>
7
- <body>
8
- <main>
9
- <h1>App panel</h1>
10
- <p>This panel renders inside the Qinto platform. Outside it the bridge stays inert, so the fields below stay empty.</p>
11
- <dl>
12
- <dt>Site</dt>
13
- <dd id='site'></dd>
14
- <dt>Version</dt>
15
- <dd id='version'></dd>
16
- </dl>
17
- <h2>Installs</h2>
18
- <p>Every install the site made of this app, with the values it configured. A variable the manifest declares as a secret never reaches this panel — your server handler receives those.</p>
19
- <ul id='installs'></ul>
20
- </main>
21
- <script>
22
- // Resolved from the app's own dependency one directory up; the panel declares only its build tooling.
23
- import { createAppUi } from '@blotout/qinto-sdk-app/ui'
24
-
25
- const ui = createAppUi()
26
- const context = await ui.getContext()
27
-
28
- document.getElementById('site').textContent = context.siteOrigin
29
- document.getElementById('version').textContent = context.appVersion
30
-
31
- const line = (install) => {
32
- const values = Object.entries(install.variables).map(([name, value]) => `${name}=${value}`)
33
- const own = install.id === context.appInstallId ? ' — this panel' : ''
34
- return `${install.label}${own}: ${values.join(', ') || 'nothing configured yet'}`
35
- }
36
-
37
- document.getElementById('installs').replaceChildren(
38
- ...context.installs.map((install) => {
39
- const item = document.createElement('li')
40
- item.textContent = line(install)
41
- return item
42
- })
43
- )
44
-
45
- // ui.fetch(path) calls this app's own `ui` server handler with the panel token attached —
46
- // that is where a secret, an app-scope credential, or a settings write belongs. getToken() is
47
- // for calling your own endpoints instead.
48
- </script>
49
- </body>
50
- </html>