create-spatius-app 0.1.3-beta.1 → 0.1.3

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 (45) hide show
  1. package/README.md +12 -10
  2. package/dist/cli.js +199 -207
  3. package/dist/templates.js +183 -199
  4. package/package.json +3 -3
  5. package/templates/cloudflare-livekit/AGENTS.md +2 -0
  6. package/templates/cloudflare-livekit/README.md +10 -0
  7. package/templates/cloudflare-livekit/agent/Dockerfile.pip +8 -0
  8. package/templates/cloudflare-livekit/agent/Dockerfile.uv +8 -0
  9. package/templates/cloudflare-livekit/agent/README.md +16 -1
  10. package/templates/cloudflare-livekit/agent/pyproject.toml +1 -1
  11. package/templates/cloudflare-livekit/agent/src/agent.py +2 -1
  12. package/templates/cloudflare-livekit/agent/tests/test_agent_startup.py +49 -0
  13. package/templates/cloudflare-livekit/agent/uv.lock +14 -14
  14. package/templates/cloudflare-livekit/gitignore +1 -0
  15. package/templates/cloudflare-livekit/package-lock.json +511 -411
  16. package/templates/cloudflare-livekit/package.json +17 -16
  17. package/templates/cloudflare-livekit/pnpm-lock.yaml +572 -480
  18. package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +7 -2
  19. package/templates/cloudflare-livekit/tests/dev-supervision.test.mjs +82 -20
  20. package/templates/layers/agora/AGENTS.md +9 -4
  21. package/templates/layers/agora/README.md +14 -8
  22. package/templates/layers/agora/package-lock.json +6307 -4129
  23. package/templates/layers/agora/package.json +24 -18
  24. package/templates/layers/agora/pnpm-lock.yaml +1765 -563
  25. package/templates/layers/agora/pnpm-workspace.yaml +8 -0
  26. package/templates/layers/agora/scripts/check-worker.mjs +98 -0
  27. package/templates/layers/agora/tsconfig.worker.json +23 -0
  28. package/templates/layers/agora/worker/agora.test.ts +2 -0
  29. package/templates/layers/agora/worker/agora.ts +1 -10
  30. package/templates/layers/agora/worker/index.test.ts +66 -0
  31. package/templates/layers/agora/worker/index.ts +4 -0
  32. package/templates/layers/agora/wrangler.jsonc +24 -0
  33. package/templates/cloudflare-livekit/worker-configuration.d.ts +0 -17184
  34. package/templates/layers/agora/tsconfig.json +0 -16
  35. package/templates/layers/node/Dockerfile +0 -12
  36. package/templates/layers/node/dockerignore +0 -10
  37. package/templates/layers/node/package-lock.json +0 -5569
  38. package/templates/layers/node/pnpm-lock.yaml +0 -3470
  39. package/templates/layers/node/railway.json +0 -8
  40. package/templates/layers/node/scripts/dev-service.mjs +0 -14
  41. package/templates/layers/node/scripts/dev-web.mjs +0 -22
  42. package/templates/layers/node/server/index.ts +0 -92
  43. package/templates/layers/node/vite.config.ts +0 -17
  44. package/templates/layers/railway-agent/agent/railway.json +0 -5
  45. /package/templates/layers/agora/{env.example → dev.vars.example} +0 -0
