@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @auriclabs/jobs-infra@2.0.0 build /home/runner/work/packages/packages/packages/jobs-infra
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
  [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.
@@ -7,19 +7,25 @@
7
7
  ℹ entry: src/index.ts
8
8
  ℹ tsconfig: tsconfig.build.json
9
9
  ℹ Build start
10
- ℹ [CJS] dist/index.cjs 1.67 kB │ gzip: 0.76 kB
11
- ℹ [CJS] 1 files, total: 1.67 kB
12
- ℹ [CJS] dist/index.d.cts.map 0.55 kB │ gzip: 0.27 kB
13
- ℹ [CJS] dist/index.d.cts 0.88 kB │ gzip: 0.39 kB
14
- ℹ [CJS] 2 files, total: 1.43 kB
15
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
10
+ ℹ [CJS] dist/index.cjs 5.43 kB │ gzip: 2.21 kB
11
+ ℹ [CJS] 1 files, total: 5.43 kB
12
+ ℹ [CJS] dist/index.d.cts.map 1.06 kB │ gzip: 0.43 kB
13
+ ℹ [CJS] dist/index.d.cts 3.21 kB │ gzip: 1.30 kB
14
+ ℹ [CJS] 2 files, total: 4.27 kB
15
+ [PLUGIN_TIMINGS] Warning: 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
- ℹ [ESM] dist/index.mjs 1.58 kB │ gzip: 0.73 kB
18
- ℹ [ESM] dist/index.mjs.map 3.73 kB │ gzip: 1.35 kB
19
- ℹ [ESM] dist/index.d.mts.map 0.55 kB │ gzip: 0.27 kB
20
- ℹ [ESM] dist/index.d.mts 0.88 kB │ gzip: 0.38 kB
21
- ℹ [ESM] 4 files, total: 6.74 kB
22
- ✔ Build complete in 7174ms
23
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
20
+ ✔ Build complete in 5776ms
21
+ ℹ [ESM] dist/index.mjs  4.22 kB │ gzip: 1.75 kB
22
+ ℹ [ESM] dist/index.mjs.map 11.40 kB │ gzip: 4.08 kB
23
+ ℹ [ESM] dist/index.d.mts.map  1.06 kB │ gzip: 0.43 kB
24
+ ℹ [ESM] dist/index.d.mts  3.21 kB │ gzip: 1.30 kB
25
+ ℹ [ESM] 4 files, total: 19.89 kB
26
+ [PLUGIN_TIMINGS] Warning: 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
- ✔ Build complete in 7175ms
31
+ ✔ Build complete in 5779ms
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: { LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])) }
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
- export { JobResource, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, registerJobResources };
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
@@ -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,UAE7B,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"}
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
- export { JobResource, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, registerJobResources };
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
@@ -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,UAE7B,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"}
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: { LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])) }
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
@@ -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.0.0",
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/sst-utils": "1.1.7",
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
  },
@@ -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
@@ -1,2 +1,3 @@
1
1
  export * from './job-table';
2
2
  export * from './job-resources';
3
+ export * from './dashboard';
@@ -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
  {