@auriclabs/jobs-infra 2.0.0 → 2.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/.turbo/turbo-build.log +21 -15
- package/CHANGELOG.md +25 -0
- package/README.md +71 -1
- package/dist/index.cjs +108 -1
- package/dist/index.d.cts +57 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +57 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +85 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -2
- package/src/dashboard.ts +134 -0
- package/src/index.ts +1 -0
- package/src/job-resources.ts +36 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @auriclabs/jobs-infra@2.
|
|
2
|
+
> @auriclabs/jobs-infra@2.1.0 build /home/runner/work/packages/packages/packages/jobs-infra
|
|
3
3
|
> tsdown src/index.ts --format cjs,esm --dts --tsconfig tsconfig.build.json --no-hash
|
|
4
4
|
|
|
5
5
|
[33m[tsdown] Node.js v20.20.2 is deprecated. Support will be removed in the next minor release. Please upgrade to Node.js 22.18.0 or later.[39m
|
|
@@ -7,19 +7,25 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.build.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.cjs[22m [
|
|
11
|
-
[34mℹ[39m [33m[CJS][39m 1 files, total:
|
|
12
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.cts.map [
|
|
13
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.cts[22m[39m [
|
|
14
|
-
[34mℹ[39m [33m[CJS][39m 2 files, total:
|
|
15
|
-
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in
|
|
10
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.cjs[22m [2m5.43 kB[22m [2m│ gzip: 2.21 kB[22m
|
|
11
|
+
[34mℹ[39m [33m[CJS][39m 1 files, total: 5.43 kB
|
|
12
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.cts.map [2m1.06 kB[22m [2m│ gzip: 0.43 kB[22m
|
|
13
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.cts[22m[39m [2m3.21 kB[22m [2m│ gzip: 1.30 kB[22m
|
|
14
|
+
[34mℹ[39m [33m[CJS][39m 2 files, total: 4.27 kB
|
|
15
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugins. Here is a breakdown:
|
|
16
|
+
- rolldown-plugin-dts:generate (55%)
|
|
17
|
+
- tsdown:deps (44%)
|
|
18
|
+
See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
16
19
|
|
|
17
|
-
[
|
|
18
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[
|
|
19
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.
|
|
20
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[
|
|
21
|
-
[34mℹ[39m [34m[ESM][39m
|
|
22
|
-
[
|
|
23
|
-
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in
|
|
20
|
+
[32m✔[39m Build complete in [32m5776ms[39m
|
|
21
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m 4.22 kB[22m [2m│ gzip: 1.75 kB[22m
|
|
22
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m11.40 kB[22m [2m│ gzip: 4.08 kB[22m
|
|
23
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m 1.06 kB[22m [2m│ gzip: 0.43 kB[22m
|
|
24
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 3.21 kB[22m [2m│ gzip: 1.30 kB[22m
|
|
25
|
+
[34mℹ[39m [34m[ESM][39m 4 files, total: 19.89 kB
|
|
26
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugins. Here is a breakdown:
|
|
27
|
+
- rolldown-plugin-dts:generate (80%)
|
|
28
|
+
- tsdown:deps (19%)
|
|
29
|
+
See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
24
30
|
|
|
25
|
-
[32m✔[39m Build complete in [
|
|
31
|
+
[32m✔[39m Build complete in [32m5779ms[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @auriclabs/jobs-infra
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2f09e53: Add typed job registry (defineJobs), in-process registry executor
|
|
8
|
+
(createRegistryExecutorHandler), continuation/time-budget helpers (continueJob, createTimeBudget)
|
|
9
|
+
with per-attempt continuation state, and retryJob (which resumes from the last attempt's
|
|
10
|
+
continuation state). Fix prepareNextJobAttempt to allow retrying failed jobs and to reject
|
|
11
|
+
concurrent attempts on running jobs.
|
|
12
|
+
|
|
13
|
+
Add a jobs dashboard modeled on the migrations dashboard: a bundled Vite/React UI (ui/), a
|
|
14
|
+
dashboard API (createJobsDashboardApiHandler — list/summary/detail/retry/cancel; no job creation),
|
|
15
|
+
list/summary read methods on the job service, and an `auric-jobs-dashboard` local CLI that serves
|
|
16
|
+
the same UI against a deployed table via SSO.
|
|
17
|
+
|
|
18
|
+
jobs-infra: add 'in-process' executor resource variant that subscribes a consumer-provided handler
|
|
19
|
+
with QUEUE_URL_LIST wired, wire QUEUE_URL_LIST on the lambda executor so scheduledAt re-enqueue
|
|
20
|
+
works there too, and add createJobsDashboard (ApiGatewayV2 + StaticSite + optional CloudFront
|
|
21
|
+
basic-auth) for deploying the dashboard.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Updated dependencies [2f09e53]
|
|
26
|
+
- @auriclabs/jobs@0.3.0
|
|
27
|
+
|
|
3
28
|
## 2.0.0
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -91,6 +91,16 @@ interface LambdaJobResource {
|
|
|
91
91
|
fns: FunctionWithName[]; // From @auriclabs/sst-utils
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// In-process jobs: SQS → consumer runs registered handlers directly
|
|
95
|
+
interface InProcessJobResource {
|
|
96
|
+
id: string;
|
|
97
|
+
executor: 'in-process';
|
|
98
|
+
queue: sst.aws.Queue;
|
|
99
|
+
handler: string; // wraps createRegistryExecutorHandler(...) from @auriclabs/jobs
|
|
100
|
+
link?: unknown[]; // extra linkables the handlers depend on
|
|
101
|
+
environment?: Record<string, string>;
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
// Worker jobs: SQS → custom processing (no executor subscription)
|
|
95
105
|
interface WorkerJobResource {
|
|
96
106
|
id: string;
|
|
@@ -98,7 +108,36 @@ interface WorkerJobResource {
|
|
|
98
108
|
queue: sst.aws.Queue;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
type JobResource = LambdaJobResource | WorkerJobResource;
|
|
111
|
+
type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Example `'in-process'` executor wiring:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
registerJobResources({
|
|
118
|
+
table,
|
|
119
|
+
resources: [
|
|
120
|
+
{
|
|
121
|
+
id: 'worker',
|
|
122
|
+
executor: 'in-process',
|
|
123
|
+
queue: workerJobQueue,
|
|
124
|
+
handler: 'services/job/handlers/registry-executor.handler',
|
|
125
|
+
link: [bucket],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
handlerPaths: { ... },
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// services/job/handlers/registry-executor.ts
|
|
134
|
+
import { createRegistryExecutorHandler, initJobs } from '@auriclabs/jobs';
|
|
135
|
+
import { Resource } from 'sst';
|
|
136
|
+
|
|
137
|
+
initJobs({ tableName: Resource.JobTable.name });
|
|
138
|
+
export const handler = createRegistryExecutorHandler({
|
|
139
|
+
syncItems: async (payload) => { /* ... */ },
|
|
140
|
+
});
|
|
102
141
|
```
|
|
103
142
|
|
|
104
143
|
### What `registerJobResources` sets up
|
|
@@ -114,6 +153,37 @@ type JobResource = LambdaJobResource | WorkerJobResource;
|
|
|
114
153
|
- Sets `LAMBDA_FUNCTION_LIST` env var (maps function names to ARNs)
|
|
115
154
|
- Batch config: 10 items, 3 second window, partial responses enabled
|
|
116
155
|
|
|
156
|
+
3. **In-process executor subscriptions** for each `InProcessJobResource` (`executor: 'in-process'`)
|
|
157
|
+
- Subscribes queue to the consumer-provided handler
|
|
158
|
+
- Links table, queue, and any extra `link` entries
|
|
159
|
+
- Sets `QUEUE_URL_LIST` env var (needed for scheduledAt re-enqueue and continuations)
|
|
160
|
+
- Same batch config as the Lambda executor
|
|
161
|
+
|
|
162
|
+
### `createJobsDashboard(options)`
|
|
163
|
+
|
|
164
|
+
Deploys the jobs dashboard that ships inside `@auriclabs/jobs`: an
|
|
165
|
+
`ApiGatewayV2` routing `$default` to your dashboard API handler, and a
|
|
166
|
+
`StaticSite` serving the package's `ui/` bundle with the API URL injected
|
|
167
|
+
at deploy time. Optional CloudFront basic-auth gates the static site
|
|
168
|
+
(discovery prevention only — the API itself is unauthenticated, so deploy
|
|
169
|
+
on dev/demo stages only).
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { createJobsDashboard } from '@auriclabs/jobs-infra';
|
|
173
|
+
|
|
174
|
+
if (['dev', 'demo'].includes($app.stage)) {
|
|
175
|
+
createJobsDashboard({
|
|
176
|
+
apiHandler: 'services/job/dashboard.handler', // wraps createJobsDashboardApiHandler
|
|
177
|
+
table,
|
|
178
|
+
basicAuth: { username: 'ops', password: secret.value },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Options: `apiHandler`, `table`, `link?` (extra linkables for the API fn),
|
|
184
|
+
`domain?`, `uiPath?` (override the resolved @auriclabs/jobs ui dir),
|
|
185
|
+
`basicAuth?` (`{ username, password, realm? }`). Returns `{ api, site }`.
|
|
186
|
+
|
|
117
187
|
## Full Example
|
|
118
188
|
|
|
119
189
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_module = require("node:module");
|
|
25
|
+
let node_path = require("node:path");
|
|
26
|
+
node_path = __toESM(node_path);
|
|
2
27
|
//#region src/job-table.ts
|
|
3
28
|
function createJobTable(name) {
|
|
4
29
|
return new sst.aws.Dynamo(name, {
|
|
@@ -46,7 +71,26 @@ function registerJobResources(config) {
|
|
|
46
71
|
resource.queue,
|
|
47
72
|
...resource.fns
|
|
48
73
|
],
|
|
49
|
-
environment: {
|
|
74
|
+
environment: {
|
|
75
|
+
LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),
|
|
76
|
+
QUEUE_URL_LIST
|
|
77
|
+
}
|
|
78
|
+
}, { batch: {
|
|
79
|
+
size: 10,
|
|
80
|
+
window: "3 seconds",
|
|
81
|
+
partialResponses: true
|
|
82
|
+
} });
|
|
83
|
+
if (resource.executor === "in-process") resource.queue.subscribe({
|
|
84
|
+
handler: resource.handler,
|
|
85
|
+
link: [
|
|
86
|
+
table,
|
|
87
|
+
resource.queue,
|
|
88
|
+
...resource.link ?? []
|
|
89
|
+
],
|
|
90
|
+
environment: {
|
|
91
|
+
...resource.environment,
|
|
92
|
+
QUEUE_URL_LIST
|
|
93
|
+
}
|
|
50
94
|
}, { batch: {
|
|
51
95
|
size: 10,
|
|
52
96
|
window: "3 seconds",
|
|
@@ -55,5 +99,68 @@ function registerJobResources(config) {
|
|
|
55
99
|
});
|
|
56
100
|
}
|
|
57
101
|
//#endregion
|
|
102
|
+
//#region src/dashboard.ts
|
|
103
|
+
function createJobsDashboard(options) {
|
|
104
|
+
const api = new sst.aws.ApiGatewayV2("JobsDashboardApi", { cors: true });
|
|
105
|
+
const link = [options.table, ...options.link ?? []];
|
|
106
|
+
api.route("$default", {
|
|
107
|
+
handler: options.apiHandler,
|
|
108
|
+
link
|
|
109
|
+
});
|
|
110
|
+
const uiPath = options.uiPath ?? resolveJobsUiPath();
|
|
111
|
+
const uiRelative = node_path.default.relative(process.cwd(), uiPath);
|
|
112
|
+
const basicAuthInjection = options.basicAuth ? buildBasicAuthInjection(options.basicAuth) : void 0;
|
|
113
|
+
return {
|
|
114
|
+
api,
|
|
115
|
+
site: new sst.aws.StaticSite("JobsDashboard", {
|
|
116
|
+
path: uiRelative,
|
|
117
|
+
build: {
|
|
118
|
+
command: [
|
|
119
|
+
"rm -rf _deploy",
|
|
120
|
+
"cp -r dist _deploy",
|
|
121
|
+
`sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__="'$VITE_API_URL'"<\/script>|' _deploy/index.html`,
|
|
122
|
+
"rm -f _deploy/index.html.bak"
|
|
123
|
+
].join(" && "),
|
|
124
|
+
output: "_deploy"
|
|
125
|
+
},
|
|
126
|
+
dev: {
|
|
127
|
+
command: "npx vite dev",
|
|
128
|
+
url: "http://localhost:3101"
|
|
129
|
+
},
|
|
130
|
+
environment: { VITE_API_URL: api.url },
|
|
131
|
+
domain: options.domain,
|
|
132
|
+
...basicAuthInjection && { edge: { viewerRequest: { injection: basicAuthInjection } } }
|
|
133
|
+
})
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a
|
|
138
|
+
* separate package, so the UI ships with `@auriclabs/jobs` — resolve it
|
|
139
|
+
* through the consumer's node_modules (the workspace link covers local dev).
|
|
140
|
+
*/
|
|
141
|
+
function resolveJobsUiPath() {
|
|
142
|
+
const pkgJsonPath = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve("@auriclabs/jobs/package.json");
|
|
143
|
+
const pkgRoot = node_path.default.dirname(pkgJsonPath);
|
|
144
|
+
return node_path.default.join(pkgRoot, "ui");
|
|
145
|
+
}
|
|
146
|
+
function buildBasicAuthInjection(auth) {
|
|
147
|
+
const realm = (auth.realm ?? "Jobs Dashboard").replace(/"/g, "\\\"");
|
|
148
|
+
return $output([auth.username, auth.password]).apply(([username, password]) => {
|
|
149
|
+
return [
|
|
150
|
+
"var __auth = event.request.headers.authorization && event.request.headers.authorization.value;",
|
|
151
|
+
`if (__auth !== "Basic ${Buffer.from(`${username}:${password}`).toString("base64")}") {`,
|
|
152
|
+
" return {",
|
|
153
|
+
" statusCode: 401,",
|
|
154
|
+
" statusDescription: \"Unauthorized\",",
|
|
155
|
+
" headers: {",
|
|
156
|
+
` "www-authenticate": { value: 'Basic realm="${realm}"' }`,
|
|
157
|
+
" }",
|
|
158
|
+
" };",
|
|
159
|
+
"}"
|
|
160
|
+
].join("\n");
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
58
164
|
exports.createJobTable = createJobTable;
|
|
165
|
+
exports.createJobsDashboard = createJobsDashboard;
|
|
59
166
|
exports.registerJobResources = registerJobResources;
|
package/dist/index.d.cts
CHANGED
|
@@ -10,12 +10,22 @@ interface LambdaJobResource {
|
|
|
10
10
|
queue: sst.aws.Queue;
|
|
11
11
|
fns: FunctionWithName[];
|
|
12
12
|
}
|
|
13
|
+
interface InProcessJobResource {
|
|
14
|
+
id: string;
|
|
15
|
+
executor: 'in-process';
|
|
16
|
+
queue: sst.aws.Queue;
|
|
17
|
+
/** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */
|
|
18
|
+
handler: string;
|
|
19
|
+
/** Extra linkables the in-process handlers depend on. */
|
|
20
|
+
link?: unknown[];
|
|
21
|
+
environment?: Record<string, string>;
|
|
22
|
+
}
|
|
13
23
|
interface WorkerJobResource {
|
|
14
24
|
id: string;
|
|
15
25
|
executor?: never;
|
|
16
26
|
queue: sst.aws.Queue;
|
|
17
27
|
}
|
|
18
|
-
type JobResource = LambdaJobResource | WorkerJobResource;
|
|
28
|
+
type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
|
|
19
29
|
interface RegisterJobResourcesConfig {
|
|
20
30
|
table: sst.aws.Dynamo;
|
|
21
31
|
resources: JobResource[];
|
|
@@ -26,5 +36,50 @@ interface RegisterJobResourcesConfig {
|
|
|
26
36
|
}
|
|
27
37
|
declare function registerJobResources(config: RegisterJobResourcesConfig): void;
|
|
28
38
|
//#endregion
|
|
29
|
-
|
|
39
|
+
//#region src/dashboard.d.ts
|
|
40
|
+
interface JobsDashboardBasicAuthConfig {
|
|
41
|
+
/** Plaintext username — typically wired from an SST secret. */
|
|
42
|
+
username: $util.Input<string>;
|
|
43
|
+
/** Plaintext password — typically wired from an SST secret. */
|
|
44
|
+
password: $util.Input<string>;
|
|
45
|
+
/** Realm shown in browser auth dialog. Defaults to "Jobs Dashboard". */
|
|
46
|
+
realm?: string;
|
|
47
|
+
}
|
|
48
|
+
interface JobsDashboardOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Handler path for the dashboard API Lambda. The handler should call
|
|
51
|
+
* `initJobs({ tableName: Resource.<JobTable>.name })` and export
|
|
52
|
+
* `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.
|
|
53
|
+
*/
|
|
54
|
+
apiHandler: string;
|
|
55
|
+
/** Job table — linked into the API function. */
|
|
56
|
+
table: sst.aws.Dynamo;
|
|
57
|
+
/** Extra linkables for the API function (beyond the table). */
|
|
58
|
+
link?: unknown[];
|
|
59
|
+
domain?: sst.aws.StaticSiteArgs['domain'];
|
|
60
|
+
/**
|
|
61
|
+
* Override the path to the `@auriclabs/jobs` package's `ui/` directory.
|
|
62
|
+
* Defaults to resolving the installed package via `require.resolve`.
|
|
63
|
+
*/
|
|
64
|
+
uiPath?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Optional HTTP basic auth gate on the static site. When set, injects a
|
|
67
|
+
* basic-auth check into the StaticSite's CloudFront viewer-request
|
|
68
|
+
* function — requests without the matching `Authorization: Basic <base64>`
|
|
69
|
+
* header get a 401 before any routing logic runs. Credentials are inlined
|
|
70
|
+
* into the function source at deploy time via Pulumi apply.
|
|
71
|
+
*
|
|
72
|
+
* Note: this only gates the static UI. The API gateway URL remains
|
|
73
|
+
* directly callable — browsers don't auto-send basic auth credentials
|
|
74
|
+
* cross-origin, so the dashboard's fetch() can't carry them. Treat this
|
|
75
|
+
* as discovery-prevention for the dashboard, not API protection.
|
|
76
|
+
*/
|
|
77
|
+
basicAuth?: JobsDashboardBasicAuthConfig;
|
|
78
|
+
}
|
|
79
|
+
declare function createJobsDashboard(options: JobsDashboardOptions): {
|
|
80
|
+
api: sst.aws.ApiGatewayV2;
|
|
81
|
+
site: sst.aws.StaticSite;
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { InProcessJobResource, JobResource, JobsDashboardBasicAuthConfig, JobsDashboardOptions, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, createJobsDashboard, registerJobResources };
|
|
30
85
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts"],"mappings":";;;iBAAgB,cAAA,CAAe,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCE1B,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,iBAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts","../src/dashboard.ts"],"mappings":";;;iBAAgB,cAAA,CAAe,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCE1B,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,oBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EDZ0B;ECczC,OAAA;;EAEA,IAAA;EACA,WAAA,GAAc,MAAA;AAAA;AAAA,UAGC,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,oBAAA,GAAuB,iBAAA;AAAA,UAEpD,0BAAA;EACf,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EACf,SAAA,EAAW,WAAA;EACX,YAAA;IACE,MAAA;IACA,QAAA;EAAA;AAAA;AAAA,iBAIY,oBAAA,CAAqB,MAAA,EAAQ,0BAAA;;;UClC5B,4BAAA;;EAEf,QAAA,EAAU,KAAA,CAAM,KAAA;EFLF;EEOd,QAAA,EAAU,KAAA,CAAM,KAAA;;EAEhB,KAAA;AAAA;AAAA,UAGe,oBAAA;EFZ0B;;;;;EEkBzC,UAAA;;EAEA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EDlBiB;ECoBhC,IAAA;EACA,MAAA,GAAS,GAAA,CAAI,GAAA,CAAI,cAAA;EDpBjB;;;;ECyBA,MAAA;EDvBe;;;;;AAIjB;;;;;;;ECgCE,SAAA,GAAY,4BAAA;AAAA;AAAA,iBAGE,mBAAA,CAAoB,OAAA,EAAS,oBAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -10,12 +10,22 @@ interface LambdaJobResource {
|
|
|
10
10
|
queue: sst.aws.Queue;
|
|
11
11
|
fns: FunctionWithName[];
|
|
12
12
|
}
|
|
13
|
+
interface InProcessJobResource {
|
|
14
|
+
id: string;
|
|
15
|
+
executor: 'in-process';
|
|
16
|
+
queue: sst.aws.Queue;
|
|
17
|
+
/** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */
|
|
18
|
+
handler: string;
|
|
19
|
+
/** Extra linkables the in-process handlers depend on. */
|
|
20
|
+
link?: unknown[];
|
|
21
|
+
environment?: Record<string, string>;
|
|
22
|
+
}
|
|
13
23
|
interface WorkerJobResource {
|
|
14
24
|
id: string;
|
|
15
25
|
executor?: never;
|
|
16
26
|
queue: sst.aws.Queue;
|
|
17
27
|
}
|
|
18
|
-
type JobResource = LambdaJobResource | WorkerJobResource;
|
|
28
|
+
type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
|
|
19
29
|
interface RegisterJobResourcesConfig {
|
|
20
30
|
table: sst.aws.Dynamo;
|
|
21
31
|
resources: JobResource[];
|
|
@@ -26,5 +36,50 @@ interface RegisterJobResourcesConfig {
|
|
|
26
36
|
}
|
|
27
37
|
declare function registerJobResources(config: RegisterJobResourcesConfig): void;
|
|
28
38
|
//#endregion
|
|
29
|
-
|
|
39
|
+
//#region src/dashboard.d.ts
|
|
40
|
+
interface JobsDashboardBasicAuthConfig {
|
|
41
|
+
/** Plaintext username — typically wired from an SST secret. */
|
|
42
|
+
username: $util.Input<string>;
|
|
43
|
+
/** Plaintext password — typically wired from an SST secret. */
|
|
44
|
+
password: $util.Input<string>;
|
|
45
|
+
/** Realm shown in browser auth dialog. Defaults to "Jobs Dashboard". */
|
|
46
|
+
realm?: string;
|
|
47
|
+
}
|
|
48
|
+
interface JobsDashboardOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Handler path for the dashboard API Lambda. The handler should call
|
|
51
|
+
* `initJobs({ tableName: Resource.<JobTable>.name })` and export
|
|
52
|
+
* `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.
|
|
53
|
+
*/
|
|
54
|
+
apiHandler: string;
|
|
55
|
+
/** Job table — linked into the API function. */
|
|
56
|
+
table: sst.aws.Dynamo;
|
|
57
|
+
/** Extra linkables for the API function (beyond the table). */
|
|
58
|
+
link?: unknown[];
|
|
59
|
+
domain?: sst.aws.StaticSiteArgs['domain'];
|
|
60
|
+
/**
|
|
61
|
+
* Override the path to the `@auriclabs/jobs` package's `ui/` directory.
|
|
62
|
+
* Defaults to resolving the installed package via `require.resolve`.
|
|
63
|
+
*/
|
|
64
|
+
uiPath?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Optional HTTP basic auth gate on the static site. When set, injects a
|
|
67
|
+
* basic-auth check into the StaticSite's CloudFront viewer-request
|
|
68
|
+
* function — requests without the matching `Authorization: Basic <base64>`
|
|
69
|
+
* header get a 401 before any routing logic runs. Credentials are inlined
|
|
70
|
+
* into the function source at deploy time via Pulumi apply.
|
|
71
|
+
*
|
|
72
|
+
* Note: this only gates the static UI. The API gateway URL remains
|
|
73
|
+
* directly callable — browsers don't auto-send basic auth credentials
|
|
74
|
+
* cross-origin, so the dashboard's fetch() can't carry them. Treat this
|
|
75
|
+
* as discovery-prevention for the dashboard, not API protection.
|
|
76
|
+
*/
|
|
77
|
+
basicAuth?: JobsDashboardBasicAuthConfig;
|
|
78
|
+
}
|
|
79
|
+
declare function createJobsDashboard(options: JobsDashboardOptions): {
|
|
80
|
+
api: sst.aws.ApiGatewayV2;
|
|
81
|
+
site: sst.aws.StaticSite;
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { InProcessJobResource, JobResource, JobsDashboardBasicAuthConfig, JobsDashboardOptions, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, createJobsDashboard, registerJobResources };
|
|
30
85
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts"],"mappings":";;;iBAAgB,cAAA,CAAe,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCE1B,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,iBAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts","../src/dashboard.ts"],"mappings":";;;iBAAgB,cAAA,CAAe,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCE1B,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,oBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EDZ0B;ECczC,OAAA;;EAEA,IAAA;EACA,WAAA,GAAc,MAAA;AAAA;AAAA,UAGC,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,oBAAA,GAAuB,iBAAA;AAAA,UAEpD,0BAAA;EACf,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EACf,SAAA,EAAW,WAAA;EACX,YAAA;IACE,MAAA;IACA,QAAA;EAAA;AAAA;AAAA,iBAIY,oBAAA,CAAqB,MAAA,EAAQ,0BAAA;;;UClC5B,4BAAA;;EAEf,QAAA,EAAU,KAAA,CAAM,KAAA;EFLF;EEOd,QAAA,EAAU,KAAA,CAAM,KAAA;;EAEhB,KAAA;AAAA;AAAA,UAGe,oBAAA;EFZ0B;;;;;EEkBzC,UAAA;;EAEA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EDlBiB;ECoBhC,IAAA;EACA,MAAA,GAAS,GAAA,CAAI,GAAA,CAAI,cAAA;EDpBjB;;;;ECyBA,MAAA;EDvBe;;;;;AAIjB;;;;;;;ECgCE,SAAA,GAAY,4BAAA;AAAA;AAAA,iBAGE,mBAAA,CAAoB,OAAA,EAAS,oBAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
//#region src/job-table.ts
|
|
2
4
|
function createJobTable(name) {
|
|
3
5
|
return new sst.aws.Dynamo(name, {
|
|
@@ -45,15 +47,96 @@ function registerJobResources(config) {
|
|
|
45
47
|
resource.queue,
|
|
46
48
|
...resource.fns
|
|
47
49
|
],
|
|
48
|
-
environment: {
|
|
50
|
+
environment: {
|
|
51
|
+
LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),
|
|
52
|
+
QUEUE_URL_LIST
|
|
53
|
+
}
|
|
49
54
|
}, { batch: {
|
|
50
55
|
size: 10,
|
|
51
56
|
window: "3 seconds",
|
|
52
57
|
partialResponses: true
|
|
53
58
|
} });
|
|
59
|
+
if (resource.executor === "in-process") resource.queue.subscribe({
|
|
60
|
+
handler: resource.handler,
|
|
61
|
+
link: [
|
|
62
|
+
table,
|
|
63
|
+
resource.queue,
|
|
64
|
+
...resource.link ?? []
|
|
65
|
+
],
|
|
66
|
+
environment: {
|
|
67
|
+
...resource.environment,
|
|
68
|
+
QUEUE_URL_LIST
|
|
69
|
+
}
|
|
70
|
+
}, { batch: {
|
|
71
|
+
size: 10,
|
|
72
|
+
window: "3 seconds",
|
|
73
|
+
partialResponses: true
|
|
74
|
+
} });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/dashboard.ts
|
|
79
|
+
function createJobsDashboard(options) {
|
|
80
|
+
const api = new sst.aws.ApiGatewayV2("JobsDashboardApi", { cors: true });
|
|
81
|
+
const link = [options.table, ...options.link ?? []];
|
|
82
|
+
api.route("$default", {
|
|
83
|
+
handler: options.apiHandler,
|
|
84
|
+
link
|
|
85
|
+
});
|
|
86
|
+
const uiPath = options.uiPath ?? resolveJobsUiPath();
|
|
87
|
+
const uiRelative = path.relative(process.cwd(), uiPath);
|
|
88
|
+
const basicAuthInjection = options.basicAuth ? buildBasicAuthInjection(options.basicAuth) : void 0;
|
|
89
|
+
return {
|
|
90
|
+
api,
|
|
91
|
+
site: new sst.aws.StaticSite("JobsDashboard", {
|
|
92
|
+
path: uiRelative,
|
|
93
|
+
build: {
|
|
94
|
+
command: [
|
|
95
|
+
"rm -rf _deploy",
|
|
96
|
+
"cp -r dist _deploy",
|
|
97
|
+
`sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__="'$VITE_API_URL'"<\/script>|' _deploy/index.html`,
|
|
98
|
+
"rm -f _deploy/index.html.bak"
|
|
99
|
+
].join(" && "),
|
|
100
|
+
output: "_deploy"
|
|
101
|
+
},
|
|
102
|
+
dev: {
|
|
103
|
+
command: "npx vite dev",
|
|
104
|
+
url: "http://localhost:3101"
|
|
105
|
+
},
|
|
106
|
+
environment: { VITE_API_URL: api.url },
|
|
107
|
+
domain: options.domain,
|
|
108
|
+
...basicAuthInjection && { edge: { viewerRequest: { injection: basicAuthInjection } } }
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a
|
|
114
|
+
* separate package, so the UI ships with `@auriclabs/jobs` — resolve it
|
|
115
|
+
* through the consumer's node_modules (the workspace link covers local dev).
|
|
116
|
+
*/
|
|
117
|
+
function resolveJobsUiPath() {
|
|
118
|
+
const pkgJsonPath = createRequire(import.meta.url).resolve("@auriclabs/jobs/package.json");
|
|
119
|
+
const pkgRoot = path.dirname(pkgJsonPath);
|
|
120
|
+
return path.join(pkgRoot, "ui");
|
|
121
|
+
}
|
|
122
|
+
function buildBasicAuthInjection(auth) {
|
|
123
|
+
const realm = (auth.realm ?? "Jobs Dashboard").replace(/"/g, "\\\"");
|
|
124
|
+
return $output([auth.username, auth.password]).apply(([username, password]) => {
|
|
125
|
+
return [
|
|
126
|
+
"var __auth = event.request.headers.authorization && event.request.headers.authorization.value;",
|
|
127
|
+
`if (__auth !== "Basic ${Buffer.from(`${username}:${password}`).toString("base64")}") {`,
|
|
128
|
+
" return {",
|
|
129
|
+
" statusCode: 401,",
|
|
130
|
+
" statusDescription: \"Unauthorized\",",
|
|
131
|
+
" headers: {",
|
|
132
|
+
` "www-authenticate": { value: 'Basic realm="${realm}"' }`,
|
|
133
|
+
" }",
|
|
134
|
+
" };",
|
|
135
|
+
"}"
|
|
136
|
+
].join("\n");
|
|
54
137
|
});
|
|
55
138
|
}
|
|
56
139
|
//#endregion
|
|
57
|
-
export { createJobTable, registerJobResources };
|
|
140
|
+
export { createJobTable, createJobsDashboard, registerJobResources };
|
|
58
141
|
|
|
59
142
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts"],"sourcesContent":["export function createJobTable(name: string) {\n return new sst.aws.Dynamo(name, {\n fields: {\n pk: 'string',\n sk: 'string',\n numberIndexPk: 'string',\n numberIndexSk: 'number',\n gsi1pk: 'string',\n gsi1sk: 'string',\n },\n primaryIndex: {\n hashKey: 'pk',\n rangeKey: 'sk',\n },\n globalIndexes: {\n numberIndex: { hashKey: 'numberIndexPk', rangeKey: 'numberIndexSk' },\n gsi1: { hashKey: 'gsi1pk', rangeKey: 'gsi1sk' },\n },\n stream: 'new-and-old-images',\n });\n}\n","import { FunctionWithName } from '@auriclabs/sst-utils';\n\nexport interface LambdaJobResource {\n id: string;\n executor: 'lambda';\n queue: sst.aws.Queue;\n fns: FunctionWithName[];\n}\n\nexport interface WorkerJobResource {\n id: string;\n executor?: never;\n queue: sst.aws.Queue;\n}\n\nexport type JobResource = LambdaJobResource | WorkerJobResource;\n\nexport interface RegisterJobResourcesConfig {\n table: sst.aws.Dynamo;\n resources: JobResource[];\n handlerPaths: {\n stream: string;\n executor: string;\n };\n}\n\nexport function registerJobResources(config: RegisterJobResourcesConfig) {\n const { table, resources, handlerPaths } = config;\n const QUEUE_URL_LIST = $jsonStringify(resources.map(({ id, queue }) => [id, queue.url]));\n\n table.subscribe(\n 'JobTableStream',\n {\n handler: handlerPaths.stream,\n link: [table, ...resources.map(({ queue }) => queue)],\n environment: {\n QUEUE_URL_LIST,\n },\n },\n {\n filters: [\n {\n dynamodb: {\n NewImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n {\n dynamodb: {\n OldImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n ],\n },\n );\n\n resources.forEach((resource) => {\n if (!('executor' in resource)) {\n return;\n }\n\n if (resource.executor === 'lambda') {\n resource.queue.subscribe(\n {\n handler: handlerPaths.executor,\n link: [table, resource.queue, ...resource.fns],\n environment: {\n LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),\n },\n },\n {\n batch: {\n size: 10,\n window: '3 seconds',\n partialResponses: true,\n },\n },\n );\n }\n });\n}\n"],"mappings":";AAAA,SAAgB,eAAe,MAAc;AAC3C,QAAO,IAAI,IAAI,IAAI,OAAO,MAAM;EAC9B,QAAQ;GACN,IAAI;GACJ,IAAI;GACJ,eAAe;GACf,eAAe;GACf,QAAQ;GACR,QAAQ;GACT;EACD,cAAc;GACZ,SAAS;GACT,UAAU;GACX;EACD,eAAe;GACb,aAAa;IAAE,SAAS;IAAiB,UAAU;IAAiB;GACpE,MAAM;IAAE,SAAS;IAAU,UAAU;IAAU;GAChD;EACD,QAAQ;EACT,CAAC;;;;ACOJ,SAAgB,qBAAqB,QAAoC;CACvE,MAAM,EAAE,OAAO,WAAW,iBAAiB;CAC3C,MAAM,iBAAiB,eAAe,UAAU,KAAK,EAAE,IAAI,YAAY,CAAC,IAAI,MAAM,IAAI,CAAC,CAAC;AAExF,OAAM,UACJ,kBACA;EACE,SAAS,aAAa;EACtB,MAAM,CAAC,OAAO,GAAG,UAAU,KAAK,EAAE,YAAY,MAAM,CAAC;EACrD,aAAa,EACX,gBACD;EACF,EACD,EACE,SAAS,CACP,EACE,UAAU,EACR,UAAU,EACR,WAAW,EACT,GAAG,CAAC,cAAc,EACnB,EACF,EACF,EACF,EACD,EACE,UAAU,EACR,UAAU,EACR,WAAW,EACT,GAAG,CAAC,cAAc,EACnB,EACF,EACF,EACF,CACF,EACF,CACF;AAED,WAAU,SAAS,aAAa;AAC9B,MAAI,EAAE,cAAc,UAClB;AAGF,MAAI,SAAS,aAAa,SACxB,UAAS,MAAM,UACb;GACE,SAAS,aAAa;GACtB,MAAM;IAAC;IAAO,SAAS;IAAO,GAAG,SAAS;IAAI;GAC9C,aAAa,EACX,sBAAsB,eAAe,SAAS,IAAI,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,EAC/E;GACF,EACD,EACE,OAAO;GACL,MAAM;GACN,QAAQ;GACR,kBAAkB;GACnB,EACF,CACF;GAEH"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts","../src/dashboard.ts"],"sourcesContent":["export function createJobTable(name: string) {\n return new sst.aws.Dynamo(name, {\n fields: {\n pk: 'string',\n sk: 'string',\n numberIndexPk: 'string',\n numberIndexSk: 'number',\n gsi1pk: 'string',\n gsi1sk: 'string',\n },\n primaryIndex: {\n hashKey: 'pk',\n rangeKey: 'sk',\n },\n globalIndexes: {\n numberIndex: { hashKey: 'numberIndexPk', rangeKey: 'numberIndexSk' },\n gsi1: { hashKey: 'gsi1pk', rangeKey: 'gsi1sk' },\n },\n stream: 'new-and-old-images',\n });\n}\n","import { FunctionWithName } from '@auriclabs/sst-utils';\n\nexport interface LambdaJobResource {\n id: string;\n executor: 'lambda';\n queue: sst.aws.Queue;\n fns: FunctionWithName[];\n}\n\nexport interface InProcessJobResource {\n id: string;\n executor: 'in-process';\n queue: sst.aws.Queue;\n /** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */\n handler: string;\n /** Extra linkables the in-process handlers depend on. */\n link?: unknown[];\n environment?: Record<string, string>;\n}\n\nexport interface WorkerJobResource {\n id: string;\n executor?: never;\n queue: sst.aws.Queue;\n}\n\nexport type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;\n\nexport interface RegisterJobResourcesConfig {\n table: sst.aws.Dynamo;\n resources: JobResource[];\n handlerPaths: {\n stream: string;\n executor: string;\n };\n}\n\nexport function registerJobResources(config: RegisterJobResourcesConfig) {\n const { table, resources, handlerPaths } = config;\n const QUEUE_URL_LIST = $jsonStringify(resources.map(({ id, queue }) => [id, queue.url]));\n\n table.subscribe(\n 'JobTableStream',\n {\n handler: handlerPaths.stream,\n link: [table, ...resources.map(({ queue }) => queue)],\n environment: {\n QUEUE_URL_LIST,\n },\n },\n {\n filters: [\n {\n dynamodb: {\n NewImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n {\n dynamodb: {\n OldImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n ],\n },\n );\n\n resources.forEach((resource) => {\n if (!('executor' in resource)) {\n return;\n }\n\n if (resource.executor === 'lambda') {\n resource.queue.subscribe(\n {\n handler: handlerPaths.executor,\n link: [table, resource.queue, ...resource.fns],\n environment: {\n LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),\n // needed for startJob's scheduledAt re-enqueue\n QUEUE_URL_LIST,\n },\n },\n {\n batch: {\n size: 10,\n window: '3 seconds',\n partialResponses: true,\n },\n },\n );\n }\n\n if (resource.executor === 'in-process') {\n resource.queue.subscribe(\n {\n handler: resource.handler,\n link: [table, resource.queue, ...(resource.link ?? [])],\n environment: {\n ...resource.environment,\n // in-process executors re-enqueue for scheduledAt deferrals and\n // continuations — must win over consumer-provided environment\n QUEUE_URL_LIST,\n },\n },\n {\n batch: {\n size: 10,\n window: '3 seconds',\n partialResponses: true,\n },\n },\n );\n }\n });\n}\n","import { createRequire } from 'node:module';\nimport path from 'node:path';\n\nexport interface JobsDashboardBasicAuthConfig {\n /** Plaintext username — typically wired from an SST secret. */\n username: $util.Input<string>;\n /** Plaintext password — typically wired from an SST secret. */\n password: $util.Input<string>;\n /** Realm shown in browser auth dialog. Defaults to \"Jobs Dashboard\". */\n realm?: string;\n}\n\nexport interface JobsDashboardOptions {\n /**\n * Handler path for the dashboard API Lambda. The handler should call\n * `initJobs({ tableName: Resource.<JobTable>.name })` and export\n * `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.\n */\n apiHandler: string;\n /** Job table — linked into the API function. */\n table: sst.aws.Dynamo;\n /** Extra linkables for the API function (beyond the table). */\n link?: unknown[];\n domain?: sst.aws.StaticSiteArgs['domain'];\n /**\n * Override the path to the `@auriclabs/jobs` package's `ui/` directory.\n * Defaults to resolving the installed package via `require.resolve`.\n */\n uiPath?: string;\n /**\n * Optional HTTP basic auth gate on the static site. When set, injects a\n * basic-auth check into the StaticSite's CloudFront viewer-request\n * function — requests without the matching `Authorization: Basic <base64>`\n * header get a 401 before any routing logic runs. Credentials are inlined\n * into the function source at deploy time via Pulumi apply.\n *\n * Note: this only gates the static UI. The API gateway URL remains\n * directly callable — browsers don't auto-send basic auth credentials\n * cross-origin, so the dashboard's fetch() can't carry them. Treat this\n * as discovery-prevention for the dashboard, not API protection.\n */\n basicAuth?: JobsDashboardBasicAuthConfig;\n}\n\nexport function createJobsDashboard(options: JobsDashboardOptions) {\n const api = new sst.aws.ApiGatewayV2('JobsDashboardApi', { cors: true });\n\n const link: unknown[] = [options.table, ...(options.link ?? [])];\n\n api.route('$default', {\n handler: options.apiHandler,\n link,\n });\n\n const uiPath = options.uiPath ?? resolveJobsUiPath();\n const uiRelative = path.relative(process.cwd(), uiPath);\n\n const basicAuthInjection = options.basicAuth\n ? buildBasicAuthInjection(options.basicAuth)\n : undefined;\n\n // Copy pre-built dist and inject the API URL at deploy time.\n // The build command copies ui/dist to a temp output dir and injects\n // a script tag that sets globalThis.__JOBS_API_URL__ before the app loads.\n const site = new sst.aws.StaticSite('JobsDashboard', {\n path: uiRelative,\n build: {\n command: [\n // _deploy persists in node_modules across deploys — a stale copy would\n // nest the new dist and keep serving the old bundle\n 'rm -rf _deploy',\n 'cp -r dist _deploy',\n `sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__=\"'$VITE_API_URL'\"</script>|' _deploy/index.html`,\n 'rm -f _deploy/index.html.bak',\n ].join(' && '),\n output: '_deploy',\n },\n dev: {\n command: 'npx vite dev',\n url: 'http://localhost:3101',\n },\n environment: {\n VITE_API_URL: api.url,\n },\n domain: options.domain,\n ...(basicAuthInjection && {\n edge: {\n viewerRequest: { injection: basicAuthInjection },\n },\n }),\n });\n\n return { api, site };\n}\n\n/**\n * Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a\n * separate package, so the UI ships with `@auriclabs/jobs` — resolve it\n * through the consumer's node_modules (the workspace link covers local dev).\n */\nfunction resolveJobsUiPath(): string {\n const require = createRequire(import.meta.url);\n const pkgJsonPath = require.resolve('@auriclabs/jobs/package.json');\n const pkgRoot = path.dirname(pkgJsonPath);\n return path.join(pkgRoot, 'ui');\n}\n\nfunction buildBasicAuthInjection(auth: JobsDashboardBasicAuthConfig) {\n const realm = (auth.realm ?? 'Jobs Dashboard').replace(/\"/g, '\\\\\"');\n\n // Inline the encoded credential into the CloudFront Function source at\n // deploy time. CFFs can't read SSM at runtime, so the credential lives\n // in the deployed function's code (same trust boundary as SST secret\n // state). Rotate by updating the upstream secret and redeploying.\n //\n // Returned as an injection string that SST splices into the start of\n // its existing `cloudfront-js-2.0` viewer-request handler — a 401\n // return short-circuits the rest of the routing logic.\n return $output([auth.username, auth.password]).apply(([username, password]) => {\n const encoded = Buffer.from(`${username}:${password}`).toString('base64');\n return [\n 'var __auth = event.request.headers.authorization && event.request.headers.authorization.value;',\n `if (__auth !== \"Basic ${encoded}\") {`,\n ' return {',\n ' statusCode: 401,',\n ' statusDescription: \"Unauthorized\",',\n ' headers: {',\n ` \"www-authenticate\": { value: 'Basic realm=\"${realm}\"' }`,\n ' }',\n ' };',\n '}',\n ].join('\\n');\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@auriclabs/jobs-infra",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "SST infrastructure helpers for job queue tables and resources",
|
|
5
5
|
"prettier": "@auriclabs/prettier-config",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -20,14 +20,21 @@
|
|
|
20
20
|
"dependencies": {},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"sst": "^4.3.7",
|
|
23
|
-
"@auriclabs/
|
|
23
|
+
"@auriclabs/jobs": "0.3.0",
|
|
24
|
+
"@auriclabs/sst-utils": "1.1.14",
|
|
24
25
|
"@auriclabs/sst-types": "0.1.0"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
28
|
+
"@auriclabs/jobs": ">=0.3.0",
|
|
27
29
|
"@auriclabs/sst-types": "^0.1.0",
|
|
28
30
|
"@auriclabs/sst-utils": "^1.2.0",
|
|
29
31
|
"sst": "^4.3.7"
|
|
30
32
|
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@auriclabs/jobs": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
31
38
|
"publishConfig": {
|
|
32
39
|
"registry": "https://registry.npmjs.org/"
|
|
33
40
|
},
|
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface JobsDashboardBasicAuthConfig {
|
|
5
|
+
/** Plaintext username — typically wired from an SST secret. */
|
|
6
|
+
username: $util.Input<string>;
|
|
7
|
+
/** Plaintext password — typically wired from an SST secret. */
|
|
8
|
+
password: $util.Input<string>;
|
|
9
|
+
/** Realm shown in browser auth dialog. Defaults to "Jobs Dashboard". */
|
|
10
|
+
realm?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface JobsDashboardOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Handler path for the dashboard API Lambda. The handler should call
|
|
16
|
+
* `initJobs({ tableName: Resource.<JobTable>.name })` and export
|
|
17
|
+
* `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.
|
|
18
|
+
*/
|
|
19
|
+
apiHandler: string;
|
|
20
|
+
/** Job table — linked into the API function. */
|
|
21
|
+
table: sst.aws.Dynamo;
|
|
22
|
+
/** Extra linkables for the API function (beyond the table). */
|
|
23
|
+
link?: unknown[];
|
|
24
|
+
domain?: sst.aws.StaticSiteArgs['domain'];
|
|
25
|
+
/**
|
|
26
|
+
* Override the path to the `@auriclabs/jobs` package's `ui/` directory.
|
|
27
|
+
* Defaults to resolving the installed package via `require.resolve`.
|
|
28
|
+
*/
|
|
29
|
+
uiPath?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional HTTP basic auth gate on the static site. When set, injects a
|
|
32
|
+
* basic-auth check into the StaticSite's CloudFront viewer-request
|
|
33
|
+
* function — requests without the matching `Authorization: Basic <base64>`
|
|
34
|
+
* header get a 401 before any routing logic runs. Credentials are inlined
|
|
35
|
+
* into the function source at deploy time via Pulumi apply.
|
|
36
|
+
*
|
|
37
|
+
* Note: this only gates the static UI. The API gateway URL remains
|
|
38
|
+
* directly callable — browsers don't auto-send basic auth credentials
|
|
39
|
+
* cross-origin, so the dashboard's fetch() can't carry them. Treat this
|
|
40
|
+
* as discovery-prevention for the dashboard, not API protection.
|
|
41
|
+
*/
|
|
42
|
+
basicAuth?: JobsDashboardBasicAuthConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createJobsDashboard(options: JobsDashboardOptions) {
|
|
46
|
+
const api = new sst.aws.ApiGatewayV2('JobsDashboardApi', { cors: true });
|
|
47
|
+
|
|
48
|
+
const link: unknown[] = [options.table, ...(options.link ?? [])];
|
|
49
|
+
|
|
50
|
+
api.route('$default', {
|
|
51
|
+
handler: options.apiHandler,
|
|
52
|
+
link,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const uiPath = options.uiPath ?? resolveJobsUiPath();
|
|
56
|
+
const uiRelative = path.relative(process.cwd(), uiPath);
|
|
57
|
+
|
|
58
|
+
const basicAuthInjection = options.basicAuth
|
|
59
|
+
? buildBasicAuthInjection(options.basicAuth)
|
|
60
|
+
: undefined;
|
|
61
|
+
|
|
62
|
+
// Copy pre-built dist and inject the API URL at deploy time.
|
|
63
|
+
// The build command copies ui/dist to a temp output dir and injects
|
|
64
|
+
// a script tag that sets globalThis.__JOBS_API_URL__ before the app loads.
|
|
65
|
+
const site = new sst.aws.StaticSite('JobsDashboard', {
|
|
66
|
+
path: uiRelative,
|
|
67
|
+
build: {
|
|
68
|
+
command: [
|
|
69
|
+
// _deploy persists in node_modules across deploys — a stale copy would
|
|
70
|
+
// nest the new dist and keep serving the old bundle
|
|
71
|
+
'rm -rf _deploy',
|
|
72
|
+
'cp -r dist _deploy',
|
|
73
|
+
`sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__="'$VITE_API_URL'"</script>|' _deploy/index.html`,
|
|
74
|
+
'rm -f _deploy/index.html.bak',
|
|
75
|
+
].join(' && '),
|
|
76
|
+
output: '_deploy',
|
|
77
|
+
},
|
|
78
|
+
dev: {
|
|
79
|
+
command: 'npx vite dev',
|
|
80
|
+
url: 'http://localhost:3101',
|
|
81
|
+
},
|
|
82
|
+
environment: {
|
|
83
|
+
VITE_API_URL: api.url,
|
|
84
|
+
},
|
|
85
|
+
domain: options.domain,
|
|
86
|
+
...(basicAuthInjection && {
|
|
87
|
+
edge: {
|
|
88
|
+
viewerRequest: { injection: basicAuthInjection },
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { api, site };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a
|
|
98
|
+
* separate package, so the UI ships with `@auriclabs/jobs` — resolve it
|
|
99
|
+
* through the consumer's node_modules (the workspace link covers local dev).
|
|
100
|
+
*/
|
|
101
|
+
function resolveJobsUiPath(): string {
|
|
102
|
+
const require = createRequire(import.meta.url);
|
|
103
|
+
const pkgJsonPath = require.resolve('@auriclabs/jobs/package.json');
|
|
104
|
+
const pkgRoot = path.dirname(pkgJsonPath);
|
|
105
|
+
return path.join(pkgRoot, 'ui');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildBasicAuthInjection(auth: JobsDashboardBasicAuthConfig) {
|
|
109
|
+
const realm = (auth.realm ?? 'Jobs Dashboard').replace(/"/g, '\\"');
|
|
110
|
+
|
|
111
|
+
// Inline the encoded credential into the CloudFront Function source at
|
|
112
|
+
// deploy time. CFFs can't read SSM at runtime, so the credential lives
|
|
113
|
+
// in the deployed function's code (same trust boundary as SST secret
|
|
114
|
+
// state). Rotate by updating the upstream secret and redeploying.
|
|
115
|
+
//
|
|
116
|
+
// Returned as an injection string that SST splices into the start of
|
|
117
|
+
// its existing `cloudfront-js-2.0` viewer-request handler — a 401
|
|
118
|
+
// return short-circuits the rest of the routing logic.
|
|
119
|
+
return $output([auth.username, auth.password]).apply(([username, password]) => {
|
|
120
|
+
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
|
|
121
|
+
return [
|
|
122
|
+
'var __auth = event.request.headers.authorization && event.request.headers.authorization.value;',
|
|
123
|
+
`if (__auth !== "Basic ${encoded}") {`,
|
|
124
|
+
' return {',
|
|
125
|
+
' statusCode: 401,',
|
|
126
|
+
' statusDescription: "Unauthorized",',
|
|
127
|
+
' headers: {',
|
|
128
|
+
` "www-authenticate": { value: 'Basic realm="${realm}"' }`,
|
|
129
|
+
' }',
|
|
130
|
+
' };',
|
|
131
|
+
'}',
|
|
132
|
+
].join('\n');
|
|
133
|
+
});
|
|
134
|
+
}
|
package/src/index.ts
CHANGED
package/src/job-resources.ts
CHANGED
|
@@ -7,13 +7,24 @@ export interface LambdaJobResource {
|
|
|
7
7
|
fns: FunctionWithName[];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface InProcessJobResource {
|
|
11
|
+
id: string;
|
|
12
|
+
executor: 'in-process';
|
|
13
|
+
queue: sst.aws.Queue;
|
|
14
|
+
/** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */
|
|
15
|
+
handler: string;
|
|
16
|
+
/** Extra linkables the in-process handlers depend on. */
|
|
17
|
+
link?: unknown[];
|
|
18
|
+
environment?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export interface WorkerJobResource {
|
|
11
22
|
id: string;
|
|
12
23
|
executor?: never;
|
|
13
24
|
queue: sst.aws.Queue;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
export type JobResource = LambdaJobResource | WorkerJobResource;
|
|
27
|
+
export type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
|
|
17
28
|
|
|
18
29
|
export interface RegisterJobResourcesConfig {
|
|
19
30
|
table: sst.aws.Dynamo;
|
|
@@ -73,6 +84,30 @@ export function registerJobResources(config: RegisterJobResourcesConfig) {
|
|
|
73
84
|
link: [table, resource.queue, ...resource.fns],
|
|
74
85
|
environment: {
|
|
75
86
|
LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),
|
|
87
|
+
// needed for startJob's scheduledAt re-enqueue
|
|
88
|
+
QUEUE_URL_LIST,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
batch: {
|
|
93
|
+
size: 10,
|
|
94
|
+
window: '3 seconds',
|
|
95
|
+
partialResponses: true,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (resource.executor === 'in-process') {
|
|
102
|
+
resource.queue.subscribe(
|
|
103
|
+
{
|
|
104
|
+
handler: resource.handler,
|
|
105
|
+
link: [table, resource.queue, ...(resource.link ?? [])],
|
|
106
|
+
environment: {
|
|
107
|
+
...resource.environment,
|
|
108
|
+
// in-process executors re-enqueue for scheduledAt deferrals and
|
|
109
|
+
// continuations — must win over consumer-provided environment
|
|
110
|
+
QUEUE_URL_LIST,
|
|
76
111
|
},
|
|
77
112
|
},
|
|
78
113
|
{
|