@app-vitals/agent-admin 1.3.0 → 1.5.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.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Unit tests for the GitHubEnterpriseApp serializer: metadata plus a
3
+ * "key set" flag only, never the private key (plain or encrypted).
4
+ */
5
+
6
+ import { describe, expect, it } from "bun:test";
7
+ import { toGitHubEnterpriseAppMetadata } from "./github-enterprise-apps.ts";
8
+
9
+ describe("toGitHubEnterpriseAppMetadata", () => {
10
+ const row = {
11
+ role: "installer",
12
+ appId: "123",
13
+ slug: "my-app",
14
+ enterpriseSlug: "acme",
15
+ privateKeyEncrypted: "aa:bb:cc",
16
+ lastValidationStatus: "ok",
17
+ lastValidatedAt: new Date("2026-01-01T00:00:00Z"),
18
+ lastValidationError: null,
19
+ createdAt: new Date("2026-01-01T00:00:00Z"),
20
+ updatedAt: new Date("2026-01-02T00:00:00Z"),
21
+ };
22
+
23
+ it("returns metadata and keySet, with no key material", () => {
24
+ const out = toGitHubEnterpriseAppMetadata(row);
25
+ expect(out.keySet).toBe(true);
26
+ expect(out).toMatchObject({
27
+ role: "installer",
28
+ appId: "123",
29
+ slug: "my-app",
30
+ });
31
+ expect(Object.keys(out).some((k) => /key/i.test(k) && k !== "keySet")).toBe(
32
+ false,
33
+ );
34
+ expect(JSON.stringify(out)).not.toContain("aa:bb:cc");
35
+ });
36
+
37
+ it("keySet is false when no ciphertext is stored", () => {
38
+ expect(
39
+ toGitHubEnterpriseAppMetadata({ ...row, privateKeyEncrypted: "" }).keySet,
40
+ ).toBe(false);
41
+ });
42
+ });
@@ -895,6 +895,43 @@ export const PatchAgentPluginBodySchema = z
895
895
  })
896
896
  .openapi("PatchAgentPluginBody");
897
897
 
898
+ // ─── AgentMarketplace ─────────────────────────────────────────────────────────
899
+
900
+ export const AgentMarketplaceSchema = z
901
+ .object({
902
+ id: z.string().openapi({ example: "clx1234567890" }),
903
+ agentId: z.string().openapi({ example: "clx1234567890" }),
904
+ name: z.string().openapi({ example: "shipwright" }),
905
+ source: z.string().openapi({ example: "app-vitals/shipwright" }),
906
+ ref: z.string().nullable().openapi({ example: "main" }),
907
+ createdAt: z
908
+ .string()
909
+ .datetime()
910
+ .openapi({ example: "2026-01-01T00:00:00.000Z" }),
911
+ updatedAt: z
912
+ .string()
913
+ .datetime()
914
+ .openapi({ example: "2026-01-01T00:00:00.000Z" }),
915
+ })
916
+ .openapi("AgentMarketplace");
917
+
918
+ export type AgentMarketplace = z.infer<typeof AgentMarketplaceSchema>;
919
+
920
+ /**
921
+ * `source` / `name` / `ref` are validated by AgentMarketplaceService (422), not
922
+ * here — the Zod layer answers 400.
923
+ */
924
+ export const UpsertAgentMarketplaceBodySchema = z
925
+ .object({
926
+ name: z.string().openapi({ example: "shipwright" }),
927
+ source: z.string().openapi({
928
+ description: "GitHub `owner/repo` or an https git URL",
929
+ example: "app-vitals/shipwright",
930
+ }),
931
+ ref: z.string().nullable().optional().openapi({ example: "main" }),
932
+ })
933
+ .openapi("UpsertAgentMarketplaceBody");
934
+
898
935
  // ─── Pipeline phases ────────────────────────────────────────────────────────────
899
936
  //
900
937
  // The six pipeline phases a methodology override can target — the keys of the
@@ -1084,6 +1121,11 @@ export const TokenIdParamSchema = z.object({
1084
1121
  tokenId: z.string().openapi({ example: "clx0987654321" }),
1085
1122
  });
1086
1123
 
1124
+ export const MarketplaceIdParamSchema = z.object({
1125
+ id: z.string().openapi({ example: "clx1234567890" }),
1126
+ marketplaceId: z.string().openapi({ example: "clx0987654321" }),
1127
+ });
1128
+
1087
1129
  export const EnvKeyParamSchema = z.object({
1088
1130
  id: z.string().openapi({ example: "clx1234567890" }),
1089
1131
  key: z.string().openapi({ example: "MY_VAR" }),
@@ -1298,6 +1340,14 @@ const AgentConfigPluginSchema = z
1298
1340
  })
1299
1341
  .openapi("AgentConfigPlugin");
1300
1342
 
1343
+ const AgentConfigMarketplaceSchema = z
1344
+ .object({
1345
+ name: z.string().openapi({ example: "shipwright" }),
1346
+ source: z.string().openapi({ example: "app-vitals/shipwright" }),
1347
+ ref: z.string().nullable().openapi({ example: "main" }),
1348
+ })
1349
+ .openapi("AgentConfigMarketplace");
1350
+
1301
1351
  export const AgentConfigResponseSchema = z
