@atolis-hq/wake 0.4.35 → 0.4.37

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.
Files changed (41) hide show
  1. package/dist/src/bootstrap/composition-root.d.ts +1 -0
  2. package/dist/src/bootstrap/composition-root.js +1 -1
  3. package/dist/src/bootstrap/integration-runtime.d.ts +3 -0
  4. package/dist/src/bootstrap/integration-runtime.js +22 -0
  5. package/dist/src/bootstrap/surface-api-applications.d.ts +3 -3
  6. package/dist/src/bootstrap/surface-api-applications.js +12 -7
  7. package/dist/src/bootstrap/surface-cli-applications.d.ts +1 -0
  8. package/dist/src/bootstrap/surface-cli-applications.js +18 -0
  9. package/dist/src/bootstrap/version.js +1 -1
  10. package/dist/src/control-plane/application/control-plane-projection.d.ts +2 -2
  11. package/dist/src/control-plane/application/control-plane-projection.js +5 -5
  12. package/dist/src/control-plane/application/control-plane-service.d.ts +3 -3
  13. package/dist/src/control-plane/application/control-plane-service.js +10 -7
  14. package/dist/src/integrations/application/poll-service.d.ts +3 -1
  15. package/dist/src/integrations/application/poll-service.js +2 -2
  16. package/dist/src/integrations/contracts/intake.d.ts +3 -1
  17. package/dist/src/integrations/contracts/provider.d.ts +7 -0
  18. package/dist/src/integrations/github/contracts/config.d.ts +3 -0
  19. package/dist/src/integrations/github/contracts/config.js +4 -0
  20. package/dist/src/integrations/github/index.d.ts +2 -0
  21. package/dist/src/integrations/github/index.js +2 -0
  22. package/dist/src/integrations/github/infrastructure/adapter-health-registry.d.ts +1 -1
  23. package/dist/src/integrations/github/infrastructure/adapter-health-registry.js +0 -0
  24. package/dist/src/integrations/github/infrastructure/client.d.ts +16 -0
  25. package/dist/src/integrations/github/infrastructure/client.js +29 -0
  26. package/dist/src/integrations/github/infrastructure/source.js +4 -2
  27. package/dist/src/integrations/github/infrastructure/webhook-state.d.ts +16 -0
  28. package/dist/src/integrations/github/infrastructure/webhook-state.js +70 -0
  29. package/dist/src/integrations/github/infrastructure/webhook.d.ts +24 -0
  30. package/dist/src/integrations/github/infrastructure/webhook.js +148 -0
  31. package/dist/src/integrations/github/provider.js +2 -0
  32. package/dist/src/surfaces/api/contracts/control-plane.d.ts +4 -4
  33. package/dist/src/surfaces/api/http-server.d.ts +1 -0
  34. package/dist/src/surfaces/api/http-server.js +11 -0
  35. package/dist/src/surfaces/api/routes/applications.d.ts +2 -2
  36. package/dist/src/surfaces/api/routes/commands.js +7 -1
  37. package/dist/src/surfaces/api/routes/control-plane.d.ts +1 -1
  38. package/dist/src/surfaces/api/routes/control-plane.js +2 -2
  39. package/dist/src/surfaces/web-assets/assets/{index-Cs3ckFXD.js → index-aL1g5gVQ.js} +1 -1
  40. package/dist/src/surfaces/web-assets/index.html +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,148 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { GitHubWebhookStateStore } from './webhook-state.js';
