@antasphere/chassis-server 1.0.2 → 1.2.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/dist/api/apikeys.d.ts +4 -1
- package/dist/api/apikeys.d.ts.map +1 -1
- package/dist/api/apikeys.js +11 -1
- package/dist/api/apikeys.js.map +1 -1
- package/dist/api/cli-package.d.ts +58 -0
- package/dist/api/cli-package.d.ts.map +1 -0
- package/dist/api/cli-package.js +83 -0
- package/dist/api/cli-package.js.map +1 -0
- package/dist/api/create-api.d.ts +3 -0
- package/dist/api/create-api.d.ts.map +1 -1
- package/dist/api/create-api.js +33 -4
- package/dist/api/create-api.js.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/invitations.d.ts.map +1 -1
- package/dist/api/invitations.js +6 -0
- package/dist/api/invitations.js.map +1 -1
- package/dist/api/members.d.ts +5 -2
- package/dist/api/members.d.ts.map +1 -1
- package/dist/api/members.js +1 -0
- package/dist/api/members.js.map +1 -1
- package/dist/api/projects.d.ts +3 -0
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +13 -1
- package/dist/api/projects.js.map +1 -1
- package/dist/api/webhooks.d.ts +24 -0
- package/dist/api/webhooks.d.ts.map +1 -0
- package/dist/api/webhooks.js +170 -0
- package/dist/api/webhooks.js.map +1 -0
- package/dist/boot.d.ts.map +1 -1
- package/dist/boot.js +47 -4
- package/dist/boot.js.map +1 -1
- package/dist/cli-package/index.d.ts +44 -0
- package/dist/cli-package/index.d.ts.map +1 -0
- package/dist/cli-package/index.js +60 -0
- package/dist/cli-package/index.js.map +1 -0
- package/dist/email/index.d.ts +1 -0
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +1 -0
- package/dist/email/index.js.map +1 -1
- package/dist/env.d.ts +11 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +24 -0
- package/dist/env.js.map +1 -1
- package/dist/identity/demo-pass-rules.d.ts.map +1 -1
- package/dist/identity/demo-pass-rules.js +6 -1
- package/dist/identity/demo-pass-rules.js.map +1 -1
- package/dist/identity/hub-reconcile.d.ts +2 -2
- package/dist/identity/hub-reconcile.d.ts.map +1 -1
- package/dist/identity/hub-reconcile.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js.map +1 -1
- package/dist/jobs/pgboss.d.ts +34 -0
- package/dist/jobs/pgboss.d.ts.map +1 -1
- package/dist/jobs/pgboss.js +32 -9
- package/dist/jobs/pgboss.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +11 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-kit.d.ts +2 -0
- package/dist/mcp/tool-kit.d.ts.map +1 -1
- package/dist/mcp/tool-kit.js +5 -1
- package/dist/mcp/tool-kit.js.map +1 -1
- package/dist/mcp/webhooks.d.ts +16 -0
- package/dist/mcp/webhooks.d.ts.map +1 -0
- package/dist/mcp/webhooks.js +226 -0
- package/dist/mcp/webhooks.js.map +1 -0
- package/dist/members/removal.d.ts +10 -2
- package/dist/members/removal.d.ts.map +1 -1
- package/dist/members/removal.js +3 -0
- package/dist/members/removal.js.map +1 -1
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +3 -1
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/middleware/scopes.d.ts +9 -1
- package/dist/middleware/scopes.d.ts.map +1 -1
- package/dist/middleware/scopes.js +8 -1
- package/dist/middleware/scopes.js.map +1 -1
- package/dist/secret-box.d.ts +10 -0
- package/dist/secret-box.d.ts.map +1 -0
- package/dist/secret-box.js +87 -0
- package/dist/secret-box.js.map +1 -0
- package/dist/testing/helpers.d.ts +8 -0
- package/dist/testing/helpers.d.ts.map +1 -1
- package/dist/testing/helpers.js.map +1 -1
- package/dist/tool-definition.d.ts +24 -0
- package/dist/tool-definition.d.ts.map +1 -1
- package/dist/tool-definition.js.map +1 -1
- package/dist/webhooks/catalog.d.ts +80 -0
- package/dist/webhooks/catalog.d.ts.map +1 -0
- package/dist/webhooks/catalog.js +134 -0
- package/dist/webhooks/catalog.js.map +1 -0
- package/dist/webhooks/index.d.ts +9 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +9 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/jobs.d.ts +12 -0
- package/dist/webhooks/jobs.d.ts.map +1 -0
- package/dist/webhooks/jobs.js +77 -0
- package/dist/webhooks/jobs.js.map +1 -0
- package/dist/webhooks/mail.d.ts +24 -0
- package/dist/webhooks/mail.d.ts.map +1 -0
- package/dist/webhooks/mail.js +21 -0
- package/dist/webhooks/mail.js.map +1 -0
- package/dist/webhooks/match.d.ts +22 -0
- package/dist/webhooks/match.d.ts.map +1 -0
- package/dist/webhooks/match.js +40 -0
- package/dist/webhooks/match.js.map +1 -0
- package/dist/webhooks/safe-fetch.d.ts +94 -0
- package/dist/webhooks/safe-fetch.d.ts.map +1 -0
- package/dist/webhooks/safe-fetch.js +263 -0
- package/dist/webhooks/safe-fetch.js.map +1 -0
- package/dist/webhooks/service.d.ts +245 -0
- package/dist/webhooks/service.d.ts.map +1 -0
- package/dist/webhooks/service.js +861 -0
- package/dist/webhooks/service.js.map +1 -0
- package/dist/webhooks/signing.d.ts +40 -0
- package/dist/webhooks/signing.d.ts.map +1 -0
- package/dist/webhooks/signing.js +108 -0
- package/dist/webhooks/signing.js.map +1 -0
- package/dist/webhooks/tx-send.d.ts +31 -0
- package/dist/webhooks/tx-send.d.ts.map +1 -0
- package/dist/webhooks/tx-send.js +22 -0
- package/dist/webhooks/tx-send.js.map +1 -0
- package/package.json +11 -3
- package/src/api/apikeys.ts +12 -1
- package/src/api/cli-package.ts +151 -0
- package/src/api/create-api.ts +42 -4
- package/src/api/index.ts +1 -0
- package/src/api/invitations.ts +6 -0
- package/src/api/members.ts +6 -2
- package/src/api/projects.ts +16 -1
- package/src/api/webhooks.ts +205 -0
- package/src/boot.ts +48 -4
- package/src/cli-package/index.ts +88 -0
- package/src/email/index.ts +1 -0
- package/src/env.ts +25 -0
- package/src/identity/demo-pass-rules.ts +6 -1
- package/src/identity/hub-reconcile.ts +2 -2
- package/src/index.ts +13 -1
- package/src/jobs/index.ts +1 -0
- package/src/jobs/pgboss.ts +71 -9
- package/src/mcp/index.ts +1 -0
- package/src/mcp/server.ts +10 -2
- package/src/mcp/tool-kit.ts +7 -1
- package/src/mcp/webhooks.ts +370 -0
- package/src/members/removal.ts +21 -1
- package/src/middleware/idempotency.ts +3 -1
- package/src/middleware/scopes.ts +14 -0
- package/src/secret-box.ts +93 -0
- package/src/testing/helpers.ts +8 -0
- package/src/tool-definition.ts +29 -0
- package/src/webhooks/catalog.ts +205 -0
- package/src/webhooks/index.ts +58 -0
- package/src/webhooks/jobs.ts +92 -0
- package/src/webhooks/mail.ts +48 -0
- package/src/webhooks/match.ts +52 -0
- package/src/webhooks/safe-fetch.ts +335 -0
- package/src/webhooks/service.ts +1089 -0
- package/src/webhooks/signing.ts +132 -0
- package/src/webhooks/tx-send.ts +53 -0
- package/test/host/drizzle/0002_webhooks.sql +49 -0
- package/test/host/drizzle/meta/0002_snapshot.json +3952 -0
- package/test/host/drizzle/meta/_journal.json +8 -1
- package/test/host/identity.ts +6 -1
- package/test/host/minimal-host.ts +2 -1
- package/test/host/minimal-tool.ts +32 -4
- package/test/integration/cli-package.test.ts +403 -0
- package/test/integration/mcp-webhooks.test.ts +360 -0
- package/test/integration/webhooks.test.ts +1684 -0
- package/test/unit/cli-package.test.ts +93 -0
- package/test/unit/secret-box.test.ts +124 -0
- package/test/unit/tx-send.test.ts +23 -0
- package/test/unit/webhook-catalog.test.ts +151 -0
- package/test/unit/webhook-env.test.ts +60 -0
- package/test/unit/webhook-jobs.test.ts +92 -0
- package/test/unit/webhook-match.test.ts +81 -0
- package/test/unit/webhook-safe-fetch.test.ts +330 -0
- package/test/unit/webhook-signing.test.ts +169 -0
package/src/api/create-api.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { registerOpenApiDoc } from './index.js';
|
|
|
7
7
|
import { ulid } from 'ulid';
|
|
8
8
|
import { and, asc, desc, eq } from 'drizzle-orm';
|
|
9
9
|
import { ACTIVE_WORKSPACE_HEADER, type Principal } from '@antasphere/chassis-contract';
|
|
10
|
-
import { instanceRoute, setupRoute } from '@antasphere/chassis-contract/routes';
|
|
10
|
+
import { cliPackageRoute, instanceRoute, setupRoute } from '@antasphere/chassis-contract/routes';
|
|
11
11
|
import {
|
|
12
12
|
account,
|
|
13
13
|
instanceSettings,
|
|
@@ -76,11 +76,16 @@ import {
|
|
|
76
76
|
type WorkspaceCloudDeps
|
|
77
77
|
} from './index.js';
|
|
78
78
|
import { registerSsoConnectRoutes } from './index.js';
|
|
79
|
+
import { registerCliPackageRoutes } from './index.js';
|
|
80
|
+
import { loadCliPackage } from '../cli-package/index.js';
|
|
79
81
|
import { registerSsoLogoutRoutes } from './index.js';
|
|
80
82
|
import { registerMemberRoutes } from './index.js';
|
|
81
83
|
import { demoSessionJudge, demoSignInOn, registerDemoPassRoutes } from './index.js';
|
|
82
84
|
import { registerProjectRoutes } from './index.js';
|
|
83
85
|
import { registerTeamRoutes } from './index.js';
|
|
86
|
+
import { registerWebhookRoutes } from './webhooks.js';
|
|
87
|
+
import type { WebhookService } from '../webhooks/index.js';
|
|
88
|
+
import { bindMembershipRemoval } from '../members/removal.js';
|
|
84
89
|
import { registerApiKeyRoutes } from './index.js';
|
|
85
90
|
import { registerInvitationRoutes } from './index.js';
|
|
86
91
|
import { registerAuditRoutes } from './index.js';
|
|
@@ -307,6 +312,8 @@ export interface ApiDeps<
|
|
|
307
312
|
* the gate compares limits against their oss values and emits nothing.
|
|
308
313
|
*/
|
|
309
314
|
entitlementCloud?: EntitlementCloud | undefined;
|
|
315
|
+
/** The outbound webhooks (PRDCT-3482): the endpoint routes and the catalog. */
|
|
316
|
+
webhooks: WebhookService;
|
|
310
317
|
}
|
|
311
318
|
|
|
312
319
|
/** Control-flow marker: the singleton claim lost (instance already set up). */
|
|
@@ -357,6 +364,13 @@ export function createApiApp<
|
|
|
357
364
|
// Demo sign-in (the demo pass spec): DEMO_SIGN_IN on the self-hosted
|
|
358
365
|
// edition only. Off, no demo route, wall or discovery key exists.
|
|
359
366
|
const demoSignIn = demoSignInOn(env, hub, logger);
|
|
367
|
+
// The tool's own CLI package (PRDCT-3312), read once here. parseEnv has
|
|
368
|
+
// already refused a CLI_PACKAGE_DIR that does not hold the pair, so a
|
|
369
|
+
// throw from here (the folder emptied between the two reads) still
|
|
370
|
+
// refuses the boot, it is never a person's failed install. Served (and
|
|
371
|
+
// advertised) only where its route is registered: the cloud block below.
|
|
372
|
+
const cliPackage = loadCliPackage(env.CLI_PACKAGE_DIR);
|
|
373
|
+
const cliServed = deps.hubSso && deps.hubGrant ? cliPackage : null;
|
|
360
374
|
|
|
361
375
|
const api = new OpenAPIHono({
|
|
362
376
|
// Validation failures use the same wire shape as every other error.
|
|
@@ -573,6 +587,9 @@ export function createApiApp<
|
|
|
573
587
|
// route itself: oss mounts zero hub surface.
|
|
574
588
|
if (deps.hubSso) {
|
|
575
589
|
api.use('/sso/cli-connect', rateLimit(limiters.cliConnect, clientIp));
|
|
590
|
+
// The CLI package download (cloud only, api/cli-package.ts) presents the
|
|
591
|
+
// same one-time hub token at the same cost: the same wall.
|
|
592
|
+
api.use('/cli/package', rateLimit(limiters.cliConnect, clientIp));
|
|
576
593
|
}
|
|
577
594
|
|
|
578
595
|
// ── JSON pre-validation in front of the Better Auth mount (AF-1 + the
|
|
@@ -832,6 +849,10 @@ export function createApiApp<
|
|
|
832
849
|
// Present only on a cloud instance whose operator restricted the tool
|
|
833
850
|
// (PRDCT-2947): organizations are opened by Antasphere, not created here.
|
|
834
851
|
...(hub && env.TOOL_RESTRICTED ? { restricted: true } : {}),
|
|
852
|
+
// Present only on a cloud instance whose release packed the tool's
|
|
853
|
+
// CLI (PRDCT-3312): what `POST /cli/package` serves, for the `team`
|
|
854
|
+
// command to install. An oss boot has no route, so no key either.
|
|
855
|
+
...(cliServed ? { cli: { name: cliServed.name, version: cliServed.version } } : {}),
|
|
835
856
|
// What this version declares for the billing rail (§7): the hub seeds
|
|
836
857
|
// from it, staff read it, a client reads the limit it is held to.
|
|
837
858
|
entitlements: {
|
|
@@ -1290,6 +1311,14 @@ export function createApiApp<
|
|
|
1290
1311
|
cliKeyScopes: tool.scopes.cliKey,
|
|
1291
1312
|
routes: { ssoCliConnectRoute }
|
|
1292
1313
|
});
|
|
1314
|
+
// The tool's own CLI package (PRDCT-3312): cloud-only for the same
|
|
1315
|
+
// reason as /sso/cli-connect (it verifies the same hub exchange token
|
|
1316
|
+
// and burns it in the same ledger), so an oss boot leaves the path to
|
|
1317
|
+
// the JSON 404 terminator. Deliberately UNLISTED in the machine scope
|
|
1318
|
+
// allowlist and absent from PUBLIC_API_PATHS: the token rides in the
|
|
1319
|
+
// body, a request without a credential reaches the handler, and an API
|
|
1320
|
+
// key or an OAuth bearer meets 403 endpoint_not_allowed before it.
|
|
1321
|
+
registerCliPackageRoutes(api, { db, hubSso, cliPackage, logger, routes: { cliPackageRoute } });
|
|
1293
1322
|
}
|
|
1294
1323
|
// Single logout (SL-2): cloud-only like /sso/cli-connect — an oss boot
|
|
1295
1324
|
// leaves POST /sso/logout to the JSON 404 terminator. Deliberately
|
|
@@ -1309,16 +1338,19 @@ export function createApiApp<
|
|
|
1309
1338
|
}
|
|
1310
1339
|
registerMemberRoutes(api, {
|
|
1311
1340
|
db,
|
|
1341
|
+
events: chassisRegistry.events,
|
|
1312
1342
|
auth,
|
|
1313
1343
|
publicBaseUrl: env.PUBLIC_BASE_URL,
|
|
1314
1344
|
accountDeletion: deps.accountDeletion,
|
|
1315
1345
|
hubManaged,
|
|
1316
1346
|
guestTargetMessage: tool.copy.guestTarget,
|
|
1317
|
-
onMembershipRemoval: tool.membershipRemoval
|
|
1347
|
+
onMembershipRemoval: bindMembershipRemoval(tool.membershipRemoval, { webhooks: deps.webhooks })
|
|
1318
1348
|
});
|
|
1319
1349
|
// Teams: the tool's own on self-hosted (and in a cloud-local workspace), the
|
|
1320
1350
|
// hub's projection in a hub-origin workspace, where the members' gate refuses the writes.
|
|
1321
1351
|
registerTeamRoutes(api, { db, hubManaged });
|
|
1352
|
+
// Webhooks (PRDCT-3482): the tool's on both editions, never hub-managed (Romain's rule of 27 September 2026).
|
|
1353
|
+
registerWebhookRoutes(api, { service: deps.webhooks });
|
|
1322
1354
|
if (demoSignIn) {
|
|
1323
1355
|
registerDemoPassRoutes(api, {
|
|
1324
1356
|
db,
|
|
@@ -1327,7 +1359,13 @@ export function createApiApp<
|
|
|
1327
1359
|
guestTargetMessage: tool.copy.guestTarget
|
|
1328
1360
|
});
|
|
1329
1361
|
}
|
|
1330
|
-
registerApiKeyRoutes(
|
|
1362
|
+
registerApiKeyRoutes(
|
|
1363
|
+
api,
|
|
1364
|
+
db,
|
|
1365
|
+
apiKeyService,
|
|
1366
|
+
{ apiKeysListRoute, apiKeyCreateRoute, apiKeyRevokeRoute },
|
|
1367
|
+
chassisRegistry.events
|
|
1368
|
+
);
|
|
1331
1369
|
registerInvitationRoutes(api, {
|
|
1332
1370
|
db,
|
|
1333
1371
|
env,
|
|
@@ -1346,7 +1384,7 @@ export function createApiApp<
|
|
|
1346
1384
|
// Projects: a subgroup of the workspace with its own members. Deliberately
|
|
1347
1385
|
// handed NO `hubManaged`: project membership is the tool's own on both
|
|
1348
1386
|
// editions, picked among the workspace's existing members.
|
|
1349
|
-
registerProjectRoutes(api, { db });
|
|
1387
|
+
registerProjectRoutes(api, { db, webhooks: deps.webhooks });
|
|
1350
1388
|
registerAuditRoutes(api, db);
|
|
1351
1389
|
registerExportRoutes(api, {
|
|
1352
1390
|
db,
|
package/src/api/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { registerApiKeyRoutes } from './apikeys.js';
|
|
|
2
2
|
export { registerAuditRoutes } from './audit.js';
|
|
3
3
|
export { registerBreakGlassRoutes } from './break-glass.js';
|
|
4
4
|
export { registerCliAuthRoutes } from './cli-auth.js';
|
|
5
|
+
export { registerCliPackageRoutes, type CliPackageRouteDeps } from './cli-package.js';
|
|
5
6
|
export { demoSessionJudge, demoSignInOn, registerDemoPassRoutes } from './demo-passes.js';
|
|
6
7
|
export {
|
|
7
8
|
registerExportRoutes,
|
package/src/api/invitations.ts
CHANGED
|
@@ -380,6 +380,12 @@ export function registerInvitationRoutes(api: OpenAPIHono, deps: InvitationRoute
|
|
|
380
380
|
invitationId: invitation.id,
|
|
381
381
|
userId
|
|
382
382
|
});
|
|
383
|
+
// The person is a member now (the webhooks' `member.joined` rides the bus).
|
|
384
|
+
registry.events.emit('member.joined', {
|
|
385
|
+
workspaceId: invitation.workspaceId,
|
|
386
|
+
userId,
|
|
387
|
+
role: invitation.role
|
|
388
|
+
});
|
|
383
389
|
// Public endpoint: no principal in context, write the audit row directly.
|
|
384
390
|
await audit.write({
|
|
385
391
|
workspaceId: invitation.workspaceId,
|
package/src/api/members.ts
CHANGED
|
@@ -20,12 +20,15 @@ import { hubManagedMembershipGate } from '../middleware/hub-managed.js';
|
|
|
20
20
|
import {
|
|
21
21
|
deleteMembershipGrants,
|
|
22
22
|
mergeRemovalCounts,
|
|
23
|
-
type
|
|
23
|
+
type BoundMembershipRemoval,
|
|
24
24
|
type RemovalCounts
|
|
25
25
|
} from '../members/removal.js';
|
|
26
|
+
import type { EventBus } from '../platform/events.js';
|
|
26
27
|
|
|
27
28
|
export interface MemberRouteDeps {
|
|
28
29
|
db: Db;
|
|
30
|
+
/** The bus, for `member.removed` after a removal commits (the webhooks bridge it). */
|
|
31
|
+
events?: EventBus | undefined;
|
|
29
32
|
auth: Auth;
|
|
30
33
|
publicBaseUrl: string;
|
|
31
34
|
accountDeletion: AccountDeletionService;
|
|
@@ -38,7 +41,7 @@ export interface MemberRouteDeps {
|
|
|
38
41
|
/** The tool's `guest_target` sentence (`copy.guestTarget`). */
|
|
39
42
|
guestTargetMessage: string;
|
|
40
43
|
/** The tool's half of a removal (the `membershipRemoval` slot), run in the removal's transaction. */
|
|
41
|
-
onMembershipRemoval?:
|
|
44
|
+
onMembershipRemoval?: BoundMembershipRemoval | undefined;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const err = (code: string, message: string) => ({ error: { code, message } });
|
|
@@ -425,6 +428,7 @@ export function registerMemberRoutes(api: OpenAPIHono, deps: MemberRouteDeps): v
|
|
|
425
428
|
.where(eq(workspaceMembers.id, target.id))
|
|
426
429
|
.limit(1);
|
|
427
430
|
|
|
431
|
+
deps.events?.emit('member.removed', { workspaceId: principal.workspaceId, userId: target.userId });
|
|
428
432
|
c.set('audit', {
|
|
429
433
|
action: 'member.remove',
|
|
430
434
|
resourceType: 'member',
|
package/src/api/projects.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
import type { WebhookEmitter } from '../webhooks/service.js';
|
|
2
3
|
import type { Context } from 'hono';
|
|
3
4
|
import { and, desc, eq, isNotNull, isNull, sql } from 'drizzle-orm';
|
|
4
5
|
import { projectRoleAtLeast, type Principal, type ProjectRole } from '@antasphere/chassis-contract';
|
|
@@ -62,6 +63,8 @@ import {
|
|
|
62
63
|
*/
|
|
63
64
|
export interface ProjectRouteDeps {
|
|
64
65
|
db: Db;
|
|
66
|
+
/** The outbound webhooks' emitter (PRDCT-3482): `project.created` inside the create's transaction, `project.archived` after the archive. */
|
|
67
|
+
webhooks: WebhookEmitter;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
const err = (code: string, message: string) => ({ error: { code, message } });
|
|
@@ -210,7 +213,7 @@ interface MemberUnionRow {
|
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
export function registerProjectRoutes(api: OpenAPIHono, deps: ProjectRouteDeps): void {
|
|
213
|
-
const { db } = deps;
|
|
216
|
+
const { db, webhooks } = deps;
|
|
214
217
|
|
|
215
218
|
// Projects are a workspace-level surface: a guest membership exists for
|
|
216
219
|
// principal resolution only (D2) and is refused the whole subtree, flat, on
|
|
@@ -362,6 +365,12 @@ export function registerProjectRoutes(api: OpenAPIHono, deps: ProjectRouteDeps):
|
|
|
362
365
|
role: 'manager',
|
|
363
366
|
addedBy: principal.userId
|
|
364
367
|
});
|
|
368
|
+
await webhooks.emit(tx, {
|
|
369
|
+
workspaceId: principal.workspaceId,
|
|
370
|
+
type: 'project.created',
|
|
371
|
+
subjects: { project: project!.id },
|
|
372
|
+
data: { projectId: project!.id, name: body.name, createdBy: principal.userId }
|
|
373
|
+
});
|
|
365
374
|
return readProject(tx, principal, project!.id);
|
|
366
375
|
});
|
|
367
376
|
// The principal resolved from an active membership a moment ago; losing
|
|
@@ -435,6 +444,12 @@ export function registerProjectRoutes(api: OpenAPIHono, deps: ProjectRouteDeps):
|
|
|
435
444
|
.returning({ id: projects.id });
|
|
436
445
|
if (updated.length === 0) return c.json(archived(), 409);
|
|
437
446
|
const project = (await readProject(db, principal, id))!;
|
|
447
|
+
await webhooks.emit(db, {
|
|
448
|
+
workspaceId: principal.workspaceId,
|
|
449
|
+
type: 'project.archived',
|
|
450
|
+
subjects: { project: id },
|
|
451
|
+
data: { projectId: id, name: project.name }
|
|
452
|
+
});
|
|
438
453
|
c.set('audit', { action: 'project.archive', resourceType: 'project', resourceId: id });
|
|
439
454
|
return c.json(toWire(project), 200);
|
|
440
455
|
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
import type { Principal } from '@antasphere/chassis-contract';
|
|
3
|
+
import {
|
|
4
|
+
webhookCatalogRoute,
|
|
5
|
+
webhookCreateRoute,
|
|
6
|
+
webhookDeleteRoute,
|
|
7
|
+
webhookDeliveriesListRoute,
|
|
8
|
+
webhookDeliveryGetRoute,
|
|
9
|
+
webhookGetRoute,
|
|
10
|
+
webhookRedeliverRoute,
|
|
11
|
+
webhookRotateSecretRoute,
|
|
12
|
+
webhooksListRoute,
|
|
13
|
+
webhookTestRoute,
|
|
14
|
+
webhookUpdateRoute
|
|
15
|
+
} from '@antasphere/chassis-contract/routes';
|
|
16
|
+
import { requireAuth, requireNonGuest } from '../middleware/auth-context.js';
|
|
17
|
+
import { catalogToWire, WebhookRefusedError, type WebhookService } from '../webhooks/index.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The webhook routes (PRDCT-3482), the same on both editions and never
|
|
21
|
+
* hub-managed: a webhook is the tool's, on a self-hosted instance and on
|
|
22
|
+
* the cloud alike (Romain's rule of 27 September 2026).
|
|
23
|
+
*
|
|
24
|
+
* A webhook is a standing read of the workspace to a URL of the caller's
|
|
25
|
+
* choice, so the whole family, reads included, is an OWNER's or an ADMIN's
|
|
26
|
+
* act, checked inside each handler (never a path-pattern `requireRole`, the
|
|
27
|
+
* `/thing/:id` trap: the catalog lives under the same prefix and is every
|
|
28
|
+
* non-guest member's). A guest is refused flat; a session a demo link
|
|
29
|
+
* opened is refused every write (`DEMO_SESSION_REFUSED_API_ROUTES`); a
|
|
30
|
+
* machine principal needs the tool's dedicated scope (`middleware/scopes.ts`).
|
|
31
|
+
* An endpoint of another workspace reads as absent: 404, never 403.
|
|
32
|
+
*
|
|
33
|
+
* The secret is answered ONCE, at creation and at rotation, and never
|
|
34
|
+
* written to the audit row.
|
|
35
|
+
*/
|
|
36
|
+
export interface WebhookRouteDeps {
|
|
37
|
+
service: WebhookService;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const err = (code: string, message: string) => ({ error: { code, message } });
|
|
41
|
+
const FORBIDDEN = err('forbidden', 'Only an owner or an admin manages webhooks');
|
|
42
|
+
const NOT_FOUND = err('not_found', 'Webhook not found');
|
|
43
|
+
const DELIVERY_NOT_FOUND = err('not_found', 'Delivery not found');
|
|
44
|
+
|
|
45
|
+
const canManage = (principal: Principal) => principal.role === 'owner' || principal.role === 'admin';
|
|
46
|
+
|
|
47
|
+
export function registerWebhookRoutes(api: OpenAPIHono, deps: WebhookRouteDeps): void {
|
|
48
|
+
const { service } = deps;
|
|
49
|
+
|
|
50
|
+
for (const path of ['/webhooks', '/webhooks/*']) {
|
|
51
|
+
api.use(path, requireAuth());
|
|
52
|
+
api.use(path, requireNonGuest());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The catalog BEFORE the `{id}` routes: `events` is not an id. Every
|
|
56
|
+
// non-guest member reads it (an agent discovers the types with a read key).
|
|
57
|
+
api.openapi(webhookCatalogRoute, async (c) => c.json(catalogToWire(service.catalog), 200));
|
|
58
|
+
|
|
59
|
+
api.openapi(webhooksListRoute, async (c) => {
|
|
60
|
+
const principal = c.get('principal')!;
|
|
61
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
62
|
+
const { cursor, limit } = c.req.valid('query');
|
|
63
|
+
return c.json(await service.list(principal.workspaceId, cursor, limit), 200);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
api.openapi(webhookCreateRoute, async (c) => {
|
|
67
|
+
const principal = c.get('principal')!;
|
|
68
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
69
|
+
const body = c.req.valid('json');
|
|
70
|
+
let created: Awaited<ReturnType<WebhookService['create']>>;
|
|
71
|
+
try {
|
|
72
|
+
created = await service.create(principal.workspaceId, principal.userId, body);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e instanceof WebhookRefusedError && e.code !== 'not_found')
|
|
75
|
+
return c.json(err(e.code, e.message), 400);
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
c.set('audit', {
|
|
79
|
+
action: 'webhook.create',
|
|
80
|
+
resourceType: 'webhook',
|
|
81
|
+
resourceId: created.endpoint.id,
|
|
82
|
+
metadata: {
|
|
83
|
+
url: created.endpoint.url,
|
|
84
|
+
events: created.endpoint.events,
|
|
85
|
+
filters: created.endpoint.filters
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return c.json({ ...created.endpoint, secret: created.secret }, 201);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
api.openapi(webhookGetRoute, async (c) => {
|
|
92
|
+
const principal = c.get('principal')!;
|
|
93
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
94
|
+
const { id } = c.req.valid('param');
|
|
95
|
+
const endpoint = await service.get(principal.workspaceId, id);
|
|
96
|
+
return endpoint ? c.json(endpoint, 200) : c.json(NOT_FOUND, 404);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
api.openapi(webhookUpdateRoute, async (c) => {
|
|
100
|
+
const principal = c.get('principal')!;
|
|
101
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
102
|
+
const { id } = c.req.valid('param');
|
|
103
|
+
const body = c.req.valid('json');
|
|
104
|
+
let updated: Awaited<ReturnType<WebhookService['update']>>;
|
|
105
|
+
try {
|
|
106
|
+
updated = await service.update(principal.workspaceId, id, body);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (e instanceof WebhookRefusedError && e.code !== 'not_found')
|
|
109
|
+
return c.json(err(e.code, e.message), 400);
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
if (!updated) return c.json(NOT_FOUND, 404);
|
|
113
|
+
c.set('audit', {
|
|
114
|
+
action: 'webhook.update',
|
|
115
|
+
resourceType: 'webhook',
|
|
116
|
+
resourceId: id,
|
|
117
|
+
metadata: { fields: Object.keys(body) }
|
|
118
|
+
});
|
|
119
|
+
return c.json(updated, 200);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
api.openapi(webhookDeleteRoute, async (c) => {
|
|
123
|
+
const principal = c.get('principal')!;
|
|
124
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
125
|
+
const { id } = c.req.valid('param');
|
|
126
|
+
const removed = await service.remove(principal.workspaceId, id);
|
|
127
|
+
if (!removed) return c.json(NOT_FOUND, 404);
|
|
128
|
+
c.set('audit', {
|
|
129
|
+
action: 'webhook.delete',
|
|
130
|
+
resourceType: 'webhook',
|
|
131
|
+
resourceId: id,
|
|
132
|
+
metadata: { url: removed.url }
|
|
133
|
+
});
|
|
134
|
+
return c.json(removed, 200);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
api.openapi(webhookRotateSecretRoute, async (c) => {
|
|
138
|
+
const principal = c.get('principal')!;
|
|
139
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
140
|
+
const { id } = c.req.valid('param');
|
|
141
|
+
const rotated = await service.rotateSecret(principal.workspaceId, id);
|
|
142
|
+
if (!rotated) return c.json(NOT_FOUND, 404);
|
|
143
|
+
c.set('audit', {
|
|
144
|
+
action: 'webhook.rotate_secret',
|
|
145
|
+
resourceType: 'webhook',
|
|
146
|
+
resourceId: id,
|
|
147
|
+
metadata: { previousSecretExpiresAt: rotated.previousSecretExpiresAt }
|
|
148
|
+
});
|
|
149
|
+
return c.json(
|
|
150
|
+
{
|
|
151
|
+
...rotated.endpoint,
|
|
152
|
+
secret: rotated.secret,
|
|
153
|
+
previousSecretExpiresAt: rotated.previousSecretExpiresAt
|
|
154
|
+
},
|
|
155
|
+
200
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
api.openapi(webhookTestRoute, async (c) => {
|
|
160
|
+
const principal = c.get('principal')!;
|
|
161
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
162
|
+
const { id } = c.req.valid('param');
|
|
163
|
+
const delivery = await service.test(principal.workspaceId, id, principal.userId);
|
|
164
|
+
if (!delivery) return c.json(NOT_FOUND, 404);
|
|
165
|
+
c.set('audit', {
|
|
166
|
+
action: 'webhook.test',
|
|
167
|
+
resourceType: 'webhook',
|
|
168
|
+
resourceId: id,
|
|
169
|
+
metadata: { deliveryId: delivery.id, status: delivery.status, lastStatusCode: delivery.lastStatusCode }
|
|
170
|
+
});
|
|
171
|
+
return c.json({ delivery }, 200);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
api.openapi(webhookDeliveriesListRoute, async (c) => {
|
|
175
|
+
const principal = c.get('principal')!;
|
|
176
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
177
|
+
const { id } = c.req.valid('param');
|
|
178
|
+
const { cursor, limit, status } = c.req.valid('query');
|
|
179
|
+
const page = await service.listDeliveries(principal.workspaceId, id, { cursor, limit, status });
|
|
180
|
+
return page ? c.json(page, 200) : c.json(NOT_FOUND, 404);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
api.openapi(webhookDeliveryGetRoute, async (c) => {
|
|
184
|
+
const principal = c.get('principal')!;
|
|
185
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
186
|
+
const { id, deliveryId } = c.req.valid('param');
|
|
187
|
+
const delivery = await service.getDelivery(principal.workspaceId, id, deliveryId);
|
|
188
|
+
return delivery ? c.json(delivery, 200) : c.json(DELIVERY_NOT_FOUND, 404);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
api.openapi(webhookRedeliverRoute, async (c) => {
|
|
192
|
+
const principal = c.get('principal')!;
|
|
193
|
+
if (!canManage(principal)) return c.json(FORBIDDEN, 403);
|
|
194
|
+
const { id, deliveryId } = c.req.valid('param');
|
|
195
|
+
const delivery = await service.redeliver(principal.workspaceId, id, deliveryId);
|
|
196
|
+
if (!delivery) return c.json(DELIVERY_NOT_FOUND, 404);
|
|
197
|
+
c.set('audit', {
|
|
198
|
+
action: 'webhook.redeliver',
|
|
199
|
+
resourceType: 'webhook',
|
|
200
|
+
resourceId: id,
|
|
201
|
+
metadata: { deliveryId: delivery.id, eventId: delivery.eventId, eventType: delivery.eventType }
|
|
202
|
+
});
|
|
203
|
+
return c.json(delivery, 201);
|
|
204
|
+
});
|
|
205
|
+
}
|
package/src/boot.ts
CHANGED
|
@@ -52,6 +52,8 @@ import { createOtel } from './observability/index.js';
|
|
|
52
52
|
import { bindEditionSeams } from './platform/index.js';
|
|
53
53
|
import { AllowAllEntitlements } from './platform/index.js';
|
|
54
54
|
import { EventBus } from './platform/index.js';
|
|
55
|
+
import { buildWebhookCatalog, WebhookService, webhookJobs } from './webhooks/index.js';
|
|
56
|
+
import { bindMembershipRemoval } from './members/removal.js';
|
|
55
57
|
import { LocalIdentityProvider } from './platform/index.js';
|
|
56
58
|
import { createRegistry } from './platform/index.js';
|
|
57
59
|
import { NoopUsageSink } from './platform/index.js';
|
|
@@ -540,6 +542,26 @@ export async function bootPlatform<
|
|
|
540
542
|
}
|
|
541
543
|
}
|
|
542
544
|
|
|
545
|
+
// Outbound webhooks (PRDCT-3482): the catalog checked, the service built
|
|
546
|
+
// BEFORE the job runtime (its four queues are declared to it like a tool's
|
|
547
|
+
// own) and attached to it right after; a tool's `services` slot receives
|
|
548
|
+
// the emitter, the API app the service, the bus the chassis's own events.
|
|
549
|
+
const webhookCatalog = buildWebhookCatalog(tool.webhooks);
|
|
550
|
+
const webhooks = new WebhookService({
|
|
551
|
+
db: db.db,
|
|
552
|
+
logger,
|
|
553
|
+
authSecret,
|
|
554
|
+
identity: tool.identity,
|
|
555
|
+
version: tool.runtime.version,
|
|
556
|
+
policy: { allowPrivateNetworks: env.WEBHOOKS_ALLOW_PRIVATE_NETWORKS, httpsOnly: env.EDITION === 'cloud' },
|
|
557
|
+
email,
|
|
558
|
+
brand: { name: tool.identity.displayName, tagline: tool.copy.mail.tagline },
|
|
559
|
+
publicBaseUrl: env.PUBLIC_BASE_URL,
|
|
560
|
+
catalog: webhookCatalog,
|
|
561
|
+
dials: overrides.webhookDials,
|
|
562
|
+
fetchOptions: overrides.webhookFetch
|
|
563
|
+
});
|
|
564
|
+
|
|
543
565
|
// The live user-scoped federation stack (cloud only, internal/federation.md):
|
|
544
566
|
// each user's OWN hub grant (offline_access refresh token on the account
|
|
545
567
|
// row) → as-the-user `GET /orgs` reads → the org reconciler that the
|
|
@@ -611,7 +633,7 @@ export async function bootPlatform<
|
|
|
611
633
|
audit,
|
|
612
634
|
logger,
|
|
613
635
|
dials: hubDials,
|
|
614
|
-
onMembershipRemoval: tool.membershipRemoval,
|
|
636
|
+
onMembershipRemoval: bindMembershipRemoval(tool.membershipRemoval, { webhooks }),
|
|
615
637
|
...(overrides.hubNow ? { now: overrides.hubNow } : {})
|
|
616
638
|
})
|
|
617
639
|
: undefined;
|
|
@@ -662,9 +684,14 @@ export async function bootPlatform<
|
|
|
662
684
|
overrides.usageDownstream ?? hubUsagePoster ?? new NoopUsageSink(),
|
|
663
685
|
auth,
|
|
664
686
|
audit,
|
|
665
|
-
|
|
687
|
+
[
|
|
688
|
+
...webhookJobs(webhooks, logger, env.WEBHOOK_DELIVERY_RETENTION_DAYS),
|
|
689
|
+
...(tool.jobs?.({ env, db: db.db, logger, getTool: () => domainRef.current }) ?? [])
|
|
690
|
+
],
|
|
666
691
|
usageRetry
|
|
667
692
|
);
|
|
693
|
+
webhooks.attach(jobs.boss);
|
|
694
|
+
webhooks.bindBus(events.chassisView());
|
|
668
695
|
// The tool's billing-rail declarations (slot 22), asserted once here: a
|
|
669
696
|
// route that meters an action the price book does not know, or a limit no
|
|
670
697
|
// tier values, stops the boot naming the route.
|
|
@@ -806,6 +833,7 @@ export async function bootPlatform<
|
|
|
806
833
|
events,
|
|
807
834
|
authSecret,
|
|
808
835
|
timers: jobs.timers,
|
|
836
|
+
webhooks,
|
|
809
837
|
overrides: overrides.tool
|
|
810
838
|
};
|
|
811
839
|
const domain = tool.services(serviceCore);
|
|
@@ -849,6 +877,7 @@ export async function bootPlatform<
|
|
|
849
877
|
principalGate: seams.principalGate,
|
|
850
878
|
entitlements,
|
|
851
879
|
entitlementCloud,
|
|
880
|
+
webhooks,
|
|
852
881
|
// Cloud only (PRDCT-2443): POST /workspaces creates the organization at
|
|
853
882
|
// the hub AS THE CALLER, then forces their reconcile. undefined on oss —
|
|
854
883
|
// the route creates locally there.
|
|
@@ -885,7 +914,8 @@ export async function bootPlatform<
|
|
|
885
914
|
...(hubGrant?.promMetrics ?? []),
|
|
886
915
|
...(hubUsagePoster?.promMetrics ?? []),
|
|
887
916
|
...(hubCreditCheck?.promMetrics ?? []),
|
|
888
|
-
...jobs.promMetrics
|
|
917
|
+
...jobs.promMetrics,
|
|
918
|
+
...webhooks.promMetrics
|
|
889
919
|
]) {
|
|
890
920
|
metrics.registry.registerMetric(metric);
|
|
891
921
|
}
|
|
@@ -952,5 +982,19 @@ export async function bootPlatform<
|
|
|
952
982
|
state.reason = readinessRefusal;
|
|
953
983
|
}
|
|
954
984
|
|
|
955
|
-
return {
|
|
985
|
+
return {
|
|
986
|
+
app,
|
|
987
|
+
env,
|
|
988
|
+
logger,
|
|
989
|
+
state,
|
|
990
|
+
db,
|
|
991
|
+
auth,
|
|
992
|
+
registry,
|
|
993
|
+
jobs,
|
|
994
|
+
email,
|
|
995
|
+
otel,
|
|
996
|
+
authSecret,
|
|
997
|
+
webhooks,
|
|
998
|
+
tool: domain
|
|
999
|
+
};
|
|
956
1000
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFileSync, statSync, type Stats } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The tool's own CLI package, as the release packed it (PRDCT-3312): the
|
|
6
|
+
* folder `CLI_PACKAGE_DIR` names holds `package.tgz` (the output of
|
|
7
|
+
* `pnpm pack` of the tool's `packages/cli`) beside a copy of that package's
|
|
8
|
+
* `package.json`. The instance reads the pair ONCE at boot and serves the
|
|
9
|
+
* tarball on `POST /cli/package` (api/cli-package.ts, cloud only) to a member
|
|
10
|
+
* the hub let into the tool; discovery advertises `cli { name, version }` so
|
|
11
|
+
* the `team` command knows what it installs and whether to update.
|
|
12
|
+
*
|
|
13
|
+
* A folder that is named but does not hold the pair, or whose `package.json`
|
|
14
|
+
* carries no string `name` and `version`, refuses the boot (env.ts): a
|
|
15
|
+
* misconfiguration is proven at the image, never discovered by a person's
|
|
16
|
+
* install. Pure: nothing runs at import; the reads are synchronous and happen
|
|
17
|
+
* when the boot asks.
|
|
18
|
+
*/
|
|
19
|
+
export interface CliPackage {
|
|
20
|
+
/** The package's npm name, from its `package.json` (e.g. `@scope/tool`). */
|
|
21
|
+
name: string;
|
|
22
|
+
/** The package's version, from its `package.json`. */
|
|
23
|
+
version: string;
|
|
24
|
+
/** The tarball's path (the configured folder joined with `package.tgz`), what the route streams. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** The tarball's size when the boot read it (the route stats the file again when it serves). */
|
|
27
|
+
sizeBytes: number;
|
|
28
|
+
/** npm's own pack name for the pair, the attachment's filename. */
|
|
29
|
+
filename: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The tarball's name inside the folder, whatever the package is called. */
|
|
33
|
+
export const CLI_PACKAGE_TARBALL = 'package.tgz';
|
|
34
|
+
/** The manifest copy beside it. */
|
|
35
|
+
export const CLI_PACKAGE_MANIFEST = 'package.json';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* npm's own pack filename: the scope's `@` dropped and its `/` turned into
|
|
39
|
+
* `-` (`@scope/tool` + `1.2.3` → `scope-tool-1.2.3.tgz`); an unscoped name is
|
|
40
|
+
* kept as it is (`tool` → `tool-1.2.3.tgz`).
|
|
41
|
+
*/
|
|
42
|
+
export function packFilename(name: string, version: string): string {
|
|
43
|
+
const base = name.startsWith('@') ? name.slice(1).replace('/', '-') : name;
|
|
44
|
+
return `${base}-${version}.tgz`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read the folder `CLI_PACKAGE_DIR` names. `undefined` (the variable unset)
|
|
49
|
+
* answers null: the instance serves no CLI. A named folder answers the
|
|
50
|
+
* package, or THROWS an Error naming the folder and the missing piece.
|
|
51
|
+
*/
|
|
52
|
+
export function loadCliPackage(dir: string | undefined): CliPackage | null {
|
|
53
|
+
if (dir === undefined) return null;
|
|
54
|
+
const refuse = (what: string): never => {
|
|
55
|
+
throw new Error(`CLI_PACKAGE_DIR=${dir}: ${what}`);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const tarball = join(dir, CLI_PACKAGE_TARBALL);
|
|
59
|
+
let stat: Stats;
|
|
60
|
+
try {
|
|
61
|
+
stat = statSync(tarball);
|
|
62
|
+
} catch {
|
|
63
|
+
return refuse(`no ${CLI_PACKAGE_TARBALL} in the folder (the output of \`pnpm pack\` of the CLI package)`);
|
|
64
|
+
}
|
|
65
|
+
if (!stat.isFile()) return refuse(`${CLI_PACKAGE_TARBALL} is not a file`);
|
|
66
|
+
|
|
67
|
+
let raw: string;
|
|
68
|
+
try {
|
|
69
|
+
raw = readFileSync(join(dir, CLI_PACKAGE_MANIFEST), 'utf8');
|
|
70
|
+
} catch {
|
|
71
|
+
return refuse(`no ${CLI_PACKAGE_MANIFEST} in the folder (a copy of the CLI package's manifest)`);
|
|
72
|
+
}
|
|
73
|
+
let manifest: unknown;
|
|
74
|
+
try {
|
|
75
|
+
manifest = JSON.parse(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
return refuse(`${CLI_PACKAGE_MANIFEST} is not valid JSON`);
|
|
78
|
+
}
|
|
79
|
+
const { name, version } = (manifest ?? {}) as { name?: unknown; version?: unknown };
|
|
80
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
81
|
+
return refuse(`${CLI_PACKAGE_MANIFEST} carries no string "name"`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof version !== 'string' || version.length === 0) {
|
|
84
|
+
return refuse(`${CLI_PACKAGE_MANIFEST} carries no string "version"`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { name, version, path: tarball, sizeBytes: stat.size, filename: packFilename(name, version) };
|
|
88
|
+
}
|
package/src/email/index.ts
CHANGED