@@ -0,0 +1,8 @@
1
+ allowBuilds:
2
+ core-js: true
3
+ esbuild: true
4
+ workerd: true
5
+ autoInstallPeers: false
6
+ preferFrozenLockfile: true
7
+ saveExact: true
8
+ strictPeerDependencies: true
@@ -0,0 +1,98 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { Miniflare } from 'miniflare';
5
+
6
+ // Run the production bundle in workerd, with all provider traffic mocked.
7
+ const root = resolve(import.meta.dirname, '..');
8
+ const deploymentPath = resolve(root, '.wrangler/deploy/config.json');
9
+ const deployment = JSON.parse(await readFile(deploymentPath, 'utf8'));
10
+ const configPath = resolve(dirname(deploymentPath), deployment.configPath);
11
+ const config = JSON.parse(await readFile(configPath, 'utf8'));
12
+ const bindings = {
13
+ ...config.vars,
14
+ AGORA_APP_ID: 'a'.repeat(32),
15
+ AGORA_APP_CERTIFICATE: 'b'.repeat(32),
16
+ AGORA_PIPELINE_ID: 'test-pipeline',
17
+ SPATIUS_APP_ID: 'test-app',
18
+ SPATIUS_API_KEY: 'private-test-key',
19
+ SPATIUS_AVATAR_ID: 'test-avatar',
20
+ };
21
+ const calls = [];
22
+ const outboundService = async (request) => {
23
+ const url = new URL(request.url);
24
+ assert.equal(url.origin, 'https://api.agora.io');
25
+ assert.equal(request.method, 'POST');
26
+ assert.match(request.headers.get('authorization'), /^agora token=".+"$/);
27
+ calls.push(url.pathname);
28
+ if (url.pathname.endsWith('/join')) {
29
+ const body = await request.json();
30
+ assert.equal(body.pipeline_id, bindings.AGORA_PIPELINE_ID);
31
+ assert.equal(
32
+ body.properties.avatar.params.spatius_api_key,
33
+ bindings.SPATIUS_API_KEY,
34
+ );
35
+ return Response.json({ agent_id: 'test-agent' });
36
+ }
37
+ assert.ok(url.pathname.endsWith('/agents/test-agent/leave'));
38
+ return new Response(null, { status: 404 });
39
+ };
40
+ const worker = new Miniflare({
41
+ workers: [
42
+ {
43
+ config: {
44
+ name: config.name,
45
+ type: 'worker',
46
+ compatibilityDate: config.compatibility_date,
47
+ compatibilityFlags: config.compatibility_flags,
48
+ manifest: {
49
+ mainModule: config.main,
50
+ modulesRoot: dirname(configPath),
51
+ modules: {
52
+ [config.main]: {
53
+ type: 'esm',
54
+ contents: await readFile(
55
+ resolve(dirname(configPath), config.main),
56
+ 'utf8',
57
+ ),
58
+ },
59
+ },
60
+ },
61
+ env: Object.fromEntries(
62
+ Object.entries(bindings).map(([name, value]) => [
63
+ name,
64
+ { type: 'text', value },
65
+ ]),
66
+ ),
67
+ },
68
+ dev: { outboundService: { type: 'fetcher', handler: outboundService } },
69
+ },
70
+ ],
71
+ });
72
+ try {
73
+ const health = await worker.dispatchFetch('https://app.example/api/health');
74
+ assert.deepEqual(await health.json(), { ok: true });
75
+ const response = await worker.dispatchFetch(
76
+ 'https://app.example/api/session',
77
+ { method: 'POST' },
78
+ );
79
+ assert.equal(response.status, 201, await response.clone().text());
80
+ const session = await response.json();
81
+ assert.equal(typeof session.token, 'string');
82
+ assert.notEqual(session.uid, session.agentUid);
83
+ assert.ok(!JSON.stringify(session).includes(bindings.SPATIUS_API_KEY));
84
+ assert.ok(!JSON.stringify(session).includes(bindings.AGORA_APP_CERTIFICATE));
85
+ const stop = (capability) =>
86
+ worker.dispatchFetch('https://app.example/api/session/stop', {
87
+ method: 'POST',
88
+ body: JSON.stringify({ capability }),
89
+ });
90
+ assert.equal((await stop('invalid')).status, 403);
91
+ assert.equal((await stop(session.capability)).status, 200);
92
+ assert.equal(calls.length, 2);
93
+ console.log(
94
+ 'Cloudflare Worker session startup and cleanup verified with mocked Agora.',
95
+ );
96
+ } finally {
97
+ await worker.dispose();
98
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "resolveJsonModule": true,
5
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
6
+ "target": "ES2023",
7
+ "lib": ["ES2023", "WebWorker"],
8
+ "module": "ESNext",
9
+ "moduleResolution": "Bundler",
10
+ "types": ["./worker-configuration.d.ts", "node"],
11
+ "skipLibCheck": true,
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noUncheckedSideEffectImports": true
21
+ },
22
+ "include": ["worker", "worker-configuration.d.ts"]
23
+ }
@@ -11,6 +11,8 @@ const env: AgoraEnvironment = {
11
11
  SPATIUS_API_KEY: 'private-key',
12
12
  SPATIUS_APP_ID: 'test-app',
13
13
  SPATIUS_AVATAR_ID: 'test-avatar',
14
+ SPATIUS_REGION: 'cn-beijing',
15
+ AGORA_AVATAR_SAMPLE_RATE: '24000',
14
16
  };