3
+ const events = new Set([
4
+ 'issues',
5
+ 'issue_comment',
6
+ 'pull_request',
7
+ 'pull_request_review',
8
+ 'pull_request_review_comment',
9
+ 'check_run',
10
+ 'status',
11
+ ]);
12
+ export function createGitHubWebhook(adapter, config, publicUrl, root, client, health) {
13
+ const state = new GitHubWebhookStateStore(root, adapter);
14
+ const endpoint = publicUrl === undefined ? undefined : webhookEndpoint(publicUrl);
15
+ return {
16
+ async provision() {
17
+ for (const repository of config.repositories) {
18
+ if (!config.webhooks.enabled)
19
+ continue;
20
+ if (endpoint === undefined) {
21
+ health.recordFailure(`${repository.owner}/${repository.repo}`, 'webhook', new Error('surfaces.web.publicUrl is not configured'));
22
+ continue;
23
+ }
24
+ try {
25
+ await state.serialise(repository.owner, repository.repo, () => provisionRepository(repository, endpoint, state, client, health));
26
+ }
27
+ catch (error) {
28
+ health.recordFailure(`${repository.owner}/${repository.repo}`, 'webhook', error);
29
+ }
30
+ }
31
+ },
32
+ async receive(body, headers, trigger) {
33
+ if (!config.webhooks.enabled)
34
+ return 404;
35
+ const signature = header(headers, 'x-hub-signature-256');
36
+ if (signature === undefined)
37
+ return 401;
38
+ const event = header(headers, 'x-github-event');
39
+ if (event === undefined)
40
+ return 400;
41
+ const fullName = parseRepository(body);
42
+ if (fullName === null)
43
+ return 400;
44
+ if (fullName === undefined)
45
+ return 404;
46
+ const [owner, repo] = fullName.split('/');
47
+ if (owner === undefined ||
48
+ repo === undefined ||
49
+ !config.repositories.some((value) => value.owner === owner && value.repo === repo))
50
+ return 404;
51
+ return state.serialise(owner, repo, async () => {
52
+ const saved = await state.load(owner, repo);
53
+ const expected = `sha256=${createHmac('sha256', saved.secret).update(body).digest('hex')}`;
54
+ if (!safeEqual(signature, expected))
55
+ return 401;
56
+ if (!events.has(event))
57
+ return 404;
58
+ trigger();
59
+ return 202;
60
+ });
61
+ },
62
+ };
63
+ }
64
+ async function provisionRepository(repository, endpoint, state, client, health) {
65
+ const scope = `${repository.owner}/${repository.repo}`;
66
+ try {
67
+ const saved = await state.load(repository.owner, repository.repo);
68
+ if (saved.hookId !== undefined &&
69
+ (await updateManagedHook(repository, endpoint, saved, client))) {
70
+ health.recordSuccess(scope, 'webhook');
71
+ return;
72
+ }
73
+ const hookId = await client.createHook({
74
+ ...repository,
75
+ url: endpoint,
76
+ secret: saved.secret,
77
+ events: [...events],
78
+ });
79
+ await state.save(repository.owner, repository.repo, { ...saved, hookId });
80
+ health.recordSuccess(scope, 'webhook');
81
+ }
82
+ catch (error) {
83
+ health.recordFailure(scope, 'webhook', error);
84
+ }
85
+ }
86
+ async function updateManagedHook(repository, endpoint, saved, client) {
87
+ if (saved.hookId === undefined)
88
+ return false;
89
+ try {
90
+ await client.getHook(repository.owner, repository.repo, saved.hookId);
91
+ await client.updateHook({
92
+ ...repository,
93
+ hookId: saved.hookId,
94
+ url: endpoint,
95
+ secret: saved.secret,
96
+ events: [...events],
97
+ });
98
+ return true;
99
+ }
100
+ catch (error) {
101
+ if (statusOf(error) === 404)
102
+ return false;
103
+ throw error;
104
+ }
105
+ }
106
+ function header(headers, name) {
107
+ const value = headers[name] ?? headers[name.toLowerCase()];
108
+ return Array.isArray(value) ? value[0] : value;
109
+ }
110
+ function repositoryName(value) {
111
+ if (typeof value !== 'object' ||
112
+ value === null ||
113
+ !('full_name' in value) ||
114
+ typeof value.full_name !== 'string')
115
+ return undefined;
116
+ return value.full_name;
117
+ }
118
+ function parseRepository(body) {
119
+ try {
120
+ const payload = JSON.parse(body.toString('utf8'));
121
+ if (typeof payload !== 'object' || payload === null || !('repository' in payload))
122
+ return null;
123
+ return repositoryName(payload.repository);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ function safeEqual(left, right) {
130
+ const a = Buffer.from(left);
131
+ const b = Buffer.from(right);
132
+ return a.length === b.length && timingSafeEqual(a, b);
133
+ }
134
+ function statusOf(error) {
135
+ return typeof error === 'object' &&
136
+ error !== null &&
137
+ 'status' in error &&
138
+ typeof error.status === 'number'
139
+ ? error.status
140
+ : undefined;
141
+ }
142
+ function webhookEndpoint(publicUrl) {
143
+ const base = new URL(publicUrl);
144
+ base.pathname = `${base.pathname.replace(/\/$/, '')}/webhooks/github`;
145
+ base.search = '';
146
+ base.hash = '';
147
+ return base.href;
148
+ }
@@ -14,6 +14,7 @@ import { createGitHubDelivery } from './infrastructure/delivery.js';
14
14
  import { resolveGitHubCliToken } from './infrastructure/gh-auth.js';
15
15
  import { createGitHubRequestCoordinator } from './infrastructure/request-coordinator.js';
16
16
  import { createGitHubSource } from './infrastructure/source.js';
17
+ import { createGitHubWebhook } from './infrastructure/webhook.js';
17
18
  export const gitHubProviderDefinition = {
18
19
  provider: 'github',
19
20
  eventTypes: Object.values(GitHubEventType),
@@ -59,6 +60,7 @@ export const gitHubProviderDefinition = {
59
60
  requests,
60
61
  }),
61
62
  health: () => health.snapshotAll(),
63
+ webhook: createGitHubWebhook(adapter, config, services.publicUiUrl, services.providerStateRoot, client, health),
62
64
  commands: () => [
63
65
  ...Object.values(GitHubBuiltInCommand).map((syntax) => ({ syntax })),
64
66
  ...config.commands.map((syntax) => ({ syntax })),
@@ -1,12 +1,12 @@
1
1
  import { type ValueOf } from '../../../kernel/index.js';
2
2
  export interface ControlPlaneStatusResponse {
3
- readonly paused: boolean;
4
- readonly pausedUntil?: string;
5
- readonly reason?: string;
3
+ readonly dispatchPaused: boolean;
4
+ readonly dispatchPausedUntil?: string;
5
+ readonly dispatchPauseReason?: string;
6
6
  readonly updatedAt: string;
7
7
  /**
8
8
  * A retained update-maintenance lease pauses every resident loop
9
- * (intake and dispatch) independent of `paused`/`pausedUntil`, which only
9
+ * (intake and dispatch) independent of `dispatchPaused`/`dispatchPausedUntil`, which only
10
10
  * reflect an explicit pause or a runner-quota backoff. Present whenever a
11
11
  * lease exists, including a failed one -- see update-maintenance-lease.ts.
12
12
  */
@@ -24,6 +24,7 @@ export interface SurfaceHttpServerOptions {
24
24
  readonly disabled: boolean;
25
25
  readonly redeemGrant: (grant: string) => Promise<boolean>;
26
26
  };
27
+ readonly webhookReceiver?: (body: Buffer, headers: Readonly<Record<string, string | string[] | undefined>>) => Promise<number>;
27
28
  }
28
29
  /**
29
30
  * The production HTTP surface. Public login routes are declared separately;
@@ -41,6 +41,17 @@ export function createSurfaceHttpServer(options) {
41
41
  return sendFastifyJson(reply, errorResult(new MalformedJsonError()), false);
42
42
  return sendFastifyJson(reply, errorResult(error), false);
43
43
  });
44
+ if (options.webhookReceiver !== undefined) {
45
+ app.register((webhooks, _pluginOptions, done) => {
46
+ webhooks.removeAllContentTypeParsers();
47
+ webhooks.addContentTypeParser('*', { parseAs: 'buffer' }, (_request, body, callback) => callback(null, body));
48
+ webhooks.post('/github', { bodyLimit: 64 * 1024 }, async (request, reply) => {
49
+ const status = await options.webhookReceiver(request.body, request.headers);
50
+ return reply.code(status).send();
51
+ });
52
+ done();
53
+ }, { prefix: '/webhooks' });
54
+ }
44
55
  app.get('/api/v1/auth/session', async (request, reply) => {
45
56
  if (authDisabled)
46
57
  return { authenticated: true };
@@ -47,8 +47,8 @@ export interface ApiApplications {
47
47
  };
48
48
  readonly controlPlane: {
49
49
  status(): Promise<ApiResourceResult<ControlPlaneStatusResponse>>;
50
- pause?(command: ApiCommandRequest): Promise<ApiCommandResult>;
51
- resume?(command: ApiCommandRequest): Promise<ApiCommandResult>;
50
+ pauseDispatch?(command: ApiCommandRequest): Promise<ApiCommandResult>;
51
+ resumeDispatch?(command: ApiCommandRequest): Promise<ApiCommandResult>;
52
52
  clearMaintenance?(command: ApiClearMaintenanceRequest): Promise<ApiCommandResult>;
53
53
  };
54
54
  readonly work: {
@@ -94,7 +94,13 @@ function controlCommandName(pathname) {
94
94
  if (!pathname.startsWith(prefix))
95
95
  return undefined;
96
96
  const name = pathname.slice(prefix.length);
97
- return name === 'pause' || name === 'resume' || name === 'clear-maintenance' ? name : undefined;
97
+ if (name === 'pause-dispatch')
98
+ return 'pauseDispatch';
99
+ if (name === 'resume-dispatch')
100
+ return 'resumeDispatch';
101
+ if (name === 'clear-maintenance')
102
+ return 'clear-maintenance';
103
+ return undefined;
98
104
  }
99
105
  async function dispatchMaintenanceClear(applications, pathname, body) {
100
106
  if (pathname !== '/api/v1/control-plane/commands/clear-maintenance')
@@ -1 +1 @@
1
- export declare const controlPlaneRoutes: readonly ["/api/v1/control-plane/status", "/api/v1/control-plane/commands/pause", "/api/v1/control-plane/commands/resume", "/api/v1/control-plane/commands/clear-maintenance"];
1
+ export declare const controlPlaneRoutes: readonly ["/api/v1/control-plane/status", "/api/v1/control-plane/commands/pause-dispatch", "/api/v1/control-plane/commands/resume-dispatch", "/api/v1/control-plane/commands/clear-maintenance"];
@@ -1,6 +1,6 @@
1
1
  export const controlPlaneRoutes = [
2
2
  '/api/v1/control-plane/status',
3
- '/api/v1/control-plane/commands/pause',
4
- '/api/v1/control-plane/commands/resume',
3
+ '/api/v1/control-plane/commands/pause-dispatch',
4
+ '/api/v1/control-plane/commands/resume-dispatch',
5
5
  '/api/v1/control-plane/commands/clear-maintenance',
6
6
  ];