@koderlabs/tasks-sdk-nextjs 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 +179 -0
- package/README.md +9 -0
- package/dist/chunk-2YRBUQGE.js +177 -0
- package/dist/chunk-2YRBUQGE.js.map +1 -0
- package/dist/client.cjs +38 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.js +16 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +214 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.cjs +212 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +44 -0
- package/dist/plugin.d.ts +44 -0
- package/dist/plugin.js +7 -0
- package/dist/plugin.js.map +1 -0
- package/dist/server.cjs +56 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +47 -0
- package/dist/server.d.ts +47 -0
- package/dist/server.js +31 -0
- package/dist/server.js.map +1 -0
- package/package.json +85 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
InstantTasksSourceMapsPlugin: () => InstantTasksSourceMapsPlugin,
|
|
34
|
+
withInstantTasks: () => withInstantTasks
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
|
|
38
|
+
// src/sourcemaps-upload.ts
|
|
39
|
+
var fs = __toESM(require("fs"), 1);
|
|
40
|
+
var path = __toESM(require("path"), 1);
|
|
41
|
+
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
42
|
+
var MAX_RETRIES = 2;
|
|
43
|
+
var RETRY_BASE_MS = 1e3;
|
|
44
|
+
var InstantTasksSourceMapsPlugin = class {
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
this.opts = {
|
|
47
|
+
distDir: ".next",
|
|
48
|
+
maxFileBytes: DEFAULT_MAX_BYTES,
|
|
49
|
+
strict: opts.strict ?? process.env.CI === "true",
|
|
50
|
+
...opts
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
apply(compiler) {
|
|
54
|
+
const isProduction = compiler.options?.mode === "production";
|
|
55
|
+
const isClientCompiler = !compiler.name || compiler.name === "client" || compiler.name === "browser";
|
|
56
|
+
if (!isProduction || !isClientCompiler) return;
|
|
57
|
+
compiler.hooks.afterEmit.tapAsync(
|
|
58
|
+
"InstantTasksSourceMapsPlugin",
|
|
59
|
+
async (compilation, callback) => {
|
|
60
|
+
try {
|
|
61
|
+
const failed = await this._uploadMaps(compilation.outputPath ?? this.opts.distDir);
|
|
62
|
+
if (failed.length > 0 && this.opts.strict) {
|
|
63
|
+
const summary = failed.map((f) => ` ${f.file}: ${f.status ? `HTTP ${f.status}` : f.error ?? "unknown"}`).join("\n");
|
|
64
|
+
callback(new Error(`[InstantTasks] ${failed.length} source-map upload(s) failed:
|
|
65
|
+
${summary}`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (this.opts.strict) {
|
|
70
|
+
callback(err);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.warn("[InstantTasks] source-map upload failed:", err);
|
|
74
|
+
}
|
|
75
|
+
callback();
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
async _uploadMaps(outputPath) {
|
|
80
|
+
const resolvedRoot = path.resolve(outputPath);
|
|
81
|
+
const mapFiles = this._findMapFiles(resolvedRoot);
|
|
82
|
+
if (mapFiles.length === 0) {
|
|
83
|
+
console.log("[InstantTasks] No .map files found to upload in", resolvedRoot);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
console.log(`[InstantTasks] Uploading ${mapFiles.length} source map(s)\u2026`);
|
|
87
|
+
const uploadUrl = `${this.opts.endpoint}/api/v1/sdk/source-maps`;
|
|
88
|
+
const results = await Promise.all(mapFiles.map((f) => this._uploadOne(uploadUrl, resolvedRoot, f)));
|
|
89
|
+
const failed = results.filter((r) => !r.ok);
|
|
90
|
+
const ok = results.length - failed.length;
|
|
91
|
+
console.log(`[InstantTasks] Source map upload complete \u2014 ${ok} ok, ${failed.length} failed.`);
|
|
92
|
+
return failed;
|
|
93
|
+
}
|
|
94
|
+
async _uploadOne(uploadUrl, root, filePath) {
|
|
95
|
+
const resolved = path.resolve(filePath);
|
|
96
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
97
|
+
return { file: filePath, ok: false, error: "path escapes dist root" };
|
|
98
|
+
}
|
|
99
|
+
const relativePath = path.relative(root, resolved);
|
|
100
|
+
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
101
|
+
return { file: filePath, ok: false, error: "invalid relative path" };
|
|
102
|
+
}
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = await fs.promises.stat(resolved);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { file: relativePath, ok: false, error: e.message };
|
|
108
|
+
}
|
|
109
|
+
if (stat.size > this.opts.maxFileBytes) {
|
|
110
|
+
console.warn(`[InstantTasks] Skipping ${relativePath} \u2014 ${stat.size} bytes exceeds cap ${this.opts.maxFileBytes}`);
|
|
111
|
+
return { file: relativePath, ok: false, error: `file too large (${stat.size} bytes)` };
|
|
112
|
+
}
|
|
113
|
+
let content;
|
|
114
|
+
try {
|
|
115
|
+
content = await fs.promises.readFile(resolved, "utf-8");
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return { file: relativePath, ok: false, error: e.message };
|
|
118
|
+
}
|
|
119
|
+
let lastErr;
|
|
120
|
+
let lastStatus;
|
|
121
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(uploadUrl, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
Authorization: `Bearer ${this.opts.secretKey}`
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ path: relativePath, content })
|
|
130
|
+
});
|
|
131
|
+
if (res.ok) return { file: relativePath, ok: true, status: res.status };
|
|
132
|
+
lastStatus = res.status;
|
|
133
|
+
if (res.status < 500 || res.status >= 600) break;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
lastErr = e.message;
|
|
136
|
+
}
|
|
137
|
+
if (attempt < MAX_RETRIES) {
|
|
138
|
+
await new Promise((r) => setTimeout(r, RETRY_BASE_MS * Math.pow(2, attempt)));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.warn(`[InstantTasks] Failed to upload ${relativePath}: ${lastStatus ? `HTTP ${lastStatus}` : lastErr ?? "unknown"}`);
|
|
142
|
+
return { file: relativePath, ok: false, status: lastStatus, error: lastErr };
|
|
143
|
+
}
|
|
144
|
+
_findMapFiles(dir) {
|
|
145
|
+
if (!fs.existsSync(dir)) return [];
|
|
146
|
+
const results = [];
|
|
147
|
+
const root = path.resolve(dir);
|
|
148
|
+
const walk = (current) => {
|
|
149
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
150
|
+
const full = path.join(current, entry.name);
|
|
151
|
+
if (entry.isSymbolicLink()) continue;
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
walk(full);
|
|
154
|
+
} else if (entry.isFile() && entry.name.endsWith(".map")) {
|
|
155
|
+
const resolved = path.resolve(full);
|
|
156
|
+
if (resolved.startsWith(root + path.sep) || resolved === root) results.push(full);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
walk(root);
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/plugin.ts
|
|
166
|
+
function withInstantTasks(opts = {}) {
|
|
167
|
+
return function applyInstantTasks(nextConfig) {
|
|
168
|
+
if (typeof nextConfig === "function") {
|
|
169
|
+
return async (phase, args) => {
|
|
170
|
+
const resolved = await nextConfig(phase, args);
|
|
171
|
+
return applyConfig(resolved, opts);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return applyConfig(nextConfig, opts);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function applyConfig(config, opts) {
|
|
178
|
+
for (const key of Object.keys(process.env)) {
|
|
179
|
+
if (/^NEXT_PUBLIC_.*INSTANTTASKS.*SECRET/i.test(key)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[InstantTasks] ${key} is exposed via NEXT_PUBLIC_* and will leak into the client bundle. Move the value to a non-NEXT_PUBLIC env (e.g. .env.local or CI secret) and read via INSTANTTASKS_SECRET_KEY.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const secretKey = opts.secretKey ?? process.env["INSTANTTASKS_SECRET_KEY"] ?? "";
|
|
186
|
+
const endpoint = opts.endpoint ?? "https://tasks.koderlabs.net";
|
|
187
|
+
const distDir = opts.distDir ?? config.distDir ?? ".next";
|
|
188
|
+
const merged = {
|
|
189
|
+
...config,
|
|
190
|
+
// Always emit browser source maps in production — required for meaningful
|
|
191
|
+
// stack traces in the Issues dashboard.
|
|
192
|
+
productionBrowserSourceMaps: true
|
|
193
|
+
};
|
|
194
|
+
const previousWebpack = config.webpack;
|
|
195
|
+
merged.webpack = (webpackConfig, context) => {
|
|
196
|
+
let finalConfig = previousWebpack ? previousWebpack(webpackConfig, context) : webpackConfig;
|
|
197
|
+
const { isServer, dev } = context;
|
|
198
|
+
const shouldUpload = !dev && !isServer && !opts.disableSourceMapUpload && Boolean(secretKey);
|
|
199
|
+
if (shouldUpload) {
|
|
200
|
+
finalConfig.plugins = [
|
|
201
|
+
...finalConfig.plugins ?? [],
|
|
202
|
+
new InstantTasksSourceMapsPlugin({ endpoint, secretKey, distDir })
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
return finalConfig;
|
|
206
|
+
};
|
|
207
|
+
return merged;
|
|
208
|
+
}
|
|
209
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
210
|
+
0 && (module.exports = {
|
|
211
|
+
InstantTasksSourceMapsPlugin,
|
|
212
|
+
withInstantTasks
|
|
213
|
+
});
|
|
214
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sourcemaps-upload.ts","../src/plugin.ts"],"sourcesContent":["// Main entry — re-exports the plugin for next.config.js usage.\n// For client-component hooks, import from '@koderlabs/tasks-sdk-nextjs/client'.\n// For server-side usage, import from '@koderlabs/tasks-sdk-nextjs/server'.\n\nexport { withInstantTasks } from './plugin';\nexport type { WithInstantTasksOptions } from './plugin';\n\nexport { InstantTasksSourceMapsPlugin } from './sourcemaps-upload';\nexport type { SourceMapsUploadOptions } from './sourcemaps-upload';\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Options for the source-maps upload webpack plugin.\n */\nexport interface SourceMapsUploadOptions {\n /** InstantTasks API base URL (e.g. \"https://tasks.koderlabs.net\") */\n endpoint: string;\n /** Project access key with upload permissions */\n secretKey: string;\n /** Directory to scan for .map files (defaults to \".next\") */\n distDir?: string;\n /**\n * When true (or `process.env.CI === 'true'`), exit the build with a non-zero\n * code if ANY map fails to upload. Default: opt-in via env so existing\n * pipelines don't break on rollout — set explicitly once stable.\n */\n strict?: boolean;\n /**\n * Skip files larger than this (bytes). Default 50MB — beyond which the upload\n * is almost certainly going to OOM or time out anyway.\n */\n maxFileBytes?: number;\n}\n\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_MS = 1_000;\n\ninterface UploadOutcome { file: string; ok: boolean; status?: number; error?: string }\n\n/**\n * Webpack plugin that uploads source maps to the InstantTasks SDK endpoint\n * after a **production** build completes.\n *\n * Failure handling: when `strict` is true (or `CI=true`), any failed upload\n * fails the build. Otherwise failures are warned but the build continues\n * — same as before, but with a much louder report (per-file + summary).\n *\n * Path safety: the relative path stored alongside the map is validated to\n * never contain `..` or be absolute — defends against a misconfigured\n * `outputPath` accidentally letting the plugin walk symlinked files outside\n * the dist directory and store them under attacker-influenced keys.\n */\nexport class InstantTasksSourceMapsPlugin {\n private readonly opts: Required<Omit<SourceMapsUploadOptions, 'strict'>> & { strict: boolean };\n\n constructor(opts: SourceMapsUploadOptions) {\n this.opts = {\n distDir: '.next',\n maxFileBytes: DEFAULT_MAX_BYTES,\n strict: opts.strict ?? process.env.CI === 'true',\n ...opts,\n };\n }\n\n apply(compiler: any) {\n // Skip in any environment other than a production client build.\n const isProduction = compiler.options?.mode === 'production';\n const isClientCompiler =\n !compiler.name || compiler.name === 'client' || compiler.name === 'browser';\n\n if (!isProduction || !isClientCompiler) return;\n\n compiler.hooks.afterEmit.tapAsync(\n 'InstantTasksSourceMapsPlugin',\n async (compilation: any, callback: (err?: Error) => void) => {\n try {\n const failed = await this._uploadMaps(compilation.outputPath ?? this.opts.distDir);\n if (failed.length > 0 && this.opts.strict) {\n const summary = failed\n .map((f) => ` ${f.file}: ${f.status ? `HTTP ${f.status}` : f.error ?? 'unknown'}`)\n .join('\\n');\n callback(new Error(`[InstantTasks] ${failed.length} source-map upload(s) failed:\\n${summary}`));\n return;\n }\n } catch (err) {\n if (this.opts.strict) {\n callback(err as Error);\n return;\n }\n console.warn('[InstantTasks] source-map upload failed:', err);\n }\n callback();\n },\n );\n }\n\n private async _uploadMaps(outputPath: string): Promise<UploadOutcome[]> {\n const resolvedRoot = path.resolve(outputPath);\n const mapFiles = this._findMapFiles(resolvedRoot);\n if (mapFiles.length === 0) {\n console.log('[InstantTasks] No .map files found to upload in', resolvedRoot);\n return [];\n }\n\n console.log(`[InstantTasks] Uploading ${mapFiles.length} source map(s)…`);\n\n const uploadUrl = `${this.opts.endpoint}/api/v1/sdk/source-maps`;\n const results = await Promise.all(mapFiles.map((f) => this._uploadOne(uploadUrl, resolvedRoot, f)));\n const failed = results.filter((r) => !r.ok);\n const ok = results.length - failed.length;\n console.log(`[InstantTasks] Source map upload complete — ${ok} ok, ${failed.length} failed.`);\n return failed;\n }\n\n private async _uploadOne(uploadUrl: string, root: string, filePath: string): Promise<UploadOutcome> {\n // Path traversal defense — the on-disk file path must resolve inside the\n // dist root, and the stored relative key must not climb above it.\n const resolved = path.resolve(filePath);\n if (!resolved.startsWith(root + path.sep) && resolved !== root) {\n return { file: filePath, ok: false, error: 'path escapes dist root' };\n }\n const relativePath = path.relative(root, resolved);\n if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {\n return { file: filePath, ok: false, error: 'invalid relative path' };\n }\n\n let stat: fs.Stats;\n try {\n stat = await fs.promises.stat(resolved);\n } catch (e) {\n return { file: relativePath, ok: false, error: (e as Error).message };\n }\n if (stat.size > this.opts.maxFileBytes) {\n console.warn(`[InstantTasks] Skipping ${relativePath} — ${stat.size} bytes exceeds cap ${this.opts.maxFileBytes}`);\n return { file: relativePath, ok: false, error: `file too large (${stat.size} bytes)` };\n }\n\n let content: string;\n try {\n content = await fs.promises.readFile(resolved, 'utf-8');\n } catch (e) {\n return { file: relativePath, ok: false, error: (e as Error).message };\n }\n\n let lastErr: string | undefined;\n let lastStatus: number | undefined;\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(uploadUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.opts.secretKey}`,\n },\n body: JSON.stringify({ path: relativePath, content }),\n });\n if (res.ok) return { file: relativePath, ok: true, status: res.status };\n lastStatus = res.status;\n // Retry only transient 5xx; 4xx is configuration — fail fast.\n if (res.status < 500 || res.status >= 600) break;\n } catch (e) {\n lastErr = (e as Error).message;\n }\n if (attempt < MAX_RETRIES) {\n await new Promise((r) => setTimeout(r, RETRY_BASE_MS * Math.pow(2, attempt)));\n }\n }\n console.warn(`[InstantTasks] Failed to upload ${relativePath}: ${lastStatus ? `HTTP ${lastStatus}` : lastErr ?? 'unknown'}`);\n return { file: relativePath, ok: false, status: lastStatus, error: lastErr };\n }\n\n private _findMapFiles(dir: string): string[] {\n if (!fs.existsSync(dir)) return [];\n const results: string[] = [];\n const root = path.resolve(dir);\n const walk = (current: string) => {\n for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n const full = path.join(current, entry.name);\n // Refuse to follow symlinks — they could point outside dist root.\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.isFile() && entry.name.endsWith('.map')) {\n const resolved = path.resolve(full);\n if (resolved.startsWith(root + path.sep) || resolved === root) results.push(full);\n }\n }\n };\n walk(root);\n return results;\n }\n}\n","import { InstantTasksSourceMapsPlugin } from './sourcemaps-upload';\nimport type { InitOptions } from '@koderlabs/tasks-sdk';\n\nexport interface WithInstantTasksOptions {\n /** Matches InitOptions.endpoint — where to POST source maps */\n endpoint?: string;\n /**\n * Management secret key for source-map uploads.\n * Defaults to process.env.INSTANTTASKS_SECRET_KEY.\n */\n secretKey?: string;\n /**\n * Skip source-map upload even in production. Useful during CI dry-runs.\n * @default false\n */\n disableSourceMapUpload?: boolean;\n /**\n * Relative path to Next.js output directory.\n * @default '.next'\n */\n distDir?: string;\n}\n\ntype NextConfig = Record<string, any>;\ntype NextConfigFn = (phase: string, args: Record<string, any>) => NextConfig | Promise<NextConfig>;\n\n/**\n * `withInstantTasks(opts)(nextConfig)` — higher-order wrapper for next.config.js/ts.\n *\n * What it does:\n * 1. Enables `productionBrowserSourceMaps: true` so Next.js emits browser .map files.\n * 2. In production builds only, injects `InstantTasksSourceMapsPlugin` into the\n * Webpack config so source maps are uploaded after the client bundle is emitted.\n *\n * Turbopack v1 decision: Next.js 15 ships Turbopack as the **dev** bundler only;\n * production builds continue to use Webpack. We hook into `webpack` here (v1).\n * Turbopack plugin support is deferred to v1.1 once the `experimental.turbopack`\n * plugin API stabilises.\n *\n * @example\n * ```js\n * // next.config.js\n * const { withInstantTasks } = require('@koderlabs/tasks-sdk-nextjs/plugin');\n * module.exports = withInstantTasks({ secretKey: process.env.IT_KEY })({});\n * ```\n */\nexport function withInstantTasks(opts: WithInstantTasksOptions = {}) {\n return function applyInstantTasks(nextConfig: NextConfig | NextConfigFn): NextConfig | NextConfigFn {\n // Support next.config.js that exports a function (phase-based config).\n if (typeof nextConfig === 'function') {\n return async (phase: string, args: Record<string, any>) => {\n const resolved = await (nextConfig as NextConfigFn)(phase, args);\n return applyConfig(resolved, opts);\n };\n }\n return applyConfig(nextConfig, opts);\n };\n}\n\nfunction applyConfig(config: NextConfig, opts: WithInstantTasksOptions): NextConfig {\n // Refuse to read the secret from a NEXT_PUBLIC_* env. Next.js inlines those into\n // the client bundle; any value present there is already compromised. Crashing\n // the build is the only safe response.\n for (const key of Object.keys(process.env)) {\n if (/^NEXT_PUBLIC_.*INSTANTTASKS.*SECRET/i.test(key)) {\n throw new Error(\n `[InstantTasks] ${key} is exposed via NEXT_PUBLIC_* and will leak into the client bundle. ` +\n 'Move the value to a non-NEXT_PUBLIC env (e.g. .env.local or CI secret) and read via INSTANTTASKS_SECRET_KEY.',\n );\n }\n }\n const secretKey =\n opts.secretKey ?? process.env['INSTANTTASKS_SECRET_KEY'] ?? '';\n const endpoint =\n opts.endpoint ?? 'https://tasks.koderlabs.net';\n const distDir = opts.distDir ?? config.distDir ?? '.next';\n\n const merged: NextConfig = {\n ...config,\n // Always emit browser source maps in production — required for meaningful\n // stack traces in the Issues dashboard.\n productionBrowserSourceMaps: true,\n };\n\n // Wrap the existing webpack config function (or create one).\n const previousWebpack = config.webpack;\n\n merged.webpack = (webpackConfig: any, context: any) => {\n // Call any pre-existing webpack customisation first.\n let finalConfig = previousWebpack\n ? previousWebpack(webpackConfig, context)\n : webpackConfig;\n\n // Only upload in production builds; skip in dev and in the server compiler.\n const { isServer, dev } = context;\n const shouldUpload =\n !dev &&\n !isServer &&\n !opts.disableSourceMapUpload &&\n Boolean(secretKey);\n\n if (shouldUpload) {\n finalConfig.plugins = [\n ...(finalConfig.plugins ?? []),\n new InstantTasksSourceMapsPlugin({ endpoint, secretKey, distDir }),\n ];\n }\n\n return finalConfig;\n };\n\n return merged;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export { WithInstantTasksOptions, withInstantTasks } from './plugin.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for the source-maps upload webpack plugin.
|
|
5
|
+
*/
|
|
6
|
+
interface SourceMapsUploadOptions {
|
|
7
|
+
/** InstantTasks API base URL (e.g. "https://tasks.koderlabs.net") */
|
|
8
|
+
endpoint: string;
|
|
9
|
+
/** Project access key with upload permissions */
|
|
10
|
+
secretKey: string;
|
|
11
|
+
/** Directory to scan for .map files (defaults to ".next") */
|
|
12
|
+
distDir?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When true (or `process.env.CI === 'true'`), exit the build with a non-zero
|
|
15
|
+
* code if ANY map fails to upload. Default: opt-in via env so existing
|
|
16
|
+
* pipelines don't break on rollout — set explicitly once stable.
|
|
17
|
+
*/
|
|
18
|
+
strict?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Skip files larger than this (bytes). Default 50MB — beyond which the upload
|
|
21
|
+
* is almost certainly going to OOM or time out anyway.
|
|
22
|
+
*/
|
|
23
|
+
maxFileBytes?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Webpack plugin that uploads source maps to the InstantTasks SDK endpoint
|
|
27
|
+
* after a **production** build completes.
|
|
28
|
+
*
|
|
29
|
+
* Failure handling: when `strict` is true (or `CI=true`), any failed upload
|
|
30
|
+
* fails the build. Otherwise failures are warned but the build continues
|
|
31
|
+
* — same as before, but with a much louder report (per-file + summary).
|
|
32
|
+
*
|
|
33
|
+
* Path safety: the relative path stored alongside the map is validated to
|
|
34
|
+
* never contain `..` or be absolute — defends against a misconfigured
|
|
35
|
+
* `outputPath` accidentally letting the plugin walk symlinked files outside
|
|
36
|
+
* the dist directory and store them under attacker-influenced keys.
|
|
37
|
+
*/
|
|
38
|
+
declare class InstantTasksSourceMapsPlugin {
|
|
39
|
+
private readonly opts;
|
|
40
|
+
constructor(opts: SourceMapsUploadOptions);
|
|
41
|
+
apply(compiler: any): void;
|
|
42
|
+
private _uploadMaps;
|
|
43
|
+
private _uploadOne;
|
|
44
|
+
private _findMapFiles;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { InstantTasksSourceMapsPlugin, type SourceMapsUploadOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export { WithInstantTasksOptions, withInstantTasks } from './plugin.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for the source-maps upload webpack plugin.
|
|
5
|
+
*/
|
|
6
|
+
interface SourceMapsUploadOptions {
|
|
7
|
+
/** InstantTasks API base URL (e.g. "https://tasks.koderlabs.net") */
|
|
8
|
+
endpoint: string;
|
|
9
|
+
/** Project access key with upload permissions */
|
|
10
|
+
secretKey: string;
|
|
11
|
+
/** Directory to scan for .map files (defaults to ".next") */
|
|
12
|
+
distDir?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When true (or `process.env.CI === 'true'`), exit the build with a non-zero
|
|
15
|
+
* code if ANY map fails to upload. Default: opt-in via env so existing
|
|
16
|
+
* pipelines don't break on rollout — set explicitly once stable.
|
|
17
|
+
*/
|
|
18
|
+
strict?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Skip files larger than this (bytes). Default 50MB — beyond which the upload
|
|
21
|
+
* is almost certainly going to OOM or time out anyway.
|
|
22
|
+
*/
|
|
23
|
+
maxFileBytes?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Webpack plugin that uploads source maps to the InstantTasks SDK endpoint
|
|
27
|
+
* after a **production** build completes.
|
|
28
|
+
*
|
|
29
|
+
* Failure handling: when `strict` is true (or `CI=true`), any failed upload
|
|
30
|
+
* fails the build. Otherwise failures are warned but the build continues
|
|
31
|
+
* — same as before, but with a much louder report (per-file + summary).
|
|
32
|
+
*
|
|
33
|
+
* Path safety: the relative path stored alongside the map is validated to
|
|
34
|
+
* never contain `..` or be absolute — defends against a misconfigured
|
|
35
|
+
* `outputPath` accidentally letting the plugin walk symlinked files outside
|
|
36
|
+
* the dist directory and store them under attacker-influenced keys.
|
|
37
|
+
*/
|
|
38
|
+
declare class InstantTasksSourceMapsPlugin {
|
|
39
|
+
private readonly opts;
|
|
40
|
+
constructor(opts: SourceMapsUploadOptions);
|
|
41
|
+
apply(compiler: any): void;
|
|
42
|
+
private _uploadMaps;
|
|
43
|
+
private _uploadOne;
|
|
44
|
+
private _findMapFiles;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { InstantTasksSourceMapsPlugin, type SourceMapsUploadOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/plugin.cjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/plugin.ts
|
|
31
|
+
var plugin_exports = {};
|
|
32
|
+
__export(plugin_exports, {
|
|
33
|
+
withInstantTasks: () => withInstantTasks
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(plugin_exports);
|
|
36
|
+
|
|
37
|
+
// src/sourcemaps-upload.ts
|
|
38
|
+
var fs = __toESM(require("fs"), 1);
|
|
39
|
+
var path = __toESM(require("path"), 1);
|
|
40
|
+
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
41
|
+
var MAX_RETRIES = 2;
|
|
42
|
+
var RETRY_BASE_MS = 1e3;
|
|
43
|
+
var InstantTasksSourceMapsPlugin = class {
|
|
44
|
+
constructor(opts) {
|
|
45
|
+
this.opts = {
|
|
46
|
+
distDir: ".next",
|
|
47
|
+
maxFileBytes: DEFAULT_MAX_BYTES,
|
|
48
|
+
strict: opts.strict ?? process.env.CI === "true",
|
|
49
|
+
...opts
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
apply(compiler) {
|
|
53
|
+
const isProduction = compiler.options?.mode === "production";
|
|
54
|
+
const isClientCompiler = !compiler.name || compiler.name === "client" || compiler.name === "browser";
|
|
55
|
+
if (!isProduction || !isClientCompiler) return;
|
|
56
|
+
compiler.hooks.afterEmit.tapAsync(
|
|
57
|
+
"InstantTasksSourceMapsPlugin",
|
|
58
|
+
async (compilation, callback) => {
|
|
59
|
+
try {
|
|
60
|
+
const failed = await this._uploadMaps(compilation.outputPath ?? this.opts.distDir);
|
|
61
|
+
if (failed.length > 0 && this.opts.strict) {
|
|
62
|
+
const summary = failed.map((f) => ` ${f.file}: ${f.status ? `HTTP ${f.status}` : f.error ?? "unknown"}`).join("\n");
|
|
63
|
+
callback(new Error(`[InstantTasks] ${failed.length} source-map upload(s) failed:
|
|
64
|
+
${summary}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (this.opts.strict) {
|
|
69
|
+
callback(err);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.warn("[InstantTasks] source-map upload failed:", err);
|
|
73
|
+
}
|
|
74
|
+
callback();
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
async _uploadMaps(outputPath) {
|
|
79
|
+
const resolvedRoot = path.resolve(outputPath);
|
|
80
|
+
const mapFiles = this._findMapFiles(resolvedRoot);
|
|
81
|
+
if (mapFiles.length === 0) {
|
|
82
|
+
console.log("[InstantTasks] No .map files found to upload in", resolvedRoot);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
console.log(`[InstantTasks] Uploading ${mapFiles.length} source map(s)\u2026`);
|
|
86
|
+
const uploadUrl = `${this.opts.endpoint}/api/v1/sdk/source-maps`;
|
|
87
|
+
const results = await Promise.all(mapFiles.map((f) => this._uploadOne(uploadUrl, resolvedRoot, f)));
|
|
88
|
+
const failed = results.filter((r) => !r.ok);
|
|
89
|
+
const ok = results.length - failed.length;
|
|
90
|
+
console.log(`[InstantTasks] Source map upload complete \u2014 ${ok} ok, ${failed.length} failed.`);
|
|
91
|
+
return failed;
|
|
92
|
+
}
|
|
93
|
+
async _uploadOne(uploadUrl, root, filePath) {
|
|
94
|
+
const resolved = path.resolve(filePath);
|
|
95
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
96
|
+
return { file: filePath, ok: false, error: "path escapes dist root" };
|
|
97
|
+
}
|
|
98
|
+
const relativePath = path.relative(root, resolved);
|
|
99
|
+
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
100
|
+
return { file: filePath, ok: false, error: "invalid relative path" };
|
|
101
|
+
}
|
|
102
|
+
let stat;
|
|
103
|
+
try {
|
|
104
|
+
stat = await fs.promises.stat(resolved);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return { file: relativePath, ok: false, error: e.message };
|
|
107
|
+
}
|
|
108
|
+
if (stat.size > this.opts.maxFileBytes) {
|
|
109
|
+
console.warn(`[InstantTasks] Skipping ${relativePath} \u2014 ${stat.size} bytes exceeds cap ${this.opts.maxFileBytes}`);
|
|
110
|
+
return { file: relativePath, ok: false, error: `file too large (${stat.size} bytes)` };
|
|
111
|
+
}
|
|
112
|
+
let content;
|
|
113
|
+
try {
|
|
114
|
+
content = await fs.promises.readFile(resolved, "utf-8");
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return { file: relativePath, ok: false, error: e.message };
|
|
117
|
+
}
|
|
118
|
+
let lastErr;
|
|
119
|
+
let lastStatus;
|
|
120
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(uploadUrl, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
Authorization: `Bearer ${this.opts.secretKey}`
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({ path: relativePath, content })
|
|
129
|
+
});
|
|
130
|
+
if (res.ok) return { file: relativePath, ok: true, status: res.status };
|
|
131
|
+
lastStatus = res.status;
|
|
132
|
+
if (res.status < 500 || res.status >= 600) break;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
lastErr = e.message;
|
|
135
|
+
}
|
|
136
|
+
if (attempt < MAX_RETRIES) {
|
|
137
|
+
await new Promise((r) => setTimeout(r, RETRY_BASE_MS * Math.pow(2, attempt)));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.warn(`[InstantTasks] Failed to upload ${relativePath}: ${lastStatus ? `HTTP ${lastStatus}` : lastErr ?? "unknown"}`);
|
|
141
|
+
return { file: relativePath, ok: false, status: lastStatus, error: lastErr };
|
|
142
|
+
}
|
|
143
|
+
_findMapFiles(dir) {
|
|
144
|
+
if (!fs.existsSync(dir)) return [];
|
|
145
|
+
const results = [];
|
|
146
|
+
const root = path.resolve(dir);
|
|
147
|
+
const walk = (current) => {
|
|
148
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
149
|
+
const full = path.join(current, entry.name);
|
|
150
|
+
if (entry.isSymbolicLink()) continue;
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
walk(full);
|
|
153
|
+
} else if (entry.isFile() && entry.name.endsWith(".map")) {
|
|
154
|
+
const resolved = path.resolve(full);
|
|
155
|
+
if (resolved.startsWith(root + path.sep) || resolved === root) results.push(full);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
walk(root);
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/plugin.ts
|
|
165
|
+
function withInstantTasks(opts = {}) {
|
|
166
|
+
return function applyInstantTasks(nextConfig) {
|
|
167
|
+
if (typeof nextConfig === "function") {
|
|
168
|
+
return async (phase, args) => {
|
|
169
|
+
const resolved = await nextConfig(phase, args);
|
|
170
|
+
return applyConfig(resolved, opts);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return applyConfig(nextConfig, opts);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function applyConfig(config, opts) {
|
|
177
|
+
for (const key of Object.keys(process.env)) {
|
|
178
|
+
if (/^NEXT_PUBLIC_.*INSTANTTASKS.*SECRET/i.test(key)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`[InstantTasks] ${key} is exposed via NEXT_PUBLIC_* and will leak into the client bundle. Move the value to a non-NEXT_PUBLIC env (e.g. .env.local or CI secret) and read via INSTANTTASKS_SECRET_KEY.`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const secretKey = opts.secretKey ?? process.env["INSTANTTASKS_SECRET_KEY"] ?? "";
|
|
185
|
+
const endpoint = opts.endpoint ?? "https://tasks.koderlabs.net";
|
|
186
|
+
const distDir = opts.distDir ?? config.distDir ?? ".next";
|
|
187
|
+
const merged = {
|
|
188
|
+
...config,
|
|
189
|
+
// Always emit browser source maps in production — required for meaningful
|
|
190
|
+
// stack traces in the Issues dashboard.
|
|
191
|
+
productionBrowserSourceMaps: true
|
|
192
|
+
};
|
|
193
|
+
const previousWebpack = config.webpack;
|
|
194
|
+
merged.webpack = (webpackConfig, context) => {
|
|
195
|
+
let finalConfig = previousWebpack ? previousWebpack(webpackConfig, context) : webpackConfig;
|
|
196
|
+
const { isServer, dev } = context;
|
|
197
|
+
const shouldUpload = !dev && !isServer && !opts.disableSourceMapUpload && Boolean(secretKey);
|
|
198
|
+
if (shouldUpload) {
|
|
199
|
+
finalConfig.plugins = [
|
|
200
|
+
...finalConfig.plugins ?? [],
|
|
201
|
+
new InstantTasksSourceMapsPlugin({ endpoint, secretKey, distDir })
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
return finalConfig;
|
|
205
|
+
};
|
|
206
|
+
return merged;
|
|
207
|
+
}
|
|
208
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
209
|
+
0 && (module.exports = {
|
|
210
|
+
withInstantTasks
|
|
211
|
+
});
|
|
212
|
+
//# sourceMappingURL=plugin.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts","../src/sourcemaps-upload.ts"],"sourcesContent":["import { InstantTasksSourceMapsPlugin } from './sourcemaps-upload';\nimport type { InitOptions } from '@koderlabs/tasks-sdk';\n\nexport interface WithInstantTasksOptions {\n /** Matches InitOptions.endpoint — where to POST source maps */\n endpoint?: string;\n /**\n * Management secret key for source-map uploads.\n * Defaults to process.env.INSTANTTASKS_SECRET_KEY.\n */\n secretKey?: string;\n /**\n * Skip source-map upload even in production. Useful during CI dry-runs.\n * @default false\n */\n disableSourceMapUpload?: boolean;\n /**\n * Relative path to Next.js output directory.\n * @default '.next'\n */\n distDir?: string;\n}\n\ntype NextConfig = Record<string, any>;\ntype NextConfigFn = (phase: string, args: Record<string, any>) => NextConfig | Promise<NextConfig>;\n\n/**\n * `withInstantTasks(opts)(nextConfig)` — higher-order wrapper for next.config.js/ts.\n *\n * What it does:\n * 1. Enables `productionBrowserSourceMaps: true` so Next.js emits browser .map files.\n * 2. In production builds only, injects `InstantTasksSourceMapsPlugin` into the\n * Webpack config so source maps are uploaded after the client bundle is emitted.\n *\n * Turbopack v1 decision: Next.js 15 ships Turbopack as the **dev** bundler only;\n * production builds continue to use Webpack. We hook into `webpack` here (v1).\n * Turbopack plugin support is deferred to v1.1 once the `experimental.turbopack`\n * plugin API stabilises.\n *\n * @example\n * ```js\n * // next.config.js\n * const { withInstantTasks } = require('@koderlabs/tasks-sdk-nextjs/plugin');\n * module.exports = withInstantTasks({ secretKey: process.env.IT_KEY })({});\n * ```\n */\nexport function withInstantTasks(opts: WithInstantTasksOptions = {}) {\n return function applyInstantTasks(nextConfig: NextConfig | NextConfigFn): NextConfig | NextConfigFn {\n // Support next.config.js that exports a function (phase-based config).\n if (typeof nextConfig === 'function') {\n return async (phase: string, args: Record<string, any>) => {\n const resolved = await (nextConfig as NextConfigFn)(phase, args);\n return applyConfig(resolved, opts);\n };\n }\n return applyConfig(nextConfig, opts);\n };\n}\n\nfunction applyConfig(config: NextConfig, opts: WithInstantTasksOptions): NextConfig {\n // Refuse to read the secret from a NEXT_PUBLIC_* env. Next.js inlines those into\n // the client bundle; any value present there is already compromised. Crashing\n // the build is the only safe response.\n for (const key of Object.keys(process.env)) {\n if (/^NEXT_PUBLIC_.*INSTANTTASKS.*SECRET/i.test(key)) {\n throw new Error(\n `[InstantTasks] ${key} is exposed via NEXT_PUBLIC_* and will leak into the client bundle. ` +\n 'Move the value to a non-NEXT_PUBLIC env (e.g. .env.local or CI secret) and read via INSTANTTASKS_SECRET_KEY.',\n );\n }\n }\n const secretKey =\n opts.secretKey ?? process.env['INSTANTTASKS_SECRET_KEY'] ?? '';\n const endpoint =\n opts.endpoint ?? 'https://tasks.koderlabs.net';\n const distDir = opts.distDir ?? config.distDir ?? '.next';\n\n const merged: NextConfig = {\n ...config,\n // Always emit browser source maps in production — required for meaningful\n // stack traces in the Issues dashboard.\n productionBrowserSourceMaps: true,\n };\n\n // Wrap the existing webpack config function (or create one).\n const previousWebpack = config.webpack;\n\n merged.webpack = (webpackConfig: any, context: any) => {\n // Call any pre-existing webpack customisation first.\n let finalConfig = previousWebpack\n ? previousWebpack(webpackConfig, context)\n : webpackConfig;\n\n // Only upload in production builds; skip in dev and in the server compiler.\n const { isServer, dev } = context;\n const shouldUpload =\n !dev &&\n !isServer &&\n !opts.disableSourceMapUpload &&\n Boolean(secretKey);\n\n if (shouldUpload) {\n finalConfig.plugins = [\n ...(finalConfig.plugins ?? []),\n new InstantTasksSourceMapsPlugin({ endpoint, secretKey, distDir }),\n ];\n }\n\n return finalConfig;\n };\n\n return merged;\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Options for the source-maps upload webpack plugin.\n */\nexport interface SourceMapsUploadOptions {\n /** InstantTasks API base URL (e.g. \"https://tasks.koderlabs.net\") */\n endpoint: string;\n /** Project access key with upload permissions */\n secretKey: string;\n /** Directory to scan for .map files (defaults to \".next\") */\n distDir?: string;\n /**\n * When true (or `process.env.CI === 'true'`), exit the build with a non-zero\n * code if ANY map fails to upload. Default: opt-in via env so existing\n * pipelines don't break on rollout — set explicitly once stable.\n */\n strict?: boolean;\n /**\n * Skip files larger than this (bytes). Default 50MB — beyond which the upload\n * is almost certainly going to OOM or time out anyway.\n */\n maxFileBytes?: number;\n}\n\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_MS = 1_000;\n\ninterface UploadOutcome { file: string; ok: boolean; status?: number; error?: string }\n\n/**\n * Webpack plugin that uploads source maps to the InstantTasks SDK endpoint\n * after a **production** build completes.\n *\n * Failure handling: when `strict` is true (or `CI=true`), any failed upload\n * fails the build. Otherwise failures are warned but the build continues\n * — same as before, but with a much louder report (per-file + summary).\n *\n * Path safety: the relative path stored alongside the map is validated to\n * never contain `..` or be absolute — defends against a misconfigured\n * `outputPath` accidentally letting the plugin walk symlinked files outside\n * the dist directory and store them under attacker-influenced keys.\n */\nexport class InstantTasksSourceMapsPlugin {\n private readonly opts: Required<Omit<SourceMapsUploadOptions, 'strict'>> & { strict: boolean };\n\n constructor(opts: SourceMapsUploadOptions) {\n this.opts = {\n distDir: '.next',\n maxFileBytes: DEFAULT_MAX_BYTES,\n strict: opts.strict ?? process.env.CI === 'true',\n ...opts,\n };\n }\n\n apply(compiler: any) {\n // Skip in any environment other than a production client build.\n const isProduction = compiler.options?.mode === 'production';\n const isClientCompiler =\n !compiler.name || compiler.name === 'client' || compiler.name === 'browser';\n\n if (!isProduction || !isClientCompiler) return;\n\n compiler.hooks.afterEmit.tapAsync(\n 'InstantTasksSourceMapsPlugin',\n async (compilation: any, callback: (err?: Error) => void) => {\n try {\n const failed = await this._uploadMaps(compilation.outputPath ?? this.opts.distDir);\n if (failed.length > 0 && this.opts.strict) {\n const summary = failed\n .map((f) => ` ${f.file}: ${f.status ? `HTTP ${f.status}` : f.error ?? 'unknown'}`)\n .join('\\n');\n callback(new Error(`[InstantTasks] ${failed.length} source-map upload(s) failed:\\n${summary}`));\n return;\n }\n } catch (err) {\n if (this.opts.strict) {\n callback(err as Error);\n return;\n }\n console.warn('[InstantTasks] source-map upload failed:', err);\n }\n callback();\n },\n );\n }\n\n private async _uploadMaps(outputPath: string): Promise<UploadOutcome[]> {\n const resolvedRoot = path.resolve(outputPath);\n const mapFiles = this._findMapFiles(resolvedRoot);\n if (mapFiles.length === 0) {\n console.log('[InstantTasks] No .map files found to upload in', resolvedRoot);\n return [];\n }\n\n console.log(`[InstantTasks] Uploading ${mapFiles.length} source map(s)…`);\n\n const uploadUrl = `${this.opts.endpoint}/api/v1/sdk/source-maps`;\n const results = await Promise.all(mapFiles.map((f) => this._uploadOne(uploadUrl, resolvedRoot, f)));\n const failed = results.filter((r) => !r.ok);\n const ok = results.length - failed.length;\n console.log(`[InstantTasks] Source map upload complete — ${ok} ok, ${failed.length} failed.`);\n return failed;\n }\n\n private async _uploadOne(uploadUrl: string, root: string, filePath: string): Promise<UploadOutcome> {\n // Path traversal defense — the on-disk file path must resolve inside the\n // dist root, and the stored relative key must not climb above it.\n const resolved = path.resolve(filePath);\n if (!resolved.startsWith(root + path.sep) && resolved !== root) {\n return { file: filePath, ok: false, error: 'path escapes dist root' };\n }\n const relativePath = path.relative(root, resolved);\n if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {\n return { file: filePath, ok: false, error: 'invalid relative path' };\n }\n\n let stat: fs.Stats;\n try {\n stat = await fs.promises.stat(resolved);\n } catch (e) {\n return { file: relativePath, ok: false, error: (e as Error).message };\n }\n if (stat.size > this.opts.maxFileBytes) {\n console.warn(`[InstantTasks] Skipping ${relativePath} — ${stat.size} bytes exceeds cap ${this.opts.maxFileBytes}`);\n return { file: relativePath, ok: false, error: `file too large (${stat.size} bytes)` };\n }\n\n let content: string;\n try {\n content = await fs.promises.readFile(resolved, 'utf-8');\n } catch (e) {\n return { file: relativePath, ok: false, error: (e as Error).message };\n }\n\n let lastErr: string | undefined;\n let lastStatus: number | undefined;\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(uploadUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.opts.secretKey}`,\n },\n body: JSON.stringify({ path: relativePath, content }),\n });\n if (res.ok) return { file: relativePath, ok: true, status: res.status };\n lastStatus = res.status;\n // Retry only transient 5xx; 4xx is configuration — fail fast.\n if (res.status < 500 || res.status >= 600) break;\n } catch (e) {\n lastErr = (e as Error).message;\n }\n if (attempt < MAX_RETRIES) {\n await new Promise((r) => setTimeout(r, RETRY_BASE_MS * Math.pow(2, attempt)));\n }\n }\n console.warn(`[InstantTasks] Failed to upload ${relativePath}: ${lastStatus ? `HTTP ${lastStatus}` : lastErr ?? 'unknown'}`);\n return { file: relativePath, ok: false, status: lastStatus, error: lastErr };\n }\n\n private _findMapFiles(dir: string): string[] {\n if (!fs.existsSync(dir)) return [];\n const results: string[] = [];\n const root = path.resolve(dir);\n const walk = (current: string) => {\n for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n const full = path.join(current, entry.name);\n // Refuse to follow symlinks — they could point outside dist root.\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.isFile() && entry.name.endsWith('.map')) {\n const resolved = path.resolve(full);\n if (resolved.startsWith(root + path.sep) || resolved === root) results.push(full);\n }\n }\n };\n walk(root);\n return results;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,SAAoB;AACpB,WAAsB;AAyBtB,IAAM,oBAAoB,KAAK,OAAO;AACtC,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAiBf,IAAM,+BAAN,MAAmC;AAAA,EAGxC,YAAY,MAA+B;AACzC,SAAK,OAAO;AAAA,MACV,SAAS;AAAA,MACT,cAAc;AAAA,MACd,QAAQ,KAAK,UAAU,QAAQ,IAAI,OAAO;AAAA,MAC1C,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EAEA,MAAM,UAAe;AAEnB,UAAM,eAAe,SAAS,SAAS,SAAS;AAChD,UAAM,mBACJ,CAAC,SAAS,QAAQ,SAAS,SAAS,YAAY,SAAS,SAAS;AAEpE,QAAI,CAAC,gBAAgB,CAAC,iBAAkB;AAExC,aAAS,MAAM,UAAU;AAAA,MACvB;AAAA,MACA,OAAO,aAAkB,aAAoC;AAC3D,YAAI;AACF,gBAAM,SAAS,MAAM,KAAK,YAAY,YAAY,cAAc,KAAK,KAAK,OAAO;AACjF,cAAI,OAAO,SAAS,KAAK,KAAK,KAAK,QAAQ;AACzC,kBAAM,UAAU,OACb,IAAI,CAAC,MAAM,KAAK,EAAE,IAAI,KAAK,EAAE,SAAS,QAAQ,EAAE,MAAM,KAAK,EAAE,SAAS,SAAS,EAAE,EACjF,KAAK,IAAI;AACZ,qBAAS,IAAI,MAAM,kBAAkB,OAAO,MAAM;AAAA,EAAkC,OAAO,EAAE,CAAC;AAC9F;AAAA,UACF;AAAA,QACF,SAAS,KAAK;AACZ,cAAI,KAAK,KAAK,QAAQ;AACpB,qBAAS,GAAY;AACrB;AAAA,UACF;AACA,kBAAQ,KAAK,4CAA4C,GAAG;AAAA,QAC9D;AACA,iBAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,YAA8C;AACtE,UAAM,eAAoB,aAAQ,UAAU;AAC5C,UAAM,WAAW,KAAK,cAAc,YAAY;AAChD,QAAI,SAAS,WAAW,GAAG;AACzB,cAAQ,IAAI,mDAAmD,YAAY;AAC3E,aAAO,CAAC;AAAA,IACV;AAEA,YAAQ,IAAI,4BAA4B,SAAS,MAAM,sBAAiB;AAExE,UAAM,YAAY,GAAG,KAAK,KAAK,QAAQ;AACvC,UAAM,UAAU,MAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,MAAM,KAAK,WAAW,WAAW,cAAc,CAAC,CAAC,CAAC;AAClG,UAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE;AAC1C,UAAM,KAAK,QAAQ,SAAS,OAAO;AACnC,YAAQ,IAAI,oDAA+C,EAAE,QAAQ,OAAO,MAAM,UAAU;AAC5F,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,WAAW,WAAmB,MAAc,UAA0C;AAGlG,UAAM,WAAgB,aAAQ,QAAQ;AACtC,QAAI,CAAC,SAAS,WAAW,OAAY,QAAG,KAAK,aAAa,MAAM;AAC9D,aAAO,EAAE,MAAM,UAAU,IAAI,OAAO,OAAO,yBAAyB;AAAA,IACtE;AACA,UAAM,eAAoB,cAAS,MAAM,QAAQ;AACjD,QAAI,CAAC,gBAAgB,aAAa,WAAW,IAAI,KAAU,gBAAW,YAAY,GAAG;AACnF,aAAO,EAAE,MAAM,UAAU,IAAI,OAAO,OAAO,wBAAwB;AAAA,IACrE;AAEA,QAAI;AACJ,QAAI;AACF,aAAO,MAAS,YAAS,KAAK,QAAQ;AAAA,IACxC,SAAS,GAAG;AACV,aAAO,EAAE,MAAM,cAAc,IAAI,OAAO,OAAQ,EAAY,QAAQ;AAAA,IACtE;AACA,QAAI,KAAK,OAAO,KAAK,KAAK,cAAc;AACtC,cAAQ,KAAK,2BAA2B,YAAY,WAAM,KAAK,IAAI,sBAAsB,KAAK,KAAK,YAAY,EAAE;AACjH,aAAO,EAAE,MAAM,cAAc,IAAI,OAAO,OAAO,mBAAmB,KAAK,IAAI,UAAU;AAAA,IACvF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAU,MAAS,YAAS,SAAS,UAAU,OAAO;AAAA,IACxD,SAAS,GAAG;AACV,aAAO,EAAE,MAAM,cAAc,IAAI,OAAO,OAAQ,EAAY,QAAQ;AAAA,IACtE;AAEA,QAAI;AACJ,QAAI;AACJ,aAAS,UAAU,GAAG,WAAW,aAAa,WAAW;AACvD,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,WAAW;AAAA,UACjC,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,gBAAgB;AAAA,YAChB,eAAe,UAAU,KAAK,KAAK,SAAS;AAAA,UAC9C;AAAA,UACA,MAAM,KAAK,UAAU,EAAE,MAAM,cAAc,QAAQ,CAAC;AAAA,QACtD,CAAC;AACD,YAAI,IAAI,GAAI,QAAO,EAAE,MAAM,cAAc,IAAI,MAAM,QAAQ,IAAI,OAAO;AACtE,qBAAa,IAAI;AAEjB,YAAI,IAAI,SAAS,OAAO,IAAI,UAAU,IAAK;AAAA,MAC7C,SAAS,GAAG;AACV,kBAAW,EAAY;AAAA,MACzB;AACA,UAAI,UAAU,aAAa;AACzB,cAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,MAC9E;AAAA,IACF;AACA,YAAQ,KAAK,mCAAmC,YAAY,KAAK,aAAa,QAAQ,UAAU,KAAK,WAAW,SAAS,EAAE;AAC3H,WAAO,EAAE,MAAM,cAAc,IAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ;AAAA,EAC7E;AAAA,EAEQ,cAAc,KAAuB;AAC3C,QAAI,CAAI,cAAW,GAAG,EAAG,QAAO,CAAC;AACjC,UAAM,UAAoB,CAAC;AAC3B,UAAM,OAAY,aAAQ,GAAG;AAC7B,UAAM,OAAO,CAAC,YAAoB;AAChC,iBAAW,SAAY,eAAY,SAAS,EAAE,eAAe,KAAK,CAAC,GAAG;AACpE,cAAM,OAAY,UAAK,SAAS,MAAM,IAAI;AAE1C,YAAI,MAAM,eAAe,EAAG;AAC5B,YAAI,MAAM,YAAY,GAAG;AACvB,eAAK,IAAI;AAAA,QACX,WAAW,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,MAAM,GAAG;AACxD,gBAAM,WAAgB,aAAQ,IAAI;AAClC,cAAI,SAAS,WAAW,OAAY,QAAG,KAAK,aAAa,KAAM,SAAQ,KAAK,IAAI;AAAA,QAClF;AAAA,MACF;AAAA,IACF;AACA,SAAK,IAAI;AACT,WAAO;AAAA,EACT;AACF;;;AD1IO,SAAS,iBAAiB,OAAgC,CAAC,GAAG;AACnE,SAAO,SAAS,kBAAkB,YAAkE;AAElG,QAAI,OAAO,eAAe,YAAY;AACpC,aAAO,OAAO,OAAe,SAA8B;AACzD,cAAM,WAAW,MAAO,WAA4B,OAAO,IAAI;AAC/D,eAAO,YAAY,UAAU,IAAI;AAAA,MACnC;AAAA,IACF;AACA,WAAO,YAAY,YAAY,IAAI;AAAA,EACrC;AACF;AAEA,SAAS,YAAY,QAAoB,MAA2C;AAIlF,aAAW,OAAO,OAAO,KAAK,QAAQ,GAAG,GAAG;AAC1C,QAAI,uCAAuC,KAAK,GAAG,GAAG;AACpD,YAAM,IAAI;AAAA,QACR,kBAAkB,GAAG;AAAA,MAEvB;AAAA,IACF;AAAA,EACF;AACA,QAAM,YACJ,KAAK,aAAa,QAAQ,IAAI,yBAAyB,KAAK;AAC9D,QAAM,WACJ,KAAK,YAAY;AACnB,QAAM,UAAU,KAAK,WAAW,OAAO,WAAW;AAElD,QAAM,SAAqB;AAAA,IACzB,GAAG;AAAA;AAAA;AAAA,IAGH,6BAA6B;AAAA,EAC/B;AAGA,QAAM,kBAAkB,OAAO;AAE/B,SAAO,UAAU,CAAC,eAAoB,YAAiB;AAErD,QAAI,cAAc,kBACd,gBAAgB,eAAe,OAAO,IACtC;AAGJ,UAAM,EAAE,UAAU,IAAI,IAAI;AAC1B,UAAM,eACJ,CAAC,OACD,CAAC,YACD,CAAC,KAAK,0BACN,QAAQ,SAAS;AAEnB,QAAI,cAAc;AAChB,kBAAY,UAAU;AAAA,QACpB,GAAI,YAAY,WAAW,CAAC;AAAA,QAC5B,IAAI,6BAA6B,EAAE,UAAU,WAAW,QAAQ,CAAC;AAAA,MACnE;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
interface WithInstantTasksOptions {
|
|
2
|
+
/** Matches InitOptions.endpoint — where to POST source maps */
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Management secret key for source-map uploads.
|
|
6
|
+
* Defaults to process.env.INSTANTTASKS_SECRET_KEY.
|
|
7
|
+
*/
|
|
8
|
+
secretKey?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Skip source-map upload even in production. Useful during CI dry-runs.
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
13
|
+
disableSourceMapUpload?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Relative path to Next.js output directory.
|
|
16
|
+
* @default '.next'
|
|
17
|
+
*/
|
|
18
|
+
distDir?: string;
|
|
19
|
+
}
|
|
20
|
+
type NextConfig = Record<string, any>;
|
|
21
|
+
type NextConfigFn = (phase: string, args: Record<string, any>) => NextConfig | Promise<NextConfig>;
|
|
22
|
+
/**
|
|
23
|
+
* `withInstantTasks(opts)(nextConfig)` — higher-order wrapper for next.config.js/ts.
|
|
24
|
+
*
|
|
25
|
+
* What it does:
|
|
26
|
+
* 1. Enables `productionBrowserSourceMaps: true` so Next.js emits browser .map files.
|
|
27
|
+
* 2. In production builds only, injects `InstantTasksSourceMapsPlugin` into the
|
|
28
|
+
* Webpack config so source maps are uploaded after the client bundle is emitted.
|
|
29
|
+
*
|
|
30
|
+
* Turbopack v1 decision: Next.js 15 ships Turbopack as the **dev** bundler only;
|
|
31
|
+
* production builds continue to use Webpack. We hook into `webpack` here (v1).
|
|
32
|
+
* Turbopack plugin support is deferred to v1.1 once the `experimental.turbopack`
|
|
33
|
+
* plugin API stabilises.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```js
|
|
37
|
+
* // next.config.js
|
|
38
|
+
* const { withInstantTasks } = require('@koderlabs/tasks-sdk-nextjs/plugin');
|
|
39
|
+
* module.exports = withInstantTasks({ secretKey: process.env.IT_KEY })({});
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function withInstantTasks(opts?: WithInstantTasksOptions): (nextConfig: NextConfig | NextConfigFn) => NextConfig | NextConfigFn;
|
|
43
|
+
|
|
44
|
+
export { type WithInstantTasksOptions, withInstantTasks };
|