@zhama/tego-plugin-sdk 0.0.0-stage → 1.36.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.
Files changed (98) hide show
  1. package/LICENSE +172 -0
  2. package/README.md +846 -2
  3. package/access-catalog.schema.json +304 -0
  4. package/access-fixtures/resolve.json +1359 -0
  5. package/bin/tego-plugin-pack.mjs +2 -0
  6. package/bin/tego-plugin.mjs +3 -0
  7. package/dist/access-drizzle.cjs +1 -0
  8. package/dist/access-drizzle.d.cts +726 -0
  9. package/dist/access-drizzle.d.ts +726 -0
  10. package/dist/access-drizzle.js +1 -0
  11. package/dist/access-hono.cjs +3 -0
  12. package/dist/access-hono.d.cts +108 -0
  13. package/dist/access-hono.d.ts +108 -0
  14. package/dist/access-hono.js +3 -0
  15. package/dist/access-implications.cjs +1 -0
  16. package/dist/access-implications.d.cts +25 -0
  17. package/dist/access-implications.d.ts +25 -0
  18. package/dist/access-implications.js +1 -0
  19. package/dist/access.cjs +26 -0
  20. package/dist/access.d.cts +884 -0
  21. package/dist/access.d.ts +884 -0
  22. package/dist/access.js +26 -0
  23. package/dist/catalog-CSSbrfEf.d.cts +361 -0
  24. package/dist/catalog-CSSbrfEf.d.ts +361 -0
  25. package/dist/chunk-3KUOBGJN.js +19 -0
  26. package/dist/chunk-3WA3M347.cjs +1 -0
  27. package/dist/chunk-5CU33JVY.js +1 -0
  28. package/dist/chunk-7K37PVGV.js +5 -0
  29. package/dist/chunk-7XHF3YSK.js +1 -0
  30. package/dist/chunk-AJY7ZHXK.cjs +3 -0
  31. package/dist/chunk-BN6TGHSB.cjs +1 -0
  32. package/dist/chunk-BTHXQWHT.js +1 -0
  33. package/dist/chunk-C2OCOPZQ.cjs +1 -0
  34. package/dist/chunk-IMAVYQ4U.cjs +4 -0
  35. package/dist/chunk-OP64Y4IN.cjs +1 -0
  36. package/dist/chunk-QKSPPMVN.js +1 -0
  37. package/dist/chunk-ROFUQLOZ.cjs +1 -0
  38. package/dist/chunk-SNUCO2MK.js +1 -0
  39. package/dist/chunk-THMWRUAT.cjs +19 -0
  40. package/dist/chunk-VMCGDDGP.js +3 -0
  41. package/dist/chunk-W7C357A6.cjs +5 -0
  42. package/dist/chunk-WLTASJF6.js +4 -0
  43. package/dist/egress.cjs +7 -0
  44. package/dist/egress.d.cts +9 -0
  45. package/dist/egress.d.ts +9 -0
  46. package/dist/egress.js +7 -0
  47. package/dist/frontend.cjs +2 -0
  48. package/dist/frontend.d.cts +353 -0
  49. package/dist/frontend.d.ts +353 -0
  50. package/dist/frontend.js +2 -0
  51. package/dist/gateway-signature.cjs +1 -0
  52. package/dist/gateway-signature.d.cts +112 -0
  53. package/dist/gateway-signature.d.ts +112 -0
  54. package/dist/gateway-signature.js +1 -0
  55. package/dist/index.cjs +1 -0
  56. package/dist/index.d.cts +528 -0
  57. package/dist/index.d.ts +528 -0
  58. package/dist/index.js +1 -0
  59. package/dist/manifest-types-DQARCQJt.d.cts +627 -0
  60. package/dist/manifest-types-DQARCQJt.d.ts +627 -0
  61. package/dist/manifest.cjs +1 -0
  62. package/dist/manifest.d.cts +2824 -0
  63. package/dist/manifest.d.ts +2824 -0
  64. package/dist/manifest.js +1 -0
  65. package/dist/mcp-identity.cjs +2 -0
  66. package/dist/mcp-identity.d.cts +106 -0
  67. package/dist/mcp-identity.d.ts +106 -0
  68. package/dist/mcp-identity.js +2 -0
  69. package/dist/pack.cjs +1 -0
  70. package/dist/pack.d.cts +169 -0
  71. package/dist/pack.d.ts +169 -0
  72. package/dist/pack.js +1 -0
  73. package/dist/publish.cjs +1 -0
  74. package/dist/publish.d.cts +96 -0
  75. package/dist/publish.d.ts +96 -0
  76. package/dist/publish.js +1 -0
  77. package/dist/scaffold.cjs +523 -0
  78. package/dist/scaffold.d.cts +182 -0
  79. package/dist/scaffold.d.ts +182 -0
  80. package/dist/scaffold.js +523 -0
  81. package/dist/server-runtime.cjs +1 -0
  82. package/dist/server-runtime.d.cts +63 -0
  83. package/dist/server-runtime.d.ts +63 -0
  84. package/dist/server-runtime.js +1 -0
  85. package/dist/server.cjs +1 -0
  86. package/dist/server.d.cts +1808 -0
  87. package/dist/server.d.ts +1808 -0
  88. package/dist/server.js +1 -0
  89. package/dist/testing.cjs +2 -0
  90. package/dist/testing.d.cts +92 -0
  91. package/dist/testing.d.ts +92 -0
  92. package/dist/testing.js +2 -0
  93. package/dist/vite.cjs +1 -0
  94. package/dist/vite.d.cts +133 -0
  95. package/dist/vite.d.ts +133 -0
  96. package/dist/vite.js +1 -0
  97. package/package.json +160 -4
  98. package/sql/access.sql +73 -0
