@shipfox/api-runners 7.0.2 → 8.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-runners",
3
3
  "license": "MIT",
4
- "version": "7.0.2",
4
+ "version": "8.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -25,21 +25,22 @@
25
25
  "@temporalio/workflow": "1.18.1",
26
26
  "drizzle-orm": "^0.45.2",
27
27
  "zod": "^4.4.3",
28
- "@shipfox/api-auth-context": "6.0.0",
29
- "@shipfox/api-auth-dto": "6.0.0",
28
+ "@shipfox/api-auth-context": "7.1.0",
29
+ "@shipfox/api-auth-dto": "7.1.0",
30
30
  "@shipfox/api-runners-dto": "7.0.1",
31
- "@shipfox/api-workflows-dto": "6.0.0",
31
+ "@shipfox/api-workflows-dto": "8.0.0",
32
32
  "@shipfox/config": "1.2.2",
33
33
  "@shipfox/inter-module": "0.2.0",
34
34
  "@shipfox/node-drizzle": "0.3.2",
35
+ "@shipfox/node-error-monitoring": "0.2.0",
35
36
  "@shipfox/node-auth-root-key": "0.2.1",
36
- "@shipfox/node-fastify": "0.2.4",
37
- "@shipfox/node-module": "0.4.0",
38
- "@shipfox/node-opentelemetry": "0.5.2",
37
+ "@shipfox/node-fastify": "0.3.0",
38
+ "@shipfox/node-module": "0.5.0",
39
+ "@shipfox/node-opentelemetry": "0.6.0",
39
40
  "@shipfox/node-outbox": "0.2.4",
40
41
  "@shipfox/node-postgres": "0.4.2",
41
42
  "@shipfox/node-rate-limit": "0.3.0",
42
- "@shipfox/node-temporal": "0.3.2",
43
+ "@shipfox/node-temporal": "0.4.0",
43
44
  "@shipfox/node-tokens": "0.3.0",
44
45
  "@shipfox/runner-labels": "0.1.1"
45
46
  },
@@ -1,4 +1,3 @@
1
- import {verifyRunnerSessionToken} from '@shipfox/api-auth';
2
1
  import {and, eq} from 'drizzle-orm';
3
2
  import {db} from '#db/db.js';
4
3
  import {ephemeralRegistrationTokens} from '#db/schema/ephemeral-registration-tokens.js';
@@ -7,6 +6,7 @@ import {providerRunners} from '#db/schema/runner-instances.js';
7
6
  import {runnerSessions} from '#db/schema/runner-sessions.js';