1302
1352
  .object({
1303
1353
  env: z
@@ -1305,6 +1355,12 @@ export const AgentConfigResponseSchema = z
1305
1355
  .openapi({ example: { SLACK_BOT_TOKEN: "xoxb-..." } }),
1306
1356
  allowedTools: z.array(z.string()).openapi({ example: ["Read", "Write"] }),
1307
1357
  plugins: z.array(AgentConfigPluginSchema),
1358
+ /**
1359
+ * PMK-1.2: remote plugin marketplaces to register before plugin install.
1360
+ * Optional so existing fixtures/doubles keep compiling; the handler
1361
+ * always populates it (empty array when the agent has none).
1362
+ */
1363
+ marketplaces: z.array(AgentConfigMarketplaceSchema).optional(),
1308
1364
  repos: z.array(z.string()).openapi({ example: ["org/repo1", "org/repo2"] }),
1309
1365
  reviewAuthorAllowlist: z
1310
1366
  .array(z.string())
@@ -10,6 +10,7 @@ import { type AgentRuntimeDeps, createAgentRuntimeApp } from "./api.ts";
10
10
  import {
11
11
  type AgentCronJob,
12
12
  AgentCronJobSchema,
13
+ AgentMarketplaceSchema,
13
14
  type AgentPlugin,
14
15
  AgentPluginSchema,
15
16
  type AgentToken,
@@ -18,6 +19,7 @@ import {
18
19
  AgentToolSchema,
19
20
  ErrorSchema,
20
21
  PatchAgentBodySchema,
22
+ UpsertAgentMarketplaceBodySchema,
21
23
  } from "./openapi-schemas.ts";
22
24
 
23
25
  // ─── Fixtures ─────────────────────────────────────────────────────────────────
@@ -381,3 +383,30 @@ describe("PatchAgentBodySchema.podServiceAccountName (COS-49.1)", () => {
381
383
  }
382
384
  });
383
385
  });
386
+
387
+ // ─── AgentMarketplace (PMK-1.2) ───────────────────────────────────────────────
388
+
389
+ describe("AgentMarketplace schemas", () => {
390
+ test("AgentMarketplaceSchema accepts a row with a null ref", () => {
391
+ const result = AgentMarketplaceSchema.safeParse({
392
+ id: "m1",
393
+ agentId: "a1",
394
+ name: "shipwright",
395
+ source: "app-vitals/shipwright",
396
+ ref: null,
397
+ createdAt: "2026-01-01T00:00:00.000Z",
398
+ updatedAt: "2026-01-01T00:00:00.000Z",
399
+ });
400
+ expect(result.success).toBe(true);
401
+ });
402
+
403
+ test("UpsertAgentMarketplaceBodySchema leaves source validation to the service (422)", () => {
404
+ expect(
405
+ UpsertAgentMarketplaceBodySchema.safeParse({ name: "m", source: "bad" })
406
+ .success,
407
+ ).toBe(true);
408
+ expect(
409
+ UpsertAgentMarketplaceBodySchema.safeParse({ name: "m" }).success,
410
+ ).toBe(false);
411
+ });
412
+ });
@@ -58,3 +58,28 @@ export function makeTokenCrypto(
58
58
  },
59
59
  };
60
60
  }
61
+
62
+ /**
63
+ * Strict TokenCrypto for credentials that must never be stored or passed on as
64
+ * plain text (e.g. GitHub Enterprise App private keys).
65
+ *
66
+ * Differences from makeTokenCrypto(): returns null when no key is configured
67
+ * (callers must refuse to save, not fall back to identity), and decrypt()
68
+ * throws on any failure instead of returning the stored value as legacy
69
+ * plain text. A malformed key still throws at construction. Error messages
70
+ * never include the input value.
71
+ */
72
+ export function makeStrictTokenCrypto(
73
+ key = process.env.SHIPWRIGHT_ENCRYPTION_KEY,
74
+ ): TokenCrypto | null {
75
+ if (!key) return null;
76
+ if (!isValidKeyHex(key)) {
77
+ throw new Error(
78
+ `SHIPWRIGHT_ENCRYPTION_KEY must be a 64-character hex string (32 bytes for AES-256-GCM), but got ${key.length} character(s). Generate one with: openssl rand -hex 32`,
79
+ );
80
+ }
81
+ return {
82
+ encrypt: (token) => encrypt(token, key),
83
+ decrypt: (encrypted) => decrypt(encrypted, key),
84
+ };
85
+ }
@@ -6,7 +6,11 @@
6
6
 
7
7
  import { afterEach, describe, expect, it } from "bun:test";
8
8
  import { decrypt, encrypt, isValidKeyHex } from "./crypto.ts";
9
- import { identityCrypto, makeTokenCrypto } from "./token-crypto.ts";
9
+ import {
10
+ identityCrypto,
11
+ makeStrictTokenCrypto,
12
+ makeTokenCrypto,
13
+ } from "./token-crypto.ts";
10
14
 
11
15
  // ─── Test key ─────────────────────────────────────────────────────────────────
12
16
  // A known 64-char hex key (32 bytes) for deterministic tests.
@@ -180,3 +184,22 @@ describe("identityCrypto", () => {
180
184
  expect(identityCrypto.decrypt("hello")).toBe("hello");
181
185
  });
182
186
  });
187
+
188
+ describe("makeStrictTokenCrypto", () => {
189
+ const KEY = "c".repeat(64);
190
+
191
+ it("returns null when no key is configured (no identity fallback)", () => {
192
+ expect(makeStrictTokenCrypto(undefined)).toBeNull();
193
+ expect(makeStrictTokenCrypto("")).toBeNull();
194
+ });
195
+
196
+ it("throws on a malformed key", () => {
197
+ expect(() => makeStrictTokenCrypto("short")).toThrow(/64-character/);
198
+ });
199
+
200
+ it("round-trips and throws on non-ciphertext instead of passing it through", () => {
201
+ const c = makeStrictTokenCrypto(KEY);
202
+ expect(c?.decrypt(c.encrypt("secret"))).toBe("secret");
203
+ expect(() => c?.decrypt("legacy-plain-text")).toThrow();
204
+ });
205
+ });