@@ -0,0 +1,523 @@
1
+ var j=Object.defineProperty;var i=(e,t)=>j(e,"name",{value:t,configurable:!0});import{HOST_API_VERSION as O}from"./chunk-7XHF3YSK.js";import{existsSync as C,mkdirSync as y,mkdtempSync as R,writeFileSync as D,renameSync as I,rmSync as _}from"fs";import{join as p,dirname as L,resolve as $}from"path";var w={"@tego/plugin-access-ui":"@zhama/tego-plugin-access-ui","@tego/plugin-sdk":"@zhama/tego-plugin-sdk","@tego/ui":"@zhama/tego-ui"};function k(e,t,n){if(n==="workspace")return"workspace:*";const o=w[e];if(!o)throw new Error(`${e} is not a published SDK package`);return`npm:${o}@${t.hostApiRange}`}i(k,"sdkSpec");var F={semi:!0,trailingComma:"es5",singleQuote:!0,printWidth:80,tabWidth:2,useTabs:!1,bracketSpacing:!0,bracketSameLine:!1,arrowParens:"avoid",endOfLine:"lf",quoteProps:"as-needed",jsxSingleQuote:!0,proseWrap:"preserve"},M=`node_modules/
2
+ build/
3
+ dist/
4
+ frontend/dist/
5
+ backend/dist/
6
+ claw/dist/
7
+ coverage/
8
+ .turbo/
9
+ `,q=`allowBuilds:
10
+ esbuild: true
11
+ `,E={"tsconfig.base.json":{$schema:"https://json.schemastore.org/tsconfig",compilerOptions:{strict:!0,useUnknownInCatchVariables:!0,allowJs:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,resolveJsonModule:!0,isolatedModules:!0,verbatimModuleSyntax:!0,declaration:!0,declarationMap:!0,esModuleInterop:!0,incremental:!1,noUncheckedIndexedAccess:!0,moduleDetection:"force",exactOptionalPropertyTypes:!0,target:"ES2022",lib:["ES2022","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler"},exclude:["node_modules"]},"tsconfig.frontend.json":{$schema:"https://json.schemastore.org/tsconfig",extends:"./tsconfig.base.json",compilerOptions:{target:"ES2020",lib:["ES2020","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler",jsx:"react-jsx",noEmit:!1,declaration:!0,declarationMap:!0,outDir:"dist"}},"tsconfig.backend.json":{$schema:"https://json.schemastore.org/tsconfig",extends:"./tsconfig.base.json",compilerOptions:{target:"ES2022",lib:["ES2022"],module:"ESNext",moduleResolution:"bundler",types:["node"],noEmit:!1,declaration:!0,declarationMap:!0,outDir:"dist"}}};function u(e,t){return t==="workspace"?`@tego/tsconfig/plugin-${e}.json`:`../tsconfig.${e}.json`}i(u,"tsconfigPreset");var H=`import js from '@eslint/js';
12
+ import prettierConfig from 'eslint-config-prettier';
13
+ import prettier from 'eslint-plugin-prettier';
14
+ import reactHooks from 'eslint-plugin-react-hooks';
15
+ import globals from 'globals';
16
+ import tseslint from 'typescript-eslint';
17
+
18
+ export default tseslint.config(
19
+ {
20
+ ignores: [
21
+ 'build/**',
22
+ 'dist/**',
23
+ '**/dist/**',
24
+ '.turbo/**',
25
+ 'node_modules/**',
26
+ ],
27
+ },
28
+ js.configs.recommended,
29
+ ...tseslint.configs.recommended,
30
+ {
31
+ files: ['**/*.{ts,tsx,js,mjs}'],
32
+ languageOptions: {
33
+ globals: { ...globals.browser, ...globals.node },
34
+ },
35
+ plugins: { 'react-hooks': reactHooks, prettier },
36
+ rules: {
37
+ ...reactHooks.configs.recommended.rules,
38
+ 'prettier/prettier': 'warn',
39
+ },
40
+ },
41
+ prettierConfig
42
+ );
43
+ `,z={"@eslint/js":"^9.16.0","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.4","eslint-plugin-react-hooks":"^5.2.0",globals:"^15.13.0","typescript-eslint":"^8.17.0"};function B(){return[{path:".prettierrc.json",contents:c(F)},{path:".gitignore",contents:M},{path:"pnpm-workspace.yaml",contents:q},...Object.entries(E).map(([e,t])=>({path:e,contents:c(t)}))]}i(B,"standaloneRootFiles");var U={"@typescript/native-preview":"7.0.0-dev.20260519.1",eslint:"^9.39.1",prettier:"^3.7.4",typescript:"^5.9.3"},G=["zh-CN","zh-TW","en","ja","ko","de","es","fr","it","pt"];function h(e){return`@tego/plugin-${e.replace(/^tego-/,"")}`}i(h,"packageNameFor");function v(e){return e.replace(/^tego-/,"")}i(v,"keyNamespaceFor");function c(e){return`${JSON.stringify(e,null,2)}
44
+ `}i(c,"json");function K(e){const t=v(e.id),n=e.parts.includes("frontend"),o=e.parts.includes("backend"),a=e.parts.includes("claw"),s={};n&&(s.frontend={icon:"puzzle",nav:[{key:"home",labelKey:`${t}.nav.home`,icon:"layout-dashboard",order:0,path:"/home"}]}),o&&(s.backend={limits:{memoryMb:256,requestTimeoutMs:15e3}}),a&&(s.claw={hooks:{}});const r={manifestVersion:1,id:e.id,version:"0.1.0",displayName:{"zh-CN":e.displayNameZh,en:e.displayNameEn},description:{"zh-CN":e.descriptionZh,en:e.descriptionEn},author:"Tego"};return(n||o)&&(r.hostApiRange=e.hostApiRange),r.scope=e.scope,o&&(r.declares={database:!0}),r.parts=s,{path:"tego.plugin.json",contents:c(r)}}i(K,"manifest");function J(e,t){const n=e.parts.includes("frontend"),o=e.parts.includes("backend"),a=e.parts.includes("claw"),s=["tsgo --noEmit -p shared/tsconfig.json"];n&&s.push("tsgo --noEmit -p frontend/tsconfig.json"),o&&s.push("tsgo --noEmit -p backend/tsconfig.json"),a&&s.push("tsgo --noEmit -p claw/tsconfig.json");const r={"@tego/plugin-sdk":k("@tego/plugin-sdk",e,t),"@types/node":"^22.10.1",vitest:"^4.1.10"};t==="workspace"&&Object.assign(r,{"@tego/eslint-config":"workspace:*","@tego/tsconfig":"workspace:*"}),n&&Object.assign(r,{"@tanstack/react-query":"^5.62.7","@tego/ui":k("@tego/ui",e,t),"@types/react":"^19.2.1","@types/react-dom":"^19.2.1","lucide-react":"^0.469.0",react:"^19.0.0","react-dom":"^19.0.0","react-i18next":"15.4.1",vite:"^6.3.5"}),o&&Object.assign(r,{"@types/pg":"^8.11.10",hono:"^4.10.7",pg:"^8.13.1"}),a&&(r.typebox="1.3.16"),t==="npm"&&Object.assign(r,U,z);const l={name:h(e.id),version:"0.1.0",private:!0,type:"module",description:e.descriptionEn,scripts:{build:"tego-plugin-pack",watch:"tego-plugin-pack --watch","type-check":s.join(" && "),lint:"eslint .","lint:fix":"eslint . --fix",test:"vitest run"},devDependencies:Q(r)};return{path:"package.json",contents:c(l)}}i(J,"rootPackageJson");function Q(e){const t={};for(const n of Object.keys(e).sort())t[n]=e[n];return t}i(Q,"sortedRecord");function W(e){return e==="npm"?{path:"eslint.config.js",contents:H}:{path:"eslint.config.js",contents:`import baseConfig from '@tego/eslint-config';
45
+
46
+ export default [
47
+ ...baseConfig,
48
+ {
49
+ ignores: ['build/**', 'dist/**', '**/dist/**', '.turbo/**'],
50
+ },
51
+ ];
52
+ `}}i(W,"eslintConfig");function Z(e){return[{path:"shared/tsconfig.json",contents:c({extends:u("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1},include:["**/*.ts"]})},{path:"shared/index.ts",contents:`export {};
53
+ `}]}i(Z,"sharedFiles");function V(){return{path:"vitest.config.ts",contents:`import { defineConfig } from 'vitest/config';
54
+
55
+ /**
56
+ * Node environment, no jsdom: components are not unit tested here by policy
57
+ * (\`ui-no-unit-tests.mdc\`). Pure logic in shared/frontend/claw is tested along
58
+ * with backend SQL against a throwaway schema through \`createTestPluginHost\`.
59
+ *
60
+ * \`fileParallelism: false\` because each database-backed suite creates and drops
61
+ * a schema in one shared Postgres; running them concurrently works until two
62
+ * pick the same clock-derived name.
63
+ */
64
+ export default defineConfig({
65
+ test: {
66
+ environment: 'node',
67
+ include: [
68
+ 'shared/**/*.test.ts',
69
+ 'backend/src/**/*.test.ts',
70
+ 'claw/src/**/*.test.ts',
71
+ 'frontend/src/**/*.test.ts',
72
+ ],
73
+ fileParallelism: false,
74
+ testTimeout: 20_000,
75
+ hookTimeout: 30_000,
76
+ },
77
+ });
78
+ `}}i(V,"vitestConfig");function Y(e,t){if(t==="npm")return X(e);const n=h(e.id);return{path:"README.md",contents:`# ${e.displayNameEn}
79
+
80
+ ${e.descriptionEn}
81
+
82
+ Generated by \`pnpm plugin:new\`. The workflow, gates and local-dev loop are in
83
+ [\`plugins/README.md\`](../README.md); the manifest and host API contracts are in
84
+ [\`packages/plugin-sdk/README.md\`](../../packages/plugin-sdk/README.md).
85
+
86
+ ## Gates
87
+
88
+ \`\`\`bash
89
+ pnpm -F ${n} lint
90
+ pnpm -F ${n} type-check
91
+ pnpm -F ${n} test
92
+ pnpm -F ${n} build
93
+ \`\`\`
94
+
95
+ Run \`pnpm install\` once after generating, so the new package links into the
96
+ workspace and \`@tego/plugin-sdk\` resolves.
97
+ `}}i(Y,"readme");function X(e){return{path:"README.md",contents:`# ${e.displayNameEn}
98
+
99
+ ${e.descriptionEn}
100
+
101
+ A Tego plugin. The SDK packages are installed from npm under their \`@tego/*\`
102
+ names (see \`devDependencies\`), so code reads the same as plugins built inside
103
+ the Tego repository. The manifest, host API contracts and local-dev loop are
104
+ documented in \`node_modules/@tego/plugin-sdk/README.md\`.
105
+
106
+ ## Gates
107
+
108
+ \`\`\`bash
109
+ npm install
110
+ npm run lint
111
+ npm run type-check
112
+ npm test
113
+ npm run build # writes the uploadable archive
114
+ \`\`\`
115
+
116
+ \`hostApiRange\` in \`tego.plugin.json\` and the SDK ranges in \`package.json\`
117
+ move together: raise both when the plugin starts using a newer host API.
118
+ `}}i(X,"standaloneReadme");function ee(e,t){const n=v(e.id),o=`import { readFile, readdir } from 'node:fs/promises';
119
+ import { resolve } from 'node:path';
120
+
121
+ import { tegoPluginFrontend } from '@tego/plugin-sdk/vite';
122
+
123
+ const config = tegoPluginFrontend({
124
+ entry: 'frontend/src/index.tsx',
125
+ outDir: 'frontend/dist',
126
+ pluginId: '${e.id}',
127
+ });
128
+
129
+ // The local override must serve translations from the same build as the UI.
130
+ config.plugins!.push({
131
+ name: '${e.id}-locales',
132
+ async buildStart() {
133
+ const directory = resolve(import.meta.dirname, 'i18n');
134
+ for (const name of await readdir(directory)) {
135
+ if (!name.endsWith('.json')) continue;
136
+ const file = resolve(directory, name);
137
+ this.addWatchFile(file);
138
+ this.emitFile({
139
+ type: 'asset',
140
+ fileName: \`i18n/\${name}\`,
141
+ source: await readFile(file, 'utf8'),
142
+ });
143
+ }
144
+ },
145
+ });
146
+ export default config;
147
+ `,a=c({extends:u("frontend",t),compilerOptions:{rootDir:"..",noEmit:!0,declaration:!1,declarationMap:!1,types:["node"]},include:["src/**/*","vite.config.ts"],exclude:["node_modules"]}),s=`import { definePluginFrontend } from '@tego/plugin-sdk/frontend';
148
+ import { createRoot } from 'react-dom/client';
149
+
150
+ import { App } from './app';
151
+
152
+ /**
153
+ * The frontend part's entry.
154
+ *
155
+ * React comes from the shell (\`SHARED_MODULE_IDS\`), so this root shares the
156
+ * host's instance \u2014 but not its providers: a root mounted here sits outside the
157
+ * shell's tree, so anything provider-shaped this plugin needs it establishes
158
+ * itself. \`App\` is where that happens.
159
+ */
160
+ export default definePluginFrontend({
161
+ mount(el, ctx) {
162
+ const root = createRoot(el);
163
+ root.render(<App ctx={ctx} />);
164
+ return () => root.unmount();
165
+ },
166
+ });
167
+ `,r=`import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
168
+ import { pluginI18nNamespace } from '@tego/plugin-sdk';
169
+ import type { PluginHostContext } from '@tego/plugin-sdk/frontend';
170
+ import { EmptyState } from '@tego/ui';
171
+ import { Puzzle } from 'lucide-react';
172
+ import { Component, type ReactNode, useMemo } from 'react';
173
+ import { getI18n, I18nextProvider, useTranslation } from 'react-i18next';
174
+
175
+ /**
176
+ * The plugin's own error boundary.
177
+ *
178
+ * The shell's boundary sits outside this React root and never sees an error
179
+ * thrown inside it, so without one here a screen that throws renders as a blank
180
+ * rectangle that reads like the platform lost the page.
181
+ */
182
+ class PluginErrorBoundary extends Component<
183
+ { children: ReactNode; fallback: ReactNode },
184
+ { failed: boolean }
185
+ > {
186
+ state = { failed: false };
187
+ static getDerivedStateFromError() {
188
+ return { failed: true };
189
+ }
190
+ render() {
191
+ return this.state.failed ? this.props.fallback : this.props.children;
192
+ }
193
+ }
194
+
195
+ function Home() {
196
+ const { t } = useTranslation();
197
+ return (
198
+ <EmptyState
199
+ icon={<Puzzle />}
200
+ title={t('${n}.home.title')}
201
+ description={t('${n}.home.description')}
202
+ />
203
+ );
204
+ }
205
+
206
+ /**
207
+ * The plugin's root.
208
+ *
209
+ * Two things the shell would normally provide have to be re-established here,
210
+ * because context does not cross React roots: a query client, and the i18n
211
+ * namespace this plugin's bundle was merged under. The theme is *not*
212
+ * re-established \u2014 it rides on the document's root class that \`@tego/ui\` reads.
213
+ */
214
+ export function App({ ctx }: { ctx: PluginHostContext }) {
215
+ const queryClient = useMemo(() => new QueryClient(), []);
216
+ return (
217
+ <PluginErrorBoundary fallback={<EmptyState icon={<Puzzle />} title='\u2014' />}>
218
+ <QueryClientProvider client={queryClient}>
219
+ <I18nextProvider
220
+ i18n={getI18n()}
221
+ defaultNS={pluginI18nNamespace(ctx.pluginId)}
222
+ >
223
+ <Home />
224
+ </I18nextProvider>
225
+ </QueryClientProvider>
226
+ </PluginErrorBoundary>
227
+ );
228
+ }
229
+ `,l=[{path:"frontend/vite.config.ts",contents:o},{path:"frontend/tsconfig.json",contents:a},{path:"frontend/src/index.tsx",contents:s},{path:"frontend/src/app.tsx",contents:r}];for(const d of G){const g=d==="zh-CN",N={[n]:{nav:{home:g?"\u9996\u9875":"Home"},home:{title:g?e.displayNameZh:e.displayNameEn,description:g?e.descriptionZh:e.descriptionEn}}};l.push({path:`frontend/i18n/${d}.json`,contents:c(N)})}return l}i(ee,"frontendFiles");function te(e){return[{path:"backend/tsconfig.json",contents:c({extends:u("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1,rootDir:".."},include:["src/**/*"],exclude:["node_modules"]})},{path:"backend/src/index.ts",contents:`import { serveBackend } from '@tego/plugin-sdk/server-runtime';
230
+
231
+ import { backend } from './app';
232
+
233
+ /**
234
+ * The backend part's entry, run as \`node index.js\` by the plugin runner.
235
+ *
236
+ * \`serveBackend\` owns the socket, the health path and the caller-context
237
+ * headers; everything below it is a plain fetch handler.
238
+ */
239
+ serveBackend(backend);
240
+ `},{path:"backend/src/app.ts",contents:`import { definePluginBackend } from '@tego/plugin-sdk/server';
241
+ import type { PluginHostApi, PluginUserContext } from '@tego/plugin-sdk/server';
242
+ import { Hono } from 'hono';
243
+
244
+ /**
245
+ * The backend's HTTP surface, reached at \`/plugin-api/<id>/\`.
246
+ *
247
+ * The gateway authenticates the caller, strips their credentials and forwards
248
+ * what is left here, so there is no auth code below: \`ctx\` is already a
249
+ * resolved identity. The host capabilities (\`db\`, \`storage\`, \`events\`, \`log\`)
250
+ * arrive per request and are handed to handlers through \`c.env\`.
251
+ */
252
+ interface Env {
253
+ Bindings: { host: PluginHostApi; user: PluginUserContext };
254
+ }
255
+
256
+ const app = new Hono<Env>();
257
+
258
+ app.get('/ping', c => c.json({ ok: true }));
259
+
260
+ app.get('/notes', async c => {
261
+ const notes = await c.env.host.db.query<{ id: string; body: string }>(
262
+ 'SELECT id, body FROM notes ORDER BY created_at DESC'
263
+ );
264
+ return c.json({ notes });
265
+ });
266
+
267
+ app.post('/notes', async c => {
268
+ const { body } = await c.req.json<{ body?: unknown }>();
269
+ if (typeof body !== 'string' || body.trim() === '') {
270
+ return c.json({ error: 'body is required' }, 400);
271
+ }
272
+ const [row] = await c.env.host.db.query<{ id: string }>(
273
+ 'INSERT INTO notes (author_id, body) VALUES ($1, $2) RETURNING id',
274
+ [c.env.user.userId, body]
275
+ );
276
+ return c.json({ id: row!.id }, 201);
277
+ });
278
+
279
+ export const backend = definePluginBackend({
280
+ async fetch(request, ctx, host) {
281
+ return app.fetch(request, { host, user: ctx });
282
+ },
283
+ });
284
+ `},{path:"backend/src/app.test.ts",contents:`import { dirname, join } from 'node:path';
285
+ import { fileURLToPath } from 'node:url';
286
+
287
+ import {
288
+ createTestPluginHost,
289
+ type TestPluginHost,
290
+ } from '@tego/plugin-sdk/testing';
291
+ import { afterAll, beforeEach, describe, expect, it } from 'vitest';
292
+
293
+ import { backend } from './app';
294
+
295
+ /**
296
+ * Business Cases
297
+ *
298
+ * The scaffold's backend is a starting point, so these guard the two things a
299
+ * generated plugin must get right before its author changes anything.
300
+ *
301
+ * SCAF-001 GET /ping answers 200 with { ok: true } \u2014 the liveness contract the
302
+ * platform's health check depends on.
303
+ * SCAF-002 POST a note then GET the list round-trips through the plugin's own
304
+ * schema: the write is visible on the next read, which only holds if
305
+ * the migration applied and the scoped db writes where it reads.
306
+ * SCAF-003 POST with an empty body is refused with 400, not written \u2014 the one
307
+ * negative branch the handler encodes.
308
+ */
309
+
310
+ const migrationsDir = join(
311
+ dirname(fileURLToPath(import.meta.url)),
312
+ '..',
313
+ 'migrations'
314
+ );
315
+
316
+ const CALLER = { userId: 'user_test', tenantId: 'tenant_test', roles: [] };
317
+
318
+ function request(
319
+ host: TestPluginHost['host'],
320
+ method: string,
321
+ path: string,
322
+ body?: unknown
323
+ ): Promise<Response> {
324
+ // Built conditionally rather than \`body: undefined\`: RequestInit.body has no
325
+ // undefined member, so spelling it out fails the plugin's own type-check.
326
+ const init: RequestInit =
327
+ body === undefined
328
+ ? { method }
329
+ : {
330
+ method,
331
+ headers: { 'content-type': 'application/json' },
332
+ body: JSON.stringify(body),
333
+ };
334
+ return backend.fetch(
335
+ new Request(\`http://plugin.internal\${path}\`, init),
336
+ CALLER,
337
+ host
338
+ );
339
+ }
340
+
341
+ describe('scaffold backend', () => {
342
+ let harness: TestPluginHost;
343
+
344
+ beforeEach(async () => {
345
+ harness = await createTestPluginHost({
346
+ pluginId: 'scaffold',
347
+ migrationsDir,
348
+ });
349
+ });
350
+ afterAll(() => harness?.close());
351
+
352
+ it('[SCAF-001] answers /ping with 200 and { ok: true }', async () => {
353
+ const res = await request(harness.host, 'GET', '/ping');
354
+ expect(res.status).toBe(200);
355
+ expect(await res.json()).toEqual({ ok: true });
356
+ });
357
+
358
+ it('[SCAF-002] a posted note is visible on the next read', async () => {
359
+ const created = await request(harness.host, 'POST', '/notes', {
360
+ body: 'first note',
361
+ });
362
+ expect(created.status).toBe(201);
363
+ const { id } = (await created.json()) as { id: string };
364
+ expect(id).toMatch(/./);
365
+
366
+ const listed = await request(harness.host, 'GET', '/notes');
367
+ expect(listed.status).toBe(200);
368
+ expect(await listed.json()).toEqual({
369
+ notes: [{ id, body: 'first note' }],
370
+ });
371
+ });
372
+
373
+ it('[SCAF-003] refuses an empty body with 400 and writes nothing', async () => {
374
+ const res = await request(harness.host, 'POST', '/notes', { body: ' ' });
375
+ expect(res.status).toBe(400);
376
+
377
+ const listed = await request(harness.host, 'GET', '/notes');
378
+ expect(await listed.json()).toEqual({ notes: [] });
379
+ });
380
+ });
381
+ `},{path:"backend/migrations/0001_init.sql",contents:`-- The backend part's schema.
382
+ --
383
+ -- Applied by the host once, inside a transaction whose \`search_path\` is this
384
+ -- install's own schema and nothing else. So nothing here is schema-qualified,
385
+ -- nothing references a platform table, and there is no \`tenant_id\`: one schema
386
+ -- per (tenant, plugin) install is the isolation boundary.
387
+ --
388
+ -- \`gen_random_uuid()\` is in \`pg_catalog\`, always on the path, so it needs no
389
+ -- extension. Migrations are append-only \u2014 correct a past one with a new file.
390
+
391
+ CREATE TABLE notes (
392
+ id text PRIMARY KEY DEFAULT gen_random_uuid(),
393
+ author_id text NOT NULL,
394
+ body text NOT NULL,
395
+ created_at timestamptz NOT NULL DEFAULT now()
396
+ );
397
+ `}]}i(te,"backendFiles");function ne(e,t){const n=c({id:e.id,name:e.displayNameEn,description:e.descriptionEn,enabledByDefault:!0,activation:{onStartup:!0},configSchema:{type:"object",additionalProperties:!1,properties:{}}}),o=c({name:e.id,private:!0,type:"module",openclaw:{extensions:["./index.js"]},version:"0.1.0"}),a=`{
398
+ "extends": "${u("backend",t)}",
399
+ "compilerOptions": {
400
+ "rootDir": "..",
401
+ "noEmit": true,
402
+ "declaration": false,
403
+ "declarationMap": false,
404
+ /*
405
+ * The claw part imports \`openclaw/plugin-sdk/*\`, resolved at runtime by
406
+ * claw's own alias map inside the container and not installable here. Types
407
+ * for those specifiers come from the local \`openclaw-plugin-sdk.d.ts\` shim.
408
+ */
409
+ "paths": {}
410
+ },
411
+ "include": ["src/**/*", "types/**/*"],
412
+ "exclude": ["node_modules"]
413
+ }
414
+ `,s=`/**
415
+ * The slice of claw's plugin SDK this extension uses.
416
+ *
417
+ * Declared here rather than imported because a plugin builds outside claw's
418
+ * workspace: \`openclaw/plugin-sdk/*\` is resolved at runtime by claw's own alias
419
+ * map inside the container, and claw ships no declaration files. Keep it narrow
420
+ * \u2014 only what \`claw/src\` calls \u2014 so it drifts on a small surface. Grow it as
421
+ * the plugin registers more; the full surface lives in \`plugins/tego-crm\`.
422
+ */
423
+
424
+ declare module 'openclaw/plugin-sdk/tego' {
425
+ /**
426
+ * \`api.logger\` takes one string. A second structured argument throws inside
427
+ * claw (\`message.startsWith is not a function\`) \u2014 serialize context into it.
428
+ */
429
+ export interface OpenClawPluginLogger {
430
+ info(message: string): void;
431
+ warn(message: string): void;
432
+ error(message: string): void;
433
+ debug?(message: string): void;
434
+ }
435
+
436
+ /** A tool's parameter schema \u2014 claw's own \`TSchema\` from typebox. */
437
+ export type OpenClawToolSchema = import('typebox').TSchema;
438
+
439
+ export interface OpenClawToolResult<TDetails = unknown> {
440
+ content: { type: 'text'; text: string }[];
441
+ details: TDetails;
442
+ }
443
+
444
+ export interface OpenClawAgentTool {
445
+ name: string;
446
+ label: string;
447
+ description: string;
448
+ parameters: OpenClawToolSchema;
449
+ execute(
450
+ toolCallId: string,
451
+ params: unknown,
452
+ signal?: AbortSignal
453
+ ): Promise<OpenClawToolResult<unknown>>;
454
+ }
455
+
456
+ export interface OpenClawPluginApi {
457
+ readonly id: string;
458
+ /**
459
+ * Absent or partial during claw's early registration phases \u2014 only fields
460
+ * carrying a \`default\` in \`configSchema\` are present then. Always guard.
461
+ */
462
+ readonly pluginConfig?: Record<string, unknown>;
463
+ readonly logger: OpenClawPluginLogger;
464
+ /**
465
+ * Refused unless every registered name appears in the manifest's
466
+ * \`contracts.tools\` \u2014 and the refusal happens inside the container, at
467
+ * startup, where nobody is watching.
468
+ */
469
+ registerTool(tool: OpenClawAgentTool): void;
470
+ }
471
+
472
+ export interface DefinePluginEntryOptions {
473
+ id: string;
474
+ name: string;
475
+ description: string;
476
+ register(api: OpenClawPluginApi): void | Promise<void>;
477
+ }
478
+
479
+ export function definePluginEntry(
480
+ options: DefinePluginEntryOptions
481
+ ): DefinePluginEntryOptions;
482
+ }
483
+
484
+ declare module 'openclaw/plugin-sdk/tego-api' {
485
+ /**
486
+ * The hub is installed by claw's own tego plugin at startup. A plugin that
487
+ * registers tools before it exists produces tool calls that always fail, so
488
+ * check this in \`register()\` and skip registration instead.
489
+ */
490
+ export function isTegoApiInstalled(): boolean;
491
+ }
492
+ `,r=`import {
493
+ definePluginEntry,
494
+ type OpenClawPluginApi,
495
+ } from 'openclaw/plugin-sdk/tego';
496
+ import { isTegoApiInstalled } from 'openclaw/plugin-sdk/tego-api';
497
+
498
+ /**
499
+ * The claw part's entry.
500
+ *
501
+ * \`register()\` runs once per agent during startup (dozens of times), so
502
+ * everything below it must be idempotent and free of global side effects. Tools
503
+ * reach the plugin's own backend through the tego API hub, which claw installs
504
+ * at startup \u2014 so if it is absent, skip registration rather than register tools
505
+ * that would fail on every call.
506
+ */
507
+ export default definePluginEntry({
508
+ id: '${e.id}',
509
+ name: '${e.displayNameEn}',
510
+ description: '${e.descriptionEn}',
511
+ register(api: OpenClawPluginApi) {
512
+ if (!isTegoApiInstalled()) {
513
+ api.logger.warn(
514
+ '${e.id}: tego-api hub not installed \u2014 tools will not be registered'
515
+ );
516
+ return;
517
+ }
518
+ // Register tools here with \`api.registerTool(...)\`. Each tool's name must
519
+ // also appear in \`contracts.tools\` in \`openclaw.plugin.json\`, or claw
520
+ // refuses the registration.
521
+ },
522
+ });
523
+ `;return[{path:"claw/openclaw.plugin.json",contents:n},{path:"claw/package.json",contents:o},{path:"claw/tsconfig.json",contents:a},{path:"claw/types/openclaw-plugin-sdk.d.ts",contents:s},{path:"claw/src/index.ts",contents:r}]}i(ne,"clawFiles");function m(e,{dependencies:t="workspace"}={}){const n=[K(e),J(e,t),W(t),...Z(t),V(),Y(e,t)];return t==="npm"&&n.push(...B()),e.parts.includes("frontend")&&n.push(...ee(e,t)),e.parts.includes("backend")&&n.push(...te(t)),e.parts.includes("claw")&&n.push(...ne(e,t)),n}i(m,"buildPluginFiles");var T=/^[a-z0-9][a-z0-9._-]*$/,f=["frontend","backend","claw"],b=["avatar","tenant"],x="lowercase letters and digits, may contain . _ -, starting with a letter or digit";function P(e,t){if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("answers must be a JSON object");const n=e,o=i(l=>{const d=n[l];if(typeof d!="string"||d.trim()==="")throw new Error(`${l} is required`);return d.trim()},"text"),a=o("id");if(!T.test(a))throw new Error(`id must be ${x}`);const s=n.parts;if(!Array.isArray(s)||s.length===0||s.some(l=>!f.includes(l)))throw new Error(`parts must be a non-empty list of ${f.join(", ")}`);const r=n.scope??"avatar";if(!b.includes(r))throw new Error(`scope must be one of ${b.join(", ")}`);return{id:a,displayNameZh:o("displayNameZh"),displayNameEn:o("displayNameEn"),descriptionZh:o("descriptionZh"),descriptionEn:o("descriptionEn"),parts:[...new Set(s)],scope:r,hostApiRange:t}}i(P,"validatePluginAnswers");function oe(e,t){return A(p(e,"plugins"),t,m(t))}i(oe,"writePlugin");function S(e,t){return A(e,t,m(t,{dependencies:"npm"}))}i(S,"writeStandalonePlugin");function A(e,t,n){const o=p(e,t.id);if(C(o))throw new Error(`${o} already exists \u2014 pick another id or remove it first`);y(e,{recursive:!0});const a=R(p(e,`.${t.id}-`));try{for(const s of n){const r=p(a,s.path);y(L(r),{recursive:!0}),D(r,s.contents)}I(a,o)}catch(s){throw _(a,{recursive:!0,force:!0}),s}return{directory:o,files:n.map(s=>s.path)}}i(A,"writePluginDirectory");function se(e,t){let n;try{n=JSON.parse(e)}catch(a){throw new Error(`answers must be JSON: ${a instanceof Error?a.message:String(a)}`)}const o=P(n,`^${O}`);return S($(t.cwd,t.parent),o)}i(se,"createStandalonePlugin");export{x as PLUGIN_ID_HINT,T as PLUGIN_ID_PATTERN,f as PLUGIN_PARTS,b as PLUGIN_SCOPES,w as PUBLISHED_SDK_PACKAGES,E as STANDALONE_TSCONFIG_FILES,m as buildPluginFiles,se as createStandalonePlugin,h as packageNameFor,P as validatePluginAnswers,oe as writePlugin,S as writeStandalonePlugin};
@@ -0,0 +1 @@
1
+ "use strict";var S=Object.defineProperty;var n=(a,e)=>S(a,"name",{value:e,configurable:!0});var chunkIMAVYQ4U_cjs=require("./chunk-IMAVYQ4U.cjs"),chunkBN6TGHSB_cjs=require("./chunk-BN6TGHSB.cjs"),http=require("http");function toArrayBuffer(a){const e=new ArrayBuffer(a.byteLength);return new Uint8Array(e).set(a),e}n(toArrayBuffer,"toArrayBuffer");function readEnv(a){const e=n(s=>{const c=a[s];if(!c)throw new Error(`${s} is required but was not set`);return c},"require_");return{pluginId:e("TEGO_PLUGIN_ID"),tenantId:e("TEGO_TENANT_ID"),port:Number(e("TEGO_PLUGIN_PORT")),healthPath:a.TEGO_PLUGIN_HEALTH_PATH??"/healthz",hostApiUrl:e("TEGO_PLUGIN_HOST_API_URL"),hostApiToken:e("TEGO_PLUGIN_HOST_API_TOKEN"),databaseUrl:e("DATABASE_URL"),gatewayKey:e("TEGO_PLUGIN_GATEWAY_KEY")}}n(readEnv,"readEnv");function readUserContext(a,e){const s=n(h=>{const A=a[h],I=Array.isArray(A)?A[0]:A;return I&&I.length>0?I:null},"read"),c=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.userId),d=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId);if(!c||!d)return null;const p=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature),w=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt);if(!p||!w)return null;const y=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.avatarId),m=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.roles),i=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.delegation),u={userId:c,tenantId:d,rolesHeader:m??"",avatarId:y??"",issuedAt:w,...i?{delegationHeader:i}:{}};if(!chunkIMAVYQ4U_cjs.verifyPluginContext({context:u,signature:p,key:e.key,nowMs:e.nowMs}))return null;let T=[];if(m)try{const h=JSON.parse(m);if(!Array.isArray(h))return null;T=h.filter(A=>typeof A=="string")}catch{return null}let E;if(i){if(!y)return null;try{const h=JSON.parse(i);if(!h||typeof h.id!="string"||!h.id||!Array.isArray(h.actions)||h.actions.some(A=>A!=="records.read"&&A!=="work.prepare"))return null;E={id:h.id,actions:h.actions}}catch{return null}}return{userId:c,tenantId:d,roles:T,...y?{avatarId:y}:{},...E?{delegation:E}:{}}}n(readUserContext,"readUserContext");function readPublicContext(a,e){const s=n(T=>{const E=a[T],h=Array.isArray(E)?E[0]:E;return h&&h.length>0?h:null},"read"),c=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId),d=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.installId),p=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.publicRoute),w=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.clientIp),y=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.avatarId),m=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt),i=s(chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature);if(!c||!d||!p||!w||!m||!i)return null;const u={tenantId:c,installId:d,route:p,clientIp:w,...y?{avatarId:y}:{}};return chunkIMAVYQ4U_cjs.verifyPluginPublicContext({context:{...u,issuedAt:m},signature:i,key:e.key,nowMs:e.nowMs})?u:null}n(readPublicContext,"readPublicContext");function createHostClient(a){const e=n(async(t,o)=>{const r=await fetch(`${a.hostApiUrl}${t}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify(o),...t.startsWith("/knowledge/collections/")||t.startsWith("/knowledge/admin/")?{signal:AbortSignal.timeout(12e4)}:t==="/mail/send"?{signal:AbortSignal.timeout(6e4)}:t.startsWith("/directory/")||t.startsWith("/knowledge/")||t.startsWith("/delegations/")||t.startsWith("/proactive/")||t==="/mail/status"||t==="/mail/unsubscribes"||t==="/events"?{signal:AbortSignal.timeout(1e4)}:{}});if(!r.ok){const l=await r.text();let g;try{if(g=JSON.parse(l)?.error?.details,typeof g=="string")try{g=JSON.parse(g)}catch{}}catch{}throw Object.assign(new Error(`Tego host API ${t} failed: ${r.status} ${l}`),{status:r.status,details:g,retryAfter:r.headers.get("retry-after")})}return r},"call"),s={debug:n((t,o)=>void c("debug",t,o),"debug"),info:n((t,o)=>void c("info",t,o),"info"),warn:n((t,o)=>void c("warn",t,o),"warn"),error:n((t,o)=>void c("error",t,o),"error")};async function c(t,o,r){try{await e("/log",{level:t,message:o,fields:r})}catch{}}n(c,"emitLog");const d=n((t,o)=>`${a.hostApiUrl}/storage/${t}?key=${encodeURIComponent(o)}`,"storageUrl"),p={copy:n(async(t,o)=>{const l=await(await e("/storage/copy",{sourceKey:t,destinationKey:o})).json();if(!l.data)throw new Error("Tego host API returned no copied object");return l.data},"copy"),getStream:n(async t=>{const o=await fetch(d("stream",t),{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage stream failed: ${o.status}`);return o.body},"getStream"),putStream:n(async(t,o,r)=>{const l={method:"POST",duplex:"half",body:o,headers:{authorization:`Bearer ${a.hostApiToken}`,"content-length":String(r.sizeBytes),"content-type":r.contentType??"application/octet-stream"}},g=await fetch(d("stream",t),l);if(!g.ok)throw new Error(`Tego storage stream write failed: ${g.status}`)},"putStream"),put:n(async(t,o,r)=>{const l=await fetch(d("put",t),{method:"POST",headers:{authorization:`Bearer ${a.hostApiToken}`,"content-type":r??"application/octet-stream"},body:toArrayBuffer(o)});if(!l.ok)throw new Error(`Tego storage put failed: ${l.status}`)},"put"),get:n(async(t,o)=>{const r=o?.firstBytes,l=d("get",t),g=await fetch(r===void 0?l:`${l}&bytes=${r}`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(g.status===404)return null;if(!g.ok)throw new Error(`Tego storage get failed: ${g.status}`);return new Uint8Array(await g.arrayBuffer())},"get"),head:n(async t=>{const o=await fetch(`${a.hostApiUrl}/storage/head`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${a.hostApiToken}`},body:JSON.stringify({key:t})});if(o.status===404)return null;if(!o.ok)throw new Error(`Tego storage head failed: ${o.status}`);const r=await o.json();if(!r.data)throw new Error("Tego host API returned no object head");return r.data},"head"),delete:n(async t=>{await e("/storage/delete",{key:t})},"delete"),signedUrl:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.url;if(!g)throw new Error("Tego host API returned no signed url");return g},"signedUrl"),signedPath:n(async(t,o)=>{const g=(await(await e("/storage/signed-url",{key:t,ttlSeconds:o})).json()).data?.path;if(!g)throw new Error("Tego host API returned no signed path");return g},"signedPath"),managedPath:n(async(t,o,r)=>{const g=await(await e("/storage/signed-url",{key:t,policy:o,ttlSeconds:r})).json();if(!g.data?.path)throw new Error("Tego host API returned no managed path");return g.data.path},"managedPath"),uploadPath:n(async(t,o)=>{const l=await(await e("/storage/upload-url",{key:t,maxBytes:o?.maxBytes,ttlSeconds:o?.ttlSeconds})).json();if(!l.data?.path)throw new Error("Tego host API returned no upload path");return l.data},"uploadPath")},w={publish:n(async(t,o)=>{await e("/events",{name:t,payload:o})},"publish")},y={send:n(async t=>{const r=await(await e("/notifications",t)).json();if(!r.data?.id)throw new Error("Tego host API returned no notification id");return{id:r.data.id}},"send")},m={async emit(t){const r=await(await e("/proactive/emit",t)).json();if(!r.data)throw new Error("Tego proactive response is missing data");return r.data}},i=n(async(t,o)=>{const l=await(await e(t,o)).json();if(!l.data)throw new Error("Tego attention response is missing data");return l.data},"attentionStatus"),u={async request(t){const r=await(await e("/attention/requests",t)).json();if(!r.data?.itemId)throw new Error("Tego host API returned no attention item");return r.data},get:n(t=>i("/attention/requests/get",{itemId:t}),"get"),cancel:n(t=>i("/attention/requests/cancel",{itemId:t}),"cancel")},T={async send(t){const r=await(await e("/mail/send",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async status(t){const r=await(await e("/mail/status",{userId:t})).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data},async unsubscribes(t={}){const r=await(await e("/mail/unsubscribes",t)).json();if(!r.data)throw new Error("Tego mail response is missing data");return r.data}};let E={};const A={current:n(()=>E,"current"),refresh:n(async()=>{const t=await fetch(`${a.hostApiUrl}/config`,{headers:{authorization:`Bearer ${a.hostApiToken}`}});if(!t.ok)throw new Error(`Tego host API /config failed: ${t.status} ${await t.text()}`);return E=(await t.json()).data?.values??{},E},"refresh")},I={async read(t,o){return(await(await e("/sources/read",{id:t,...o})).json()).data}},v={async list(t){const r=await(await e("/delegations/list",{userId:t})).json();if(!r.data)throw new Error("Tego delegation response is missing data");return r.data}},P={async verify(t){const r=await(await e("/knowledge/verify",t)).json();if(!r.data)throw new Error("Tego knowledge response is missing data");return r.data},collections:{ensure:n(t=>f("ensure",t),"ensure"),permissions:n(t=>f("permissions",t),"permissions"),move:n(t=>f("move",t),"move"),access:n(t=>f("access",t),"access"),import:n(t=>f("import",t),"import"),importFiles:n(t=>f("import-files",t),"importFiles"),status:n(t=>f("status",t),"status"),delete:n(t=>f("delete",t),"delete"),documentsStatus:n(t=>f("documents-status",t),"documentsStatus"),documentContent:n(t=>f("document-content",t),"documentContent"),search:n(t=>f("search",t),"search"),remove:n(t=>f("remove",t),"remove")}};async function f(t,o){const l=await(await e(`/knowledge/collections/${t}`,o)).json();if(!l.data)throw new Error("Tego knowledge collection response is missing data");return l.data}n(f,"collectionCall");const _={async identity(t){const r=await(await e("/directory/identity",{userId:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async validate(t){const r=await(await e("/directory/validate",{principals:t})).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async list(t){const r=await(await e("/directory/list",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data},async match(t){const r=await(await e("/directory/match",t)).json();if(!r.data)throw new Error("Tego directory response is missing data");return r.data}},k={provisionUser:n(t=>b("/accounts/provision",t),"provisionUser"),resetPassword:n(t=>b("/accounts/password/reset",t),"resetPassword"),inviteUser:n(t=>b("/accounts/invite",t),"inviteUser"),getInvitation:n(t=>b("/accounts/get",{invitationId:t}),"getInvitation"),resendInvitation:n(t=>b("/accounts/resend",{invitationId:t}),"resendInvitation")};async function b(t,o){const l=await(await e(t,o)).json();if(!l.data)throw new Error("Tego account response is missing data");return l.data}return n(b,"collectionEnvelope"),{accounts:k,log:s,storage:p,diagrams:{status:n(async()=>{const o=await(await e("/diagrams/status",{})).json();if(!o.data)throw new Error("Tego host API returned no renderer status");return o.data},"status"),render:n(async t=>{const r=await(await e("/diagrams/render",t)).json();if(!r.data?.key)throw new Error("Tego host API returned no diagram artifact");return r.data},"render")},events:w,notifications:y,proactive:m,attention:u,mail:T,config:A,sources:I,directory:_,knowledge:P,knowledgeAdmin:{listManageableBases:n(t=>b("/knowledge/admin/list",t),"listManageableBases"),previewCreateBase:n(t=>b("/knowledge/admin/create-preview",t),"previewCreateBase"),createApprovedBase:n(t=>b("/knowledge/admin/create",t),"createApprovedBase"),prepareDocumentContent:n(t=>b("/knowledge/admin/prepare",t),"prepareDocumentContent"),previewDocumentBatch:n(t=>b("/knowledge/admin/preview",t),"previewDocumentBatch"),stageApprovedDocumentBatch:n(t=>b("/knowledge/admin/import",t),"stageApprovedDocumentBatch"),getTask:n(t=>b("/knowledge/admin/task",t),"getTask")},delegations:v}}n(createHostClient,"createHostClient");var ACQUIRE_TIMEOUT_MS=5e3,IDLE_IN_TRANSACTION_TIMEOUT_MS=15e3;function createScopedDb(a){let e=null;async function s(){if(e)return e;const c=await import("pg"),d=c.default?.Pool??c.Pool;if(!d)throw new Error('The "pg" module resolved without a Pool constructor. The plugin runtime expects the driver the runner image provides.');return e=new d({connectionString:a,connectionTimeoutMillis:ACQUIRE_TIMEOUT_MS}),e}return n(s,"ensurePool"),{query:n(async(c,d)=>(await(await s()).query(c,d)).rows,"query"),transaction:n(async c=>{const p=await(await s()).connect();let w=!0,y=!1;const m={query:n(async(i,u)=>{if(y)throw new Error("This transaction has already ended; its `tx` cannot be used afterwards.");return(await p.query(i,u)).rows},"query")};try{await p.query("BEGIN"),await p.query(`SET LOCAL idle_in_transaction_session_timeout = ${IDLE_IN_TRANSACTION_TIMEOUT_MS}`);const i=await c(m);return await p.query("COMMIT"),i}catch(i){try{await p.query("ROLLBACK")}catch{w=!1}throw i}finally{y=!0,p.release(w?void 0:!0)}},"transaction")}}n(createScopedDb,"createScopedDb");function toWebRequest(a,e){const s=`http://localhost${a.url??"/"}`,c=new Headers;for(const[p,w]of Object.entries(a.headers))w!==void 0&&c.set(p,Array.isArray(w)?w.join(", "):w);const d=a.method??"GET";return new Request(s,{method:d,headers:c,...d==="GET"||d==="HEAD"?{}:{body:e}})}n(toWebRequest,"toWebRequest");async function writeWebResponse(a,e){const s={};e.headers.forEach((c,d)=>{s[d]=c}),a.writeHead(e.status,s),a.end(Buffer.from(await e.arrayBuffer()))}n(writeWebResponse,"writeWebResponse");function readBody(a,e){return new Promise((s,c)=>{const d=[];let p=0;a.on("data",w=>{if(p+=w.length,e!==void 0&&p>e){c(new Error("Policy request body too large")),a.destroy();return}d.push(w)}),a.on("end",()=>{s(toArrayBuffer(Buffer.concat(d)))}),a.on("error",c)})}n(readBody,"readBody");function serveBackend(a){const e=readEnv(process.env),s={pluginId:e.pluginId,tenantId:e.tenantId,db:createScopedDb(e.databaseUrl),...createHostClient(e)},c=http.createServer((i,u)=>{(async()=>{try{const T=(i.url??"/").split("?")[0];if(T===e.healthPath){u.writeHead(200,{"content-type":"application/json"}),u.end('{"status":"ok"}');return}if(i.headers["x-tego-plugin-storage-policy"]==="1"){const I=await readBody(i,16384),v=n(f=>typeof i.headers[f]=="string"?i.headers[f]:"","header"),P={installId:v("x-tego-plugin-install-id"),tenantId:v("x-tego-plugin-tenant-id"),issuedAt:v("x-tego-plugin-issued-at"),bodyHash:v("x-tego-plugin-body-hash")};if(T!==chunkIMAVYQ4U_cjs.PLUGIN_STORAGE_POLICY_PATH||i.method!=="POST"||!P.installId||!P.tenantId||!chunkIMAVYQ4U_cjs.verifyPluginStoragePolicy({context:P,signature:v("x-tego-plugin-signature"),key:e.gatewayKey,nowMs:Date.now(),body:Buffer.from(I).toString("utf8")})){u.writeHead(401),u.end();return}if(!a.authorizeStorage){u.writeHead(404),u.end();return}await writeWebResponse(u,await a.authorizeStorage(toWebRequest(i,I),P,s));return}if(i.headers[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.public]==="1"){const I=readPublicContext(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!I){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}if(!a.fetchPublic){u.writeHead(404,{"content-type":"application/json"}),u.end('{"error":"not found"}');return}const v=await readBody(i);await writeWebResponse(u,await a.fetchPublic(toWebRequest(i,v),I,s));return}const E=readUserContext(i.headers,{key:e.gatewayKey,nowMs:Date.now()});if(!E){u.writeHead(401,{"content-type":"application/json"}),u.end('{"error":"missing or unverifiable tego plugin context"}');return}const h=await readBody(i),A=await a.fetch(toWebRequest(i,h),E,s);await writeWebResponse(u,A)}catch(T){s.log.error("Unhandled error in plugin backend",{error:T instanceof Error?T.message:String(T)}),u.headersSent?u.end():(u.writeHead(500,{"content-type":"application/json"}),u.end('{"error":"plugin backend failed"}'))}})()});let d=!1,p=!1,w;const y=n(()=>(w??=Promise.resolve().then(()=>a.stop?.()),w),"stopModule"),m=(async()=>{const i=await startUp(s,e);if(i)throw new Error(i);d||(p=!0,await a.start?.(s),!d&&c.listen(e.port,()=>{s.log.info(`Plugin backend listening on ${e.port}`)}))})();m.catch(async i=>{console.error("Plugin backend startup failed",i);try{p&&await y()}catch(u){console.error("Plugin backend shutdown failed",u)}process.exit(1)}),process.on("SIGTERM",()=>{d||(d=!0,Promise.all([new Promise(i=>c.close(()=>i())),m.then(async()=>{p&&await y()})]).then(()=>process.exit(0)).catch(i=>{console.error("Plugin backend shutdown failed",i),process.exit(1)}))})}n(serveBackend,"serveBackend");async function startUp(a,e){const s=await checkDatabaseReachable(a.db,e.databaseUrl);if(s)return s;try{await a.config.refresh()}catch(c){return`Plugin backend cannot read its configuration from the host API: ${c instanceof Error?c.message:String(c)}`}return null}n(startUp,"startUp");async function checkDatabaseReachable(a,e){try{return await a.query("select 1"),null}catch(s){return`Plugin backend cannot reach its database at ${describeDatabaseTarget(e)}: ${s instanceof Error?s.message:String(s)}`}}n(checkDatabaseReachable,"checkDatabaseReachable");function describeDatabaseTarget(a){try{const e=new URL(a);return`${e.host}${e.pathname}`}catch{return"the address in DATABASE_URL"}}n(describeDatabaseTarget,"describeDatabaseTarget"),exports.checkDatabaseReachable=checkDatabaseReachable,exports.createScopedDb=createScopedDb,exports.readPublicContext=readPublicContext,exports.readUserContext=readUserContext,exports.serveBackend=serveBackend;
@@ -0,0 +1,63 @@
1
+ import { PluginUserContext } from './frontend.cjs';
2
+ import { PluginScopedDb, PluginPublicContext, PluginBackendModule } from './server.cjs';
3
+ import 'zod';
4
+
5
+ /**
6
+ * Runs a backend part: the bridge between the container it lives in and the
7
+ * `fetch(request, ctx, host)` module the plugin actually wrote.
8
+ *
9
+ * The runner image starts `backend/index.js` as an ordinary Node process, so
10
+ * something has to open a socket, work out who is calling, and assemble the
11
+ * host capabilities. That is this file, and it ships in the SDK rather than in
12
+ * the image on purpose: it is versioned with the plugin's own `hostApiRange`,
13
+ * so a plugin built against one host API keeps the bridge that matches it and
14
+ * the image stays a dumb Node runtime.
15
+ *
16
+ * Imported only by a plugin's backend entry, never by the platform — the
17
+ * `node:` imports here would not resolve in a browser build.
18
+ */
19
+
20
+ /**
21
+ * The caller, or null when the headers are absent, malformed or unsigned.
22
+ *
23
+ * Absent means the request did not come through the gateway, which is the only
24
+ * way in — so it is refused rather than defaulted to an anonymous context that
25
+ * a plugin might mistake for a real user.
26
+ *
27
+ * The signature is what makes the rest of it worth reading. Everything here
28
+ * arrives as a plain header, so without checking it the backend's idea of who
29
+ * is calling is whatever the socket said, and the gateway's permission checks
30
+ * protect only the path through the gateway.
31
+ */
32
+ declare function readUserContext(headers: Record<string, string | string[] | undefined>, verification: {
33
+ key: string;
34
+ nowMs: number;
35
+ }): PluginUserContext | null;
36
+ /** The public context, or null when any field is absent or the signature fails. */
37
+ declare function readPublicContext(headers: Record<string, string | string[] | undefined>, verification: {
38
+ key: string;
39
+ nowMs: number;
40
+ }): PluginPublicContext | null;
41
+ /** Exported for its tests; `serveBackend` is the only caller in production. */
42
+ declare function createScopedDb(databaseUrl: string): PluginScopedDb;
43
+ /**
44
+ * Serves a backend module until the container stops.
45
+ *
46
+ * A plugin's `backend/index.js` is expected to be one call to this:
47
+ *
48
+ * ```js
49
+ * import { serveBackend } from '@tego/plugin-sdk/server-runtime';
50
+ * import backend from './app.js';
51
+ * serveBackend(backend);
52
+ * ```
53
+ */
54
+ declare function serveBackend(module: PluginBackendModule): void;
55
+ /**
56
+ * The message to fail with, or null when the database answered.
57
+ *
58
+ * Returns the message instead of exiting so the decision to end the process
59
+ * stays at the one call site that owns the process.
60
+ */
61
+ declare function checkDatabaseReachable(db: Pick<PluginScopedDb, 'query'>, databaseUrl: string): Promise<string | null>;
62
+
63
+ export { checkDatabaseReachable, createScopedDb, readPublicContext, readUserContext, serveBackend };