@audiodn/agent-kit 0.1.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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/assets/content/instructions.md +48 -0
- package/assets/content/partials/auth.md +17 -0
- package/assets/content/partials/compatibility.md +10 -0
- package/assets/content/partials/playback.md +14 -0
- package/assets/content/partials/processing.md +17 -0
- package/assets/content/partials/security.md +14 -0
- package/assets/content/partials/upload.md +17 -0
- package/assets/content/partials/webhooks.md +13 -0
- package/assets/skill/SKILL.md +52 -0
- package/assets/skill/references/authentication.md +27 -0
- package/assets/skill/references/playback.md +29 -0
- package/assets/skill/references/processing.md +23 -0
- package/assets/skill/references/security.md +30 -0
- package/assets/skill/references/upload-flow.md +29 -0
- package/assets/skill/references/webhooks.md +19 -0
- package/assets/skill/scripts/known-endpoints.json +28 -0
- package/assets/skill/scripts/validate.mjs +287 -0
- package/assets/skill/templates/cloudflare-worker.md +39 -0
- package/assets/skill/templates/nextjs.md +47 -0
- package/assets/skill/templates/node-server.md +44 -0
- package/assets/skill/templates/vue-nuxt.md +49 -0
- package/assets/snapshots/llms-full.txt +347 -0
- package/assets/snapshots/openapi.json +1416 -0
- package/assets/snapshots/sources.json +19 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +101 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +35 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +28 -0
- package/dist/commands/uninstall.d.ts +6 -0
- package/dist/commands/uninstall.js +20 -0
- package/dist/commands/validate.d.ts +5 -0
- package/dist/commands/validate.js +18 -0
- package/dist/core/formats.d.ts +23 -0
- package/dist/core/formats.js +43 -0
- package/dist/core/fsutil.d.ts +5 -0
- package/dist/core/fsutil.js +34 -0
- package/dist/core/install.d.ts +16 -0
- package/dist/core/install.js +76 -0
- package/dist/core/markers.d.ts +5 -0
- package/dist/core/markers.js +13 -0
- package/dist/core/merge.d.ts +21 -0
- package/dist/core/merge.js +38 -0
- package/dist/core/prompt.d.ts +2 -0
- package/dist/core/prompt.js +23 -0
- package/dist/core/render.d.ts +5 -0
- package/dist/core/render.js +21 -0
- package/dist/core/report.d.ts +10 -0
- package/dist/core/report.js +35 -0
- package/dist/core/skill.d.ts +13 -0
- package/dist/core/skill.js +117 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +10 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +12 -0
- package/package.json +61 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AudioDN project validator — portable, dependency-free (Node 18+).
|
|
3
|
+
//
|
|
4
|
+
// Detects common AudioDN integration mistakes. Runnable standalone:
|
|
5
|
+
// node .agents/skills/audiodn/scripts/validate.mjs [path]
|
|
6
|
+
// or via the kit:
|
|
7
|
+
// npx @audiodn/agent-kit validate [path]
|
|
8
|
+
//
|
|
9
|
+
// Exit code is non-zero when any error-severity finding is present.
|
|
10
|
+
import { readFileSync, readdirSync, statSync, existsSync, realpathSync } from 'node:fs';
|
|
11
|
+
import { dirname, join, relative, extname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const API_HOST = 'api.audiodelivery.net';
|
|
16
|
+
const MARKETING_HOST = 'audiodeliverynetwork.com';
|
|
17
|
+
|
|
18
|
+
const SCAN_EXT = new Set([
|
|
19
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte', '.astro',
|
|
20
|
+
'.py', '.go', '.rb', '.java', '.kt', '.php', '.swift', '.dart', '.cs',
|
|
21
|
+
'.json', '.env', '.yml', '.yaml',
|
|
22
|
+
]);
|
|
23
|
+
const SKIP_DIRS = new Set([
|
|
24
|
+
'node_modules', '.git', 'dist', 'build', '.next', 'out', 'coverage',
|
|
25
|
+
'.turbo', '.wrangler', 'vendor', '.venv', '__pycache__',
|
|
26
|
+
// Agent tooling / guidance, not application code under test. Skipping avoids
|
|
27
|
+
// scanning this validator's own source (which contains the very patterns it detects).
|
|
28
|
+
'.agents',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// ---- endpoint truth -------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function loadKnownEndpoints() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(join(HERE, 'known-endpoints.json'), 'utf8');
|
|
36
|
+
return JSON.parse(raw).paths || [];
|
|
37
|
+
} catch {
|
|
38
|
+
return ['/v1/upload_session', '/v1/upload/{upload_session_id}/track', '/v1/track/{track_id}'];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizePath(p) {
|
|
43
|
+
const clean = p.split('?')[0].split('#')[0].replace(/\/+$/, '');
|
|
44
|
+
return clean
|
|
45
|
+
.split('/')
|
|
46
|
+
.map((seg) => {
|
|
47
|
+
if (!seg) return seg;
|
|
48
|
+
if (/^\{.*\}$/.test(seg)) return '*'; // {param}
|
|
49
|
+
if (/^:/.test(seg)) return '*'; // :param
|
|
50
|
+
if (/\$\{.*\}/.test(seg)) return '*'; // ${expr}
|
|
51
|
+
if (/[$`+]/.test(seg)) return '*'; // concatenation / template fragment
|
|
52
|
+
if (/^[A-Z0-9_]+$/.test(seg) && seg.length >= 3) return '*'; // SESSION_ID
|
|
53
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}/.test(seg)) return '*'; // uuid
|
|
54
|
+
return seg;
|
|
55
|
+
})
|
|
56
|
+
.join('/');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---- config + ignores -----------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function loadConfig(root) {
|
|
62
|
+
const p = join(root, '.audiodn-validate.json');
|
|
63
|
+
if (!existsSync(p)) return { ignore: [], ignoreFiles: [] };
|
|
64
|
+
try {
|
|
65
|
+
const cfg = JSON.parse(readFileSync(p, 'utf8'));
|
|
66
|
+
return { ignore: cfg.ignore || [], ignoreFiles: cfg.ignoreFiles || [] };
|
|
67
|
+
} catch {
|
|
68
|
+
return { ignore: [], ignoreFiles: [] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- file collection ------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function collectFiles(root) {
|
|
75
|
+
const out = [];
|
|
76
|
+
const walk = (dir) => {
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = readdirSync(dir);
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const name of entries) {
|
|
84
|
+
const full = join(dir, name);
|
|
85
|
+
let st;
|
|
86
|
+
try {
|
|
87
|
+
st = statSync(full);
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (st.isDirectory()) {
|
|
92
|
+
if (!SKIP_DIRS.has(name)) walk(full);
|
|
93
|
+
} else if (SCAN_EXT.has(extname(name)) || name === '.env') {
|
|
94
|
+
if (st.size <= 2_000_000) out.push(full);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (statSync(root).isDirectory()) walk(root);
|
|
99
|
+
else out.push(root);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isClientFile(relPath, content) {
|
|
104
|
+
const p = relPath.toLowerCase();
|
|
105
|
+
if (/(^|\/)(public|static|assets|www)\//.test(p)) return true;
|
|
106
|
+
if (/(^|\/)(ios|android|mobile|app-native|flutter)\//.test(p)) return true;
|
|
107
|
+
if (/\.(vue|svelte|jsx|tsx)$/.test(p)) return true;
|
|
108
|
+
if (/['"]use client['"]/.test(content)) return true;
|
|
109
|
+
// Browser globals with no server markers.
|
|
110
|
+
if (/\b(window|document|localStorage)\b/.test(content) && !/\b(process\.env|import\s+.*from\s+['"]node:)/.test(content)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- rules ----------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const SECRET_LITERAL = /adn_[a-z]+_[A-Za-z0-9]{6,}/;
|
|
119
|
+
const BEARER_LITERAL = /Bearer\s+adn_[A-Za-z0-9_]{6,}/i;
|
|
120
|
+
const V1_URL = /\/v1\/[A-Za-z0-9_\-/{}:$.]+/g;
|
|
121
|
+
const HOST_V1 = /https?:\/\/([A-Za-z0-9.\-]+)\/v1\b/g;
|
|
122
|
+
|
|
123
|
+
const PLACEHOLDER = /(your|example|xxxx|placeholder|changeme|\.\.\.|<[^>]+>|env\.|process\.env|import\.meta\.env)/i;
|
|
124
|
+
|
|
125
|
+
function ruleFindings(file, relPath, content, known, config) {
|
|
126
|
+
const findings = [];
|
|
127
|
+
const lines = content.split('\n');
|
|
128
|
+
const clientFile = isClientFile(relPath, content);
|
|
129
|
+
const add = (rule, severity, lineIdx, message, remedy) => {
|
|
130
|
+
if (config.ignore.includes(rule)) return;
|
|
131
|
+
const raw = lineIdx >= 0 ? lines[lineIdx] : '';
|
|
132
|
+
if (/audiodn-validate-ignore/.test(raw)) return;
|
|
133
|
+
findings.push({ rule, severity, file: relPath, line: lineIdx + 1, message, remedy });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
lines.forEach((line, i) => {
|
|
137
|
+
// 6. hardcoded-secret
|
|
138
|
+
if ((SECRET_LITERAL.test(line) || BEARER_LITERAL.test(line)) && !PLACEHOLDER.test(line)) {
|
|
139
|
+
add('hardcoded-secret', 'error', i, 'Hardcoded AudioDN key/secret literal.', 'Read the key from an environment variable instead.');
|
|
140
|
+
} else {
|
|
141
|
+
const assign = line.match(/\b(signing[_-]?secret|api[_-]?key|secret|token)\b\s*[:=]\s*['"]([A-Za-z0-9_\-]{16,})['"]/i);
|
|
142
|
+
if (assign && !PLACEHOLDER.test(line)) {
|
|
143
|
+
add('hardcoded-secret', 'error', i, 'Hardcoded secret assigned inline.', 'Move the secret to an environment variable.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 1. server-key-in-client
|
|
148
|
+
if (clientFile && (/ADN_API_KEY/.test(line) || BEARER_LITERAL.test(line) || SECRET_LITERAL.test(line) || /Authorization\s*:\s*[`'"]?Bearer/i.test(line))) {
|
|
149
|
+
add('server-key-in-client', 'error', i, 'Server API credential referenced in client-side code.', 'Move server API calls behind your backend; the browser must never hold a Bearer/API Access key.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 7. incorrect-api-origin
|
|
153
|
+
let hm;
|
|
154
|
+
HOST_V1.lastIndex = 0;
|
|
155
|
+
while ((hm = HOST_V1.exec(line)) !== null) {
|
|
156
|
+
const host = hm[1];
|
|
157
|
+
if (host === MARKETING_HOST) {
|
|
158
|
+
add('incorrect-api-origin', 'error', i, `Marketing host used as API origin (${host}/v1).`, `Use https://${API_HOST}/v1/ for API requests.`);
|
|
159
|
+
} else if (host !== API_HOST) {
|
|
160
|
+
add('incorrect-api-origin', 'error', i, `Unexpected API origin "${host}" for a /v1 request.`, `AudioDN's API host is ${API_HOST}.`);
|
|
161
|
+
} else if (/^http:\/\//.test(hm[0])) {
|
|
162
|
+
add('incorrect-api-origin', 'error', i, 'Insecure http:// used for the API.', 'Use https://.');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 2. invented-endpoint
|
|
167
|
+
let m;
|
|
168
|
+
V1_URL.lastIndex = 0;
|
|
169
|
+
while ((m = V1_URL.exec(line)) !== null) {
|
|
170
|
+
const raw = m[0].replace(/[.,'"`);]+$/, '');
|
|
171
|
+
const norm = normalizePath(raw);
|
|
172
|
+
if (!known.has(norm)) {
|
|
173
|
+
add('invented-endpoint', 'error', i, `Unknown AudioDN endpoint "${raw}".`, 'Verify the path against https://audiodeliverynetwork.com/openapi.json — do not invent endpoints.');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 4. upload-url-treated-permanent
|
|
178
|
+
if (/upload_url/.test(line) && /(localStorage|sessionStorage|INSERT\s+INTO|\.insert\(|\.save\(|\.update\(|prisma\.|db\.|writeFile|fs\.write|redis\.|cache\.set)/i.test(line)) {
|
|
179
|
+
add('upload-url-treated-permanent', 'warn', i, 'Upload URL appears to be persisted/cached.', 'The per-track upload_url is short-lived and single-use; do not store it. Create a fresh track if it expires.');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 8. obsolete-doc-url
|
|
183
|
+
const OBSOLETE = [
|
|
184
|
+
[/\/v1\/upload-session/, 'Use the underscore path /v1/upload_session.'],
|
|
185
|
+
[/\/v1\/play-session/, 'Use the underscore path /v1/play_session.'],
|
|
186
|
+
[/\bttl_seconds\b/, 'Use expires_in, not ttl_seconds.'],
|
|
187
|
+
[/session\.url\b/, 'Upload sessions do not return a URL; create a per-track upload URL (track_upload.upload_url).'],
|
|
188
|
+
[new RegExp(`${MARKETING_HOST.replace('.', '\\.')}/docs`), 'Docs live at https://audiodeliverynetwork.com/docs (marketing host), API at api.audiodelivery.net.'],
|
|
189
|
+
];
|
|
190
|
+
for (const [re, remedy] of OBSOLETE) {
|
|
191
|
+
if (re.test(line)) add('obsolete-doc-url', 'warn', i, 'Obsolete or incorrect AudioDN reference.', remedy);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return findings;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Project-level rules (need the whole codebase).
|
|
199
|
+
function projectFindings(scanned, config) {
|
|
200
|
+
const findings = [];
|
|
201
|
+
const anyText = scanned.map((s) => s.content).join('\n');
|
|
202
|
+
const add = (rule, severity, file, message, remedy) => {
|
|
203
|
+
if (config.ignore.includes(rule)) return;
|
|
204
|
+
findings.push({ rule, severity, file, line: 0, message, remedy });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// 3. missing-per-track-request
|
|
208
|
+
const createsSession = scanned.find((s) => /upload_session\b/.test(s.content) && /(POST|fetch|axios|\.post\(|method\s*:\s*['"]POST)/i.test(s.content));
|
|
209
|
+
const createsTrack = /upload\/[^'"`\s]*\/track|track_upload|\/track['"`]/.test(anyText);
|
|
210
|
+
if (createsSession && !createsTrack) {
|
|
211
|
+
add('missing-per-track-request', 'warn', createsSession.rel, 'Creates an upload session but never creates a per-track upload URL.', 'After POST /v1/upload_session, call POST /v1/upload/{id}/track per file to get track_upload.upload_url, then PUT the bytes.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 5. playback-before-ready
|
|
215
|
+
const doesPlayback = scanned.find((s) => /(play_session|<audio|\.play\(|audiodn-player|createPlaySession)/.test(s.content));
|
|
216
|
+
const checksReady = /(track_status_id|['"]ready['"]|waitForReady|files_completed_at|webhook|status_changed)/.test(anyText);
|
|
217
|
+
if (doesPlayback && !checksReady) {
|
|
218
|
+
add('playback-before-ready', 'warn', doesPlayback.rel, 'Playback is set up without any processing-readiness check.', 'Gate playback on readiness: poll GET /v1/track/{id} until track_status_id === "ready", or use the Track Processing webhook.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return findings;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---- public API -----------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
export function runValidation(root, _opts = {}) {
|
|
227
|
+
const known = new Set(loadKnownEndpoints().map(normalizePath));
|
|
228
|
+
const config = loadConfig(root);
|
|
229
|
+
const files = collectFiles(root).filter((f) => {
|
|
230
|
+
const rel = relative(root, f).split('\\').join('/');
|
|
231
|
+
return !config.ignoreFiles.some((pat) => rel.includes(pat));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const findings = [];
|
|
235
|
+
const scanned = [];
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
let content;
|
|
238
|
+
try {
|
|
239
|
+
content = readFileSync(file, 'utf8');
|
|
240
|
+
} catch {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const rel = relative(root, file).split('\\').join('/') || file;
|
|
244
|
+
scanned.push({ rel, content });
|
|
245
|
+
findings.push(...ruleFindings(file, rel, content, known, config));
|
|
246
|
+
}
|
|
247
|
+
findings.push(...projectFindings(scanned, config));
|
|
248
|
+
|
|
249
|
+
const errorCount = findings.filter((f) => f.severity === 'error').length;
|
|
250
|
+
const warnCount = findings.filter((f) => f.severity === 'warn').length;
|
|
251
|
+
return { findings, errorCount, warnCount, filesScanned: scanned.length };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function printFindings(result) {
|
|
255
|
+
if (result.findings.length === 0) {
|
|
256
|
+
console.log(`audiodn-validate: no issues found across ${result.filesScanned} file(s).`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const f of result.findings) {
|
|
260
|
+
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
261
|
+
console.log(`${f.severity.toUpperCase()} ${loc} [${f.rule}]`);
|
|
262
|
+
console.log(` ${f.message}`);
|
|
263
|
+
if (f.remedy) console.log(` -> ${f.remedy}`);
|
|
264
|
+
}
|
|
265
|
+
console.log(`\naudiodn-validate: ${result.errorCount} error(s), ${result.warnCount} warning(s) across ${result.filesScanned} file(s).`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// CLI entry when run directly (symlink-robust — /tmp is a symlink on macOS).
|
|
269
|
+
function invokedDirectly() {
|
|
270
|
+
try {
|
|
271
|
+
return (
|
|
272
|
+
Boolean(process.argv[1]) &&
|
|
273
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
274
|
+
);
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (invokedDirectly()) {
|
|
281
|
+
const target = process.argv[2] || '.';
|
|
282
|
+
const json = process.argv.includes('--json');
|
|
283
|
+
const result = runValidation(target, {});
|
|
284
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
285
|
+
else printFindings(result);
|
|
286
|
+
process.exit(result.errorCount > 0 ? 1 : 0);
|
|
287
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Template: Cloudflare Worker signed playback
|
|
2
|
+
|
|
3
|
+
Sign delivery URLs at the edge. The signing secret lives in a Worker secret
|
|
4
|
+
(`wrangler secret put ADN_SIGNING_SECRET`) and never reaches the client.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
// src/index.mjs
|
|
8
|
+
function base64url(bytes) {
|
|
9
|
+
let s = '';
|
|
10
|
+
const b = new Uint8Array(bytes);
|
|
11
|
+
for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]);
|
|
12
|
+
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
13
|
+
}
|
|
14
|
+
async function signUrl(secret, domain, path) {
|
|
15
|
+
const u = new URL('https://' + domain + '/' + path);
|
|
16
|
+
u.searchParams.delete('verify');
|
|
17
|
+
const issued = Math.floor(Date.now() / 1000).toString();
|
|
18
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret),
|
|
19
|
+
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
20
|
+
const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key,
|
|
21
|
+
new TextEncoder().encode(u.pathname + (u.search || '') + issued)));
|
|
22
|
+
u.searchParams.append('verify', issued + '-' + base64url(mac)); // verify LAST
|
|
23
|
+
return u.toString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
async fetch(request, env) {
|
|
28
|
+
const url = new URL(request.url);
|
|
29
|
+
const path = url.searchParams.get('path');
|
|
30
|
+
if (!path) return new Response('path required', { status: 400 });
|
|
31
|
+
// TODO: check entitlement here before signing.
|
|
32
|
+
const signed = await signUrl(env.ADN_SIGNING_SECRET, env.ADN_DELIVERY_DOMAIN, path);
|
|
33
|
+
return Response.json({ url: signed });
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Secret in `wrangler.toml`? No — only public `[vars]` (e.g. `ADN_DELIVERY_DOMAIN`).
|
|
39
|
+
See the runnable example `examples/cloudflare-worker-signed-playback`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Template: Next.js (App Router) secure upload
|
|
2
|
+
|
|
3
|
+
Server route mints the session (Bearer stays server-side); the browser creates
|
|
4
|
+
the per-track URL, PUTs bytes, and polls a server proxy for readiness.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// app/api/upload-session/route.ts (SERVER)
|
|
8
|
+
export const runtime = 'nodejs';
|
|
9
|
+
export async function POST() {
|
|
10
|
+
const res = await fetch('https://api.audiodelivery.net/v1/upload_session', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}`, 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify({ collection_id: process.env.ADN_COLLECTION_ID }),
|
|
14
|
+
});
|
|
15
|
+
const s = await res.json();
|
|
16
|
+
return Response.json({ upload_session_id: s.upload_session_id }); // only the id
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// app/api/track-status/route.ts (SERVER — GET /v1/track needs the key)
|
|
22
|
+
export async function GET(req: Request) {
|
|
23
|
+
const id = new URL(req.url).searchParams.get('trackId');
|
|
24
|
+
const res = await fetch(`https://api.audiodelivery.net/v1/track/${id}`, {
|
|
25
|
+
headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}` },
|
|
26
|
+
});
|
|
27
|
+
const b = await res.json();
|
|
28
|
+
return Response.json({ status: b.track?.track_status_id ?? null });
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// client component ('use client') — NO api key here
|
|
34
|
+
const { upload_session_id } = await fetch('/api/upload-session', { method: 'POST' }).then(r => r.json());
|
|
35
|
+
const { track_id, track_upload } = await fetch(
|
|
36
|
+
`https://api.audiodelivery.net/v1/upload/${upload_session_id}/track`,
|
|
37
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_name: file.name }) },
|
|
38
|
+
).then(r => r.json());
|
|
39
|
+
await fetch(track_upload.upload_url, { method: track_upload.method, body: file }); // PUT
|
|
40
|
+
let status;
|
|
41
|
+
do {
|
|
42
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
43
|
+
status = (await fetch(`/api/track-status?trackId=${track_id}`).then(r => r.json())).status;
|
|
44
|
+
} while (status && !['ready', 'incomplete', 'error'].includes(status));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
See the runnable example `examples/nextjs-secure-upload` in the AudioDN repo.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Template: Node server (ingest + play session)
|
|
2
|
+
|
|
3
|
+
Pure server-side ingest and playback provisioning. The Bearer key never leaves
|
|
4
|
+
this process.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
// ingest.mjs
|
|
8
|
+
const API = 'https://api.audiodelivery.net/v1';
|
|
9
|
+
const auth = { Authorization: `Bearer ${process.env.ADN_API_KEY}`, 'Content-Type': 'application/json' };
|
|
10
|
+
|
|
11
|
+
export async function ingest(bytes, fileName) {
|
|
12
|
+
// 1. session
|
|
13
|
+
const session = await fetch(`${API}/upload_session`, {
|
|
14
|
+
method: 'POST', headers: auth,
|
|
15
|
+
body: JSON.stringify({ collection_id: process.env.ADN_COLLECTION_ID }),
|
|
16
|
+
}).then(r => r.json());
|
|
17
|
+
|
|
18
|
+
// 2. per-track (no Bearer)
|
|
19
|
+
const { track_id, track_upload } = await fetch(`${API}/upload/${session.upload_session_id}/track`, {
|
|
20
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({ file_name: fileName }),
|
|
22
|
+
}).then(r => r.json());
|
|
23
|
+
|
|
24
|
+
// 3. PUT bytes
|
|
25
|
+
await fetch(track_upload.upload_url, { method: track_upload.method, body: bytes });
|
|
26
|
+
|
|
27
|
+
// 4. wait for readiness (prefer the Track Processing webhook in production)
|
|
28
|
+
let status;
|
|
29
|
+
do {
|
|
30
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
31
|
+
const t = await fetch(`${API}/track/${track_id}`, { headers: { Authorization: auth.Authorization } }).then(r => r.json());
|
|
32
|
+
status = t.track?.track_status_id;
|
|
33
|
+
} while (!['ready', 'incomplete', 'error'].includes(status));
|
|
34
|
+
|
|
35
|
+
return { track_id, status };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function playSession(track_id) {
|
|
39
|
+
return fetch(`${API}/play_session/track`, {
|
|
40
|
+
method: 'POST', headers: auth,
|
|
41
|
+
body: JSON.stringify({ track_id, variants: ['hq'], expires_in: 3600 }),
|
|
42
|
+
}).then(r => r.json());
|
|
43
|
+
}
|
|
44
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Template: Vue / Nuxt secure upload
|
|
2
|
+
|
|
3
|
+
Nuxt server routes hold the Bearer key; the Vue component does the per-track
|
|
4
|
+
create, the PUT, and polls the status proxy.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// server/api/upload-session.post.ts (SERVER)
|
|
8
|
+
export default defineEventHandler(async () => {
|
|
9
|
+
const res = await $fetch('https://api.audiodelivery.net/v1/upload_session', {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}`, 'Content-Type': 'application/json' },
|
|
12
|
+
body: { collection_id: process.env.ADN_COLLECTION_ID },
|
|
13
|
+
});
|
|
14
|
+
return { upload_session_id: res.upload_session_id };
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// server/api/track-status.get.ts (SERVER)
|
|
20
|
+
export default defineEventHandler(async (event) => {
|
|
21
|
+
const { trackId } = getQuery(event);
|
|
22
|
+
const res = await $fetch(`https://api.audiodelivery.net/v1/track/${trackId}`, {
|
|
23
|
+
headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}` },
|
|
24
|
+
});
|
|
25
|
+
return { status: res.track?.track_status_id };
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```vue
|
|
30
|
+
<!-- component (client) — NO api key -->
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
async function upload(file: File) {
|
|
33
|
+
const { upload_session_id } = await $fetch('/api/upload-session', { method: 'POST' });
|
|
34
|
+
const { track_id, track_upload } = await $fetch(
|
|
35
|
+
`https://api.audiodelivery.net/v1/upload/${upload_session_id}/track`,
|
|
36
|
+
{ method: 'POST', body: { file_name: file.name } },
|
|
37
|
+
);
|
|
38
|
+
await fetch(track_upload.upload_url, { method: track_upload.method, body: file });
|
|
39
|
+
let status: string | undefined;
|
|
40
|
+
do {
|
|
41
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
42
|
+
status = (await $fetch('/api/track-status', { query: { trackId: track_id } })).status;
|
|
43
|
+
} while (status && !['ready', 'incomplete', 'error'].includes(status));
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Register `<audiodn-*>` as custom elements in `nuxt.config.ts`.
|
|
49
|
+
See the AudioDN docs recipe `/docs/recipes/vue-secure-upload`.
|