8
7
  import {
9
8
  ephemeralRegistrationTokenFactory,
9
+ getRunnerSessionTokenClaims,
10
10
  manualRegistrationTokenFactory,
11
11
  providerRunnerFactory,
12
12
  runnersTestAuthClient,
@@ -51,7 +51,7 @@ describe('registerRunnerSession', () => {
51
51
  .where(eq(runnerSessions.id, result.session.id));
52
52
  expect(rows[0]?.labels).toEqual(['linux', 'x64']);
53
53
 
54
- const claims = await verifyRunnerSessionToken(result.sessionToken);
54
+ const claims = getRunnerSessionTokenClaims(result.sessionToken);
55
55
  expect(claims?.labels).toEqual(['linux', 'x64']);
56
56
  expect(claims?.maxClaims).toBeNull();
57
57
  });
@@ -104,7 +104,7 @@ describe('registerRunnerSession', () => {
104
104
  expect(session?.provisionerId).toBe(token.provisionerId);
105
105
  expect(session?.providerRunnerId).toBe(token.providerRunnerId);
106
106
 
107
- const claims = await verifyRunnerSessionToken(result.sessionToken);
107
+ const claims = getRunnerSessionTokenClaims(result.sessionToken);
108
108
  expect(claims?.maxClaims).toBe(1);
109
109
  });
110
110
 
@@ -1,4 +1,3 @@
1
- import {verifyJobLeaseToken} from '@shipfox/api-auth';
2
1
  import {
3
2
  RUNNER_JOB_CLAIMED,
4
3
  RUNNER_JOB_LEASE_EXPIRED,
@@ -8,7 +7,12 @@ import {eq, sql} from 'drizzle-orm';
8
7
  import {EmptyRequiredLabelsError, RunnerSessionExhaustedError} from '#core/errors.js';
9
8
  import {claimJobExecution} from '#core/job-executions.js';
10
9
  import {detectAndExpireStuckJobs} from '#core/maintenance.js';
11
- import {pendingJobFactory, runnerSessionFactory, runnersTestAuthClient} from '#test/index.js';
10
+ import {
11
+ getLeaseTokenClaims,
12
+ pendingJobFactory,
13
+ runnerSessionFactory,
14
+ runnersTestAuthClient,
15
+ } from '#test/index.js';
12
16
  import {db} from './db.js';
13
17
  import {
14
18
  cancelRunnerJobs,
@@ -770,7 +774,7 @@ describe('claimJobExecution', () => {
770
774
  expect(claimed?.workflowRunAttemptId).toBe(created.workflowRunAttemptId);
771
775
  expect(claimed).not.toHaveProperty('steps');
772
776
 
773
- const claims = await verifyJobLeaseToken(claimed?.leaseToken as string);
777
+ const claims = getLeaseTokenClaims(claimed?.leaseToken as string);
774
778
  expect(claims).toMatchObject({
775
779
  jobId: created.jobId,
776
780
  workflowRunAttemptId: created.workflowRunAttemptId,
@@ -1822,6 +1822,53 @@ describe('reconcileRunnerInstances', () => {
1822
1822
  });
1823
1823
 
1824
1824
  describe('runner instance provider attachment', () => {
1825
+ it('updates the pre-created runner instance before its first lifecycle report', async () => {
1826
+ const provisionerId = crypto.randomUUID();
1827
+ const providerRunnerId = 'ec2-runner-1';
1828
+ const [instance] = await db()
1829
+ .insert(providerRunners)
1830
+ .values({
1831
+ provisionerId,
1832
+ providerKind: 'ec2',
1833
+ templateKey: 'linux',
1834
+ labels: ['linux'],
1835
+ state: 'starting',
1836
+ reportedAt: new Date(),
1837
+ })
1838
+ .returning({id: providerRunners.id});
1839
+ if (!instance) throw new Error('Runner instance insert returned no row');
1840
+
1841
+ const attached = await attachRunnerInstanceProviderId({
1842
+ runnerInstanceId: instance.id,
1843
+ provisionerId,
1844
+ providerRunnerId,
1845
+ });
1846
+ await reportRunnerInstances({
1847
+ workspaceId: null,
1848
+ provisionerId,
1849
+ events: [
1850
+ {
1851
+ providerRunnerId,
1852
+ reservationId: null,
1853
+ templateKey: 'linux',
1854
+ labels: ['linux'],
1855
+ state: 'starting',
1856
+ reason: null,
1857
+ runnerSessionId: null,
1858
+ providerKind: 'ec2',
1859
+ reportedAt: new Date(),
1860
+ },
1861
+ ],
1862
+ });
1863
+ const rows = await db()
1864
+ .select()
1865
+ .from(providerRunners)
1866
+ .where(eq(providerRunners.provisionerId, provisionerId));
1867
+
1868
+ expect(attached).toBe(true);
1869
+ expect(rows).toMatchObject([{id: instance.id, providerRunnerId, state: 'starting'}]);
1870
+ });
1871
+
1825
1872
  it('belongs to its provisioner until it receives one provider runner identity', async () => {
1826
1873
  const provisionerId = crypto.randomUUID();
1827
1874
  const [instance] = await db()
@@ -1,10 +1,3 @@
1
- import {
2
- createLeaseTokenAuthMethod,
3
- createRunnerSessionAuthMethod,
4
- issueJobLeaseToken,
5
- jobLeaseParamsFrom,
6
- verifyJobLeaseToken,
7
- } from '@shipfox/api-auth';
8
1
  import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
9
2
  import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
10
3
  import type {AuthMethod} from '@shipfox/node-fastify';
@@ -17,7 +10,15 @@ import {requestJobExecutionCancellation} from '#db/job-executions.js';
17
10
  import {runnerSessions} from '#db/schema/runner-sessions.js';
18
11
  import {runningJobExecutions} from '#db/schema/running-job-executions.js';
19
12
  import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
20
- import {pendingJobFactory, runnerSessionFactory, runnersTestAuthClient} from '#test/index.js';
13
+ import {
14
+ fakeLeaseTokenAuthMethod,
15
+ fakeRunnerSessionAuthMethod,
16
+ getLeaseTokenClaims,
17
+ mintLeaseToken,
18
+ pendingJobFactory,
19
+ runnerSessionFactory,
20
+ runnersTestAuthClient,
21
+ } from '#test/index.js';
21
22
  import {createRunnerRoutes} from './index.js';
22
23
 
23
24
  const fakeUserAuth: AuthMethod = {
@@ -53,8 +54,8 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
53
54
  auth: [
54
55
  fakeUserAuth,
55
56
  createRunnerRegistrationTokenAuthMethod(),
56
- createRunnerSessionAuthMethod(),
57
- createLeaseTokenAuthMethod(),
57
+ fakeRunnerSessionAuthMethod,
58
+ fakeLeaseTokenAuthMethod,
58
59
  fakeProvisionerAuth,
59
60
  ],
60
61
  routes: createRunnerRoutes(runnersTestAuthClient),
@@ -120,7 +121,7 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
120
121
  expect(res.statusCode).toBe(200);
121
122
  const body = res.json<{cancel: boolean; lease_token: string}>();
122
123
  expect(body).toEqual({cancel: false, lease_token: expect.any(String)});
123
- const refreshedLease = await verifyJobLeaseToken(body.lease_token);
124
+ const refreshedLease = getLeaseTokenClaims(body.lease_token);
124
125
  expect(refreshedLease).toMatchObject({
125
126
  jobId,
126
127
  jobExecutionId,
@@ -236,7 +237,7 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
236
237
  expect(res.statusCode).toBe(200);
237
238
  const body = res.json<{cancel: boolean; lease_token: string}>();
238
239
  expect(body).toEqual({cancel: true, lease_token: expect.any(String)});
239
- const refreshedLease = await verifyJobLeaseToken(body.lease_token);
240
+ const refreshedLease = getLeaseTokenClaims(body.lease_token);
240
241
  expect(refreshedLease).toMatchObject({
241
242
  jobId,
242
243
  jobExecutionId,
@@ -248,10 +249,10 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
248
249
 
249
250
  it('preserves current step scope when renewing a step-scoped lease', async () => {
250
251
  const {jobId, leaseToken} = await claimAvailableJob();
251
- const lease = await verifyJobLeaseToken(leaseToken);
252
+ const lease = getLeaseTokenClaims(leaseToken);
252
253
  if (!lease) throw new Error('Expected valid lease token');
253
254
  const stepScope = {currentStepId: crypto.randomUUID(), currentStepAttempt: 3};
254
- const stepScopedLeaseToken = await issueJobLeaseToken(jobLeaseParamsFrom(lease, stepScope));
255
+ const stepScopedLeaseToken = mintLeaseToken({...lease, ...stepScope});
255
256
 
256
257
  const res = await app.inject({
257
258
  method: 'POST',
@@ -261,14 +262,14 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
261
262
 
262
263
  expect(res.statusCode).toBe(200);
263
264
  const body = res.json<{cancel: boolean; lease_token: string}>();
264
- const refreshedLease = await verifyJobLeaseToken(body.lease_token);
265
+ const refreshedLease = getLeaseTokenClaims(body.lease_token);
265
266
  expect(refreshedLease).toMatchObject(stepScope);
266
267
  });
267
268
 
268
269
  it('returns 404 when the jobId is unknown', async () => {
269
270
  const jobId = crypto.randomUUID();
270
271
  const jobExecutionId = crypto.randomUUID();
271
- const leaseToken = await issueJobLeaseToken({
272
+ const leaseToken = mintLeaseToken({
272
273
  jobId,
273
274
  jobExecutionId,
274
275
  workflowRunId: crypto.randomUUID(),
@@ -291,7 +292,7 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
291
292
  it('returns 404 when the job belongs to a different session', async () => {
292
293
  const {jobId, jobExecutionId} = await claimAvailableJob();
293
294
  const otherSession = await runnerSessionFactory.create({workspaceId});
294
- const leaseToken = await issueJobLeaseToken({
295
+ const leaseToken = mintLeaseToken({
295
296
  jobId,
296
297
  jobExecutionId,
297
298
  workflowRunId: crypto.randomUUID(),
@@ -1,4 +1,3 @@
1
- import {createLeaseTokenAuthMethod, createRunnerSessionAuthMethod} from '@shipfox/api-auth';
2
1
  import {
3
2
  AUTH_LEASED_JOB,
4
3
  AUTH_PROVISIONER_TOKEN,
@@ -18,7 +17,12 @@ import {db} from '#db/db.js';
18
17
  import {revokeManualRegistrationToken} from '#db/manual-registration-tokens.js';
19
18
  import {manualRegistrationTokens} from '#db/schema/manual-registration-tokens.js';
20
19
  import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
21
- import {manualRegistrationTokenFactory, runnersTestAuthClient} from '#test/index.js';
20
+ import {
21
+ fakeLeaseTokenAuthMethod,
22
+ fakeRunnerSessionAuthMethod,
23
+ manualRegistrationTokenFactory,
24
+ runnersTestAuthClient,
25
+ } from '#test/index.js';
22
26
  import {createRunnerRoutes} from './index.js';
23
27
 
24
28
  let authenticatedMemberships: ReadonlyArray<UserContextMembership> = [];
@@ -59,8 +63,8 @@ describe('manual registration token routes', () => {
59
63
  auth: [
60
64
  fakeUserAuth,
61
65
  createRunnerRegistrationTokenAuthMethod(),
62
- createRunnerSessionAuthMethod(),
63
- createLeaseTokenAuthMethod(),
66
+ fakeRunnerSessionAuthMethod,
67
+ fakeLeaseTokenAuthMethod,
64
68
  fakeProvisionerAuth,
65
69
  ],
66
70
  routes: createRunnerRoutes(runnersTestAuthClient),
@@ -4,7 +4,9 @@ import {
4
4
  } from '@shipfox/api-auth-context';
5
5
  import type {PollDemandTemplateDto} from '@shipfox/api-runners-dto';
6
6
  import {pollDemandBodySchema, pollDemandResponseSchema} from '@shipfox/api-runners-dto';
7
+ import {reportError} from '@shipfox/node-error-monitoring';
7
8
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
9
+ import {logger} from '@shipfox/node-opentelemetry';
8
10
  import {config} from '#config.js';
9
11
  import {pollDemand, releaseReservationGrants} from '#core/demand.js';
10
12
  import {publishWorkspaceProvisionerCapabilitySnapshot} from '#db/provisioner-capability-snapshots.js';
@@ -50,7 +52,17 @@ export function createPollDemandRoute(options: CreateRunnersModuleOptions = {})
50
52
  reply.raw.on('close', () => {
51
53
  if (!responseFinished) {
52
54
  abortController.abort();
53
- void releaseReservationGrants(responseReservations).catch(() => undefined);
55
+ void releaseReservationGrants(responseReservations).catch((error) => {
56
+ logger().error(
57
+ {err: error, reservationCount: responseReservations.length},
58
+ 'Failed to release reservations after provisioner disconnect',
59
+ );
60
+ reportError(error, {
61
+ boundary: 'runners.cleanup',
62
+ operation: 'release-disconnected-reservations',
63
+ extra: {reservationCount: responseReservations.length},
64
+ });
65
+ });
54
66
  }
55
67
  });
56
68
  const provisionerContext = requireProvisionerContext(request);
@@ -1,8 +1,3 @@
1
- import {
2
- createLeaseTokenAuthMethod,
3
- createRunnerSessionAuthMethod,
4
- verifyRunnerSessionToken,
5
- } from '@shipfox/api-auth';
6
1
  import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
7
2
  import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
8
3
  import type {AuthMethod} from '@shipfox/node-fastify';
@@ -20,6 +15,9 @@ import {runnerSessions} from '#db/schema/runner-sessions.js';
20
15
  import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
21
16
  import {
22
17
  ephemeralRegistrationTokenFactory,
18
+ fakeLeaseTokenAuthMethod,
19
+ fakeRunnerSessionAuthMethod,
20
+ getRunnerSessionTokenClaims,
23
21
  manualRegistrationTokenFactory,
24
22
  runnersTestAuthClient,
25
23
  } from '#test/index.js';
@@ -52,8 +50,8 @@ describe('POST /runners/register', () => {
52
50
  auth: [
53
51
  fakeUserAuth,
54
52
  createRunnerRegistrationTokenAuthMethod(),
55
- createRunnerSessionAuthMethod(),
56
- createLeaseTokenAuthMethod(),
53
+ fakeRunnerSessionAuthMethod,
54
+ fakeLeaseTokenAuthMethod,
57
55
  fakeProvisionerAuth,
58
56
  ],
59
57
  routes: createRunnerRoutes(runnersTestAuthClient),
@@ -87,7 +85,7 @@ describe('POST /runners/register', () => {
87
85
  expect(body.mode).toBe('manual');
88
86
  expect(body.max_claims).toBeNull();
89
87
 
90
- const claims = await verifyRunnerSessionToken(body.session_token);
88
+ const claims = getRunnerSessionTokenClaims(body.session_token);
91
89
  expect(claims).toMatchObject({
92
90
  runnerSessionId: body.session_id,
93
91
  workspaceId,
@@ -144,7 +142,7 @@ describe('POST /runners/register', () => {
144
142
  expect(body.mode).toBe('ephemeral');
145
143
  expect(body.max_claims).toBe(1);
146
144
 
147
- const claims = await verifyRunnerSessionToken(body.session_token);
145
+ const claims = getRunnerSessionTokenClaims(body.session_token);
148
146
  expect(claims?.maxClaims).toBe(1);
149
147
 
150
148
  const [session] = await db()
@@ -1,19 +1,14 @@
1
- import {
2
- createLeaseTokenAuthMethod,
3
- createRunnerSessionAuthMethod,
4
- verifyJobLeaseToken,
5
- } from '@shipfox/api-auth';
6
1
  import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
7
- import {RUNNER_SESSION_TOKEN_AUDIENCE} from '@shipfox/api-auth-dto';
8
- import {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
9
2
  import type {AuthMethod} from '@shipfox/node-fastify';
10
3
  import {closeApp, createApp} from '@shipfox/node-fastify';
11
- import {signHs256} from '@shipfox/node-jwt';
12
4
  import {generateOpaqueToken} from '@shipfox/node-tokens';
13
5
  import type {FastifyInstance} from 'fastify';
14
6
  import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
15
7
  import {
16
8
  ephemeralRegistrationTokenFactory,
9
+ fakeLeaseTokenAuthMethod,
10
+ fakeRunnerSessionAuthMethod,
11
+ getLeaseTokenClaims,
17
12
  manualRegistrationTokenFactory,
18
13
  pendingJobFactory,
19
14
  runnersTestAuthClient,
@@ -42,8 +37,8 @@ describe('POST /runners/jobs/request', () => {
42
37
  auth: [
43
38
  fakeUserAuth,
44
39
  createRunnerRegistrationTokenAuthMethod(),
45
- createRunnerSessionAuthMethod(),
46
- createLeaseTokenAuthMethod(),
40
+ fakeRunnerSessionAuthMethod,
41
+ fakeLeaseTokenAuthMethod,
47
42
  fakeProvisionerAuth,
48
43
  ],
49
44
  routes: createRunnerRoutes(runnersTestAuthClient),
@@ -116,7 +111,7 @@ describe('POST /runners/jobs/request', () => {
116
111
  expect(body.job_name).toBeUndefined();
117
112
  expect(body.steps).toBeUndefined();
118
113
 
119
- const claims = await verifyJobLeaseToken(body.lease_token);
114
+ const claims = getLeaseTokenClaims(body.lease_token);
120
115
  expect(claims).toMatchObject({
121
116
  jobId: created.jobId,
122
117
  workflowRunId: created.workflowRunId,
@@ -200,18 +195,7 @@ describe('POST /runners/jobs/request', () => {
200
195
  });
201
196
 
202
197
  it('returns 401 when the runner session token is expired', async () => {
203
- const expiredSessionToken = await signHs256({
204
- payload: {
205
- runnerSessionId,
206
- workspaceId,
207
- scope: 'workspace',
208
- labels: ['linux', 'x64'],
209
- maxClaims: null,
210
- },
211
- secret: runnerSessionTokenKey(),
212
- expiresIn: '-1s',
213
- audience: RUNNER_SESSION_TOKEN_AUDIENCE,
214
- });
198
+ const expiredSessionToken = 'invalid';
215
199
 
216
200
  const res = await app.inject({
217
201
  method: 'POST',
@@ -1,4 +1,3 @@
1
- import {createLeaseTokenAuthMethod, createRunnerSessionAuthMethod} from '@shipfox/api-auth';
2
1
  import {AUTH_PROVISIONER_TOKEN, AUTH_USER, setProvisionerContext} from '@shipfox/api-auth-context';
3
2
  import {
4
3
  type AuthMethod,
@@ -18,7 +17,11 @@ import {
18
17
  createRunnerControlSessionAuthMethod,
19
18
  createRunnerRegistrationTokenAuthMethod,
20
19
  } from '#presentation/auth/index.js';
21
- import {runnersTestAuthClient} from '#test/index.js';
20
+ import {
21
+ fakeLeaseTokenAuthMethod,
22
+ fakeRunnerSessionAuthMethod,
23
+ runnersTestAuthClient,
24
+ } from '#test/index.js';
22
25
  import {createRunnerRoutes} from './index.js';
23
26
 
24
27
  const token = 'provisioner-test-token';
@@ -44,8 +47,8 @@ describe('runner enrollment control plane', () => {
44
47
  provisionerAuth,
45
48
  createRunnerRegistrationTokenAuthMethod(),
46
49
  createRunnerControlSessionAuthMethod(),
47
- createRunnerSessionAuthMethod(),
48
- createLeaseTokenAuthMethod(),
50
+ fakeRunnerSessionAuthMethod,
51
+ fakeLeaseTokenAuthMethod,
49
52
  ],
50
53
  routes: createRunnerRoutes(runnersTestAuthClient),
51
54
  swagger: false,
@@ -1,30 +1 @@
1
- import {JOB_LEASE_TOKEN_AUDIENCE, RUNNER_SESSION_TOKEN_AUDIENCE} from '@shipfox/api-auth-dto';
2
- import type {AuthInterModuleClient} from '@shipfox/api-auth-dto/inter-module';
3
- import {jobLeaseTokenKey, runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
4
- import {signHs256} from '@shipfox/node-jwt';
5
-
6
- const leaseSecret = jobLeaseTokenKey();
7
- const runnerSessionSecret = runnerSessionTokenKey();
8
-
9
- export const runnersTestAuthClient: AuthInterModuleClient = {
10
- async mintRunnerSessionToken(claims) {
11
- return {
12
- token: await signHs256({
13
- payload: claims,
14
- secret: runnerSessionSecret,
15
- expiresIn: '1h',
16
- audience: RUNNER_SESSION_TOKEN_AUDIENCE,
17
- }),
18
- };
19
- },
20
- async mintJobLeaseToken(claims) {
21
- return {
22
- token: await signHs256({
23
- payload: claims,
24
- secret: leaseSecret,
25
- expiresIn: '90m',
26
- audience: JOB_LEASE_TOKEN_AUDIENCE,
27
- }),
28
- };
29
- },
30
- };
1
+ export {runnersTestAuthClient} from './auth.js';
@@ -0,0 +1,85 @@
1
+ import {
2
+ AUTH_LEASED_JOB,
3
+ AUTH_RUNNER_SESSION,
4
+ setLeasedJobContext,
5
+ setRunnerSessionContext,
6
+ } from '@shipfox/api-auth-context';
7
+ import {
8
+ JOB_LEASE_TOKEN_AUDIENCE,
9
+ type JobLeaseTokenClaims,
10
+ RUNNER_SESSION_TOKEN_AUDIENCE,
11
+ type RunnerSessionTokenClaims,
12
+ } from '@shipfox/api-auth-dto';
13
+ import type {AuthInterModuleClient} from '@shipfox/api-auth-dto/inter-module';
14
+ import {type AuthMethod, ClientError, extractBearerToken} from '@shipfox/node-fastify';
15
+
16
+ const leaseClaims = new Map<string, JobLeaseTokenClaims>();
17
+ const sessionClaims = new Map<string, RunnerSessionTokenClaims>();
18
+
19
+ function opaqueToken(): string {
20
+ return `test-auth-${crypto.randomUUID()}`;
21
+ }
22
+
23
+ export function mintLeaseToken(claims: Omit<JobLeaseTokenClaims, 'aud' | 'iat' | 'exp'>): string {
24
+ const token = opaqueToken();
25
+ leaseClaims.set(token, {
26
+ ...claims,
27
+ aud: JOB_LEASE_TOKEN_AUDIENCE,
28
+ iat: 0,
29
+ exp: Number.MAX_SAFE_INTEGER,
30
+ });
31
+ return token;
32
+ }
33
+
34
+ export function mintRunnerSessionToken(
35
+ claims: Omit<RunnerSessionTokenClaims, 'aud' | 'iat' | 'exp'>,
36
+ ): string {
37
+ const token = opaqueToken();
38
+ sessionClaims.set(token, {
39
+ ...claims,
40
+ aud: RUNNER_SESSION_TOKEN_AUDIENCE,
41
+ iat: 0,
42
+ exp: Number.MAX_SAFE_INTEGER,
43
+ });
44
+ return token;
45
+ }
46
+
47
+ export function getLeaseTokenClaims(token: string): JobLeaseTokenClaims | undefined {
48
+ return leaseClaims.get(token);
49
+ }
50
+
51
+ export function getRunnerSessionTokenClaims(token: string): RunnerSessionTokenClaims | undefined {
52
+ return sessionClaims.get(token);
53
+ }
54
+
55
+ function requireClaims<T>(claims: Map<string, T>, authorization: string | undefined): T {
56
+ const token = extractBearerToken(authorization);
57
+ const context = token ? claims.get(token) : undefined;
58
+ if (!context) throw new ClientError('Authentication required', 'unauthorized', {status: 401});
59
+ return context;
60
+ }
61
+
62
+ export const fakeLeaseTokenAuthMethod: AuthMethod = {
63
+ name: AUTH_LEASED_JOB,
64
+ authenticate: (request) => {
65
+ setLeasedJobContext(request, requireClaims(leaseClaims, request.headers.authorization));
66
+ return Promise.resolve();
67
+ },
68
+ };
69
+
70
+ export const fakeRunnerSessionAuthMethod: AuthMethod = {
71
+ name: AUTH_RUNNER_SESSION,
72
+ authenticate: (request) => {
73
+ setRunnerSessionContext(request, requireClaims(sessionClaims, request.headers.authorization));
74
+ return Promise.resolve();
75
+ },
76
+ };
77
+
78
+ export const runnersTestAuthClient: AuthInterModuleClient = {
79
+ mintRunnerSessionToken(claims) {
80
+ return Promise.resolve({token: mintRunnerSessionToken(claims)});
81
+ },
82
+ mintJobLeaseToken(claims) {
83
+ return Promise.resolve({token: mintLeaseToken(claims)});
84
+ },
85
+ };
package/test/index.ts CHANGED
@@ -14,4 +14,12 @@ export {
14
14
  export {reservationFactory} from './factories/reservation.js';
15
15
  export {providerRunnerFactory} from './factories/runner-instance.js';
16
16
  export {runnerSessionFactory} from './factories/runner-session.js';
17
+ export {
18
+ fakeLeaseTokenAuthMethod,
19
+ fakeRunnerSessionAuthMethod,
20
+ getLeaseTokenClaims,
21
+ getRunnerSessionTokenClaims,
22
+ mintLeaseToken,
23
+ mintRunnerSessionToken,
24
+ } from './fixtures/auth.js';
17
25
  export {runnersTestAuthClient} from './fixtures/auth-inter-module.js';