15
17
  describe('hosted Agora sessions', () => {
16
18
  it('keeps secrets server-side and authorizes idempotent cleanup', async () => {
@@ -2,16 +2,7 @@ import { randomInt, randomUUID } from 'node:crypto';
2
2
  import agoraToken from 'agora-token';
3
3
  const { RtcTokenBuilder, RtcRole } = agoraToken;
4
4
  import { signCapability, verifyCapability } from './capability.js';
5
- export interface AgoraEnvironment {
6
- AGORA_APP_ID: string;
7
- AGORA_APP_CERTIFICATE: string;
8
- AGORA_PIPELINE_ID: string;
9
- SPATIUS_APP_ID: string;
10
- SPATIUS_API_KEY: string;
11
- SPATIUS_AVATAR_ID: string;
12
- SPATIUS_REGION?: string;
13
- AGORA_AVATAR_SAMPLE_RATE?: string;
14
- }
5
+ export type AgoraEnvironment = CloudflareBindings;
15
6
  export class ConfigurationError extends Error {}
16
7
  function configuration(env: AgoraEnvironment) {
17
8
  for (const key of [
@@ -0,0 +1,66 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import worker from './index.js';
3
+
4
+ const env: CloudflareBindings = {
5
+ AGORA_APP_ID: 'a'.repeat(32),
6
+ AGORA_APP_CERTIFICATE: 'b'.repeat(32),
7
+ AGORA_PIPELINE_ID: 'test-pipeline',
8
+ SPATIUS_API_KEY: 'private-key',
9
+ SPATIUS_APP_ID: 'test-app',
10
+ SPATIUS_AVATAR_ID: 'test-avatar',
11
+ SPATIUS_REGION: 'cn-beijing',
12
+ AGORA_AVATAR_SAMPLE_RATE: '24000',
13
+ };
14
+ const request = (path: string, method = 'GET', body?: object) =>
15
+ new Request(`https://app.example${path}`, {
16
+ method,
17
+ ...(body ? { body: JSON.stringify(body) } : {}),
18
+ });
19
+ afterEach(() => vi.unstubAllGlobals());
20
+ describe('Cloudflare Agora API', () => {
21
+ it('serves health and rejects unknown routes and unsupported methods', async () => {
22
+ expect(
23
+ await (await worker.fetch(request('/api/health'), env)).json(),
24
+ ).toEqual({ ok: true });
25
+ expect((await worker.fetch(request('/api/missing'), env)).status).toBe(404);
26
+ expect((await worker.fetch(request('/api/session'), env)).status).toBe(405);
27
+ expect(
28
+ (await worker.fetch(request('/api/health', 'POST'), env)).status,
29
+ ).toBe(405);
30
+ });
31
+ it('uses Worker bindings to create and stop hosted sessions without leaking secrets', async () => {
32
+ const fetcher = vi
33
+ .fn<typeof fetch>()
34
+ .mockResolvedValueOnce(Response.json({ agent_id: 'hosted-agent' }))
35
+ .mockResolvedValueOnce(new Response(null, { status: 404 }));
36
+ vi.stubGlobal('fetch', fetcher);
37
+ const response = await worker.fetch(request('/api/session', 'POST'), env);
38
+ expect(response.status).toBe(201);
39
+ expect(response.headers.get('cache-control')).toBe('no-store');
40
+ const session = await response.json<{ capability: string }>();
41
+ expect(JSON.stringify(session)).not.toContain(env.AGORA_APP_CERTIFICATE);
42
+ expect(JSON.stringify(session)).not.toContain(env.SPATIUS_API_KEY);
43
+ expect(
44
+ (await worker.fetch(request('/api/session/stop', 'POST', session), env))
45
+ .status,
46
+ ).toBe(200);
47
+ expect(fetcher).toHaveBeenCalledTimes(2);
48
+ });
49
+ it('fails missing configuration and invalid stop authorization before contacting Agora', async () => {
50
+ const fetcher = vi.fn<typeof fetch>();
51
+ vi.stubGlobal('fetch', fetcher);
52
+ expect(
53
+ (
54
+ await worker.fetch(request('/api/session', 'POST'), {
55
+ ...env,
56
+ AGORA_APP_CERTIFICATE: '',
57
+ })
58
+ ).status,
59
+ ).toBe(503);
60
+ expect(
61
+ (await worker.fetch(request('/api/session/stop', 'POST', {}), env))
62
+ .status,
63
+ ).toBe(403);
64
+ expect(fetcher).not.toHaveBeenCalled();
65
+ });
66
+ });
@@ -48,3 +48,7 @@ export async function handleRequest(
48
48
  );
49
49
  }
50
50
  }
51
+
52
+ export default {
53
+ fetch: handleRequest,
54
+ } satisfies ExportedHandler<CloudflareBindings>;
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "./node_modules/wrangler/config-schema.json",
3
+ "name": "spatius-agora-app",
4
+ "main": "./worker/index.ts",
5
+ "compatibility_date": "2026-09-11",
6
+ "compatibility_flags": ["nodejs_compat"],
7
+ "vars": {
8
+ "AGORA_APP_ID": "your-agora-app-id",
9
+ "AGORA_PIPELINE_ID": "your-published-pipeline-id",
10
+ "SPATIUS_APP_ID": "your-spatius-app-id",
11
+ "SPATIUS_AVATAR_ID": "your-spatius-avatar-id",
12
+ "SPATIUS_REGION": "cn-beijing",
13
+ "AGORA_AVATAR_SAMPLE_RATE": "24000",
14
+ },
15
+ "secrets": {
16
+ "required": ["AGORA_APP_CERTIFICATE", "SPATIUS_API_KEY"],
17
+ },
18
+ "assets": {
19
+ "not_found_handling": "single-page-application",
20
+ "run_worker_first": ["/api/*"],
21
+ },
22
+ "observability": { "enabled": true, "head_sampling_rate": 1 },
23
+ "upload_source_maps": true,
24
+ }