@intentic/registry 1.176.3 → 1.209.1

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/README.md CHANGED
@@ -5,8 +5,8 @@ the result is shown in. Published to npm, and depended on by three otherwise-unr
5
5
  whole reason it is a package: the daemon that clones a registry, the site that builds the public gallery, and
6
6
  the [scanner](../../_tools/registry-scan) that writes the files. One zod schema instead of three that drift.
7
7
 
8
- See [Publish & the marketplace](https://intentic.dev/docs/extensions/publish/) for the author-facing version
9
- of everything below.
8
+ See [Publish & registries](https://intentic.dev/developers/publish/) for the author-facing version of everything
9
+ below, and [Verification & trust](https://intentic.dev/developers/verify/) for what each trust state claims.
10
10
 
11
11
  ## The shape of it
12
12
 
@@ -23,7 +23,7 @@ lists a team's agent plugins and its intentic extensions together.
23
23
 
24
24
  | File | Written by | Holds |
25
25
  | --- | --- | --- |
26
- | `.claude-plugin/marketplace.json` | humans, via pull request | every decision — what is listed, and at which commit and trust level |
26
+ | `.claude-plugin/marketplace.json` | humans + protected admission workflow | every decision — what is listed, the exact source, trust level, and source-bound security record |
27
27
  | `.claude-plugin/registry.generated.json` | the nightly scanner | only facts read back off the source host: stars, last push |
28
28
 
29
29
  Keeping the derived data out of the curated file is load-bearing, not tidiness. Star counts in the hand-edited
@@ -36,13 +36,37 @@ browse wire shape, so the app's list and the website's gallery are the same rows
36
36
 
37
37
  ## Trust, and what each state claims
38
38
 
39
- `listed` is the honest default: the pointer resolves, the manifest parses, the publisher owns the source repo,
40
- and **nobody read the code**. `verified` means a human read the source at that commit. `blocked` means
41
- known-malicious or known-broken, with the reason alongside.
39
+ In the official registry, `listed` means the exact source passed the deterministic scan and intentic agent audit,
40
+ but no human source review is claimed. `verified` means a human also read that same source. `blocked` means
41
+ known-malicious or known-broken, with the required reason alongside. A `verified` row without a
42
+ `securityReview`, a review copied from another repository/sha/subdirectory, or a blocked row without a reason does not parse.
43
+
44
+ `securityReview` records the repository, commit, subdirectory, both versioned policies, scanner and version,
45
+ timestamp, Trivy workflow run and agent-gate run. The official resolver accepts only current Trivy and Intentic
46
+ policies with that complete subject equal to the install pointer. A row missing either stays visible in the app
47
+ with install disabled and is omitted from the public gallery; it cannot become an update offer either. This is
48
+ the runtime backstop behind the registry's required PR check.
42
49
 
43
50
  A blocked entry stays in the file. Deleting the row hides it from people browsing and tells the people who
44
51
  already installed it nothing, which is backwards — they are the ones at risk. Absent on a third-party registry
45
52
  resolves to `listed`, because a registry that doesn't use the field hasn't asserted anything.
53
+ Third-party registries are their own admission boundary: their non-blocked rows remain installable without
54
+ adopting intentic's gate or policy, and the app states when they carry no audit record.
55
+
56
+ Installed sandboxes read these states back on a daily comparison, so trust reaches the people past the browse
57
+ moment too: a row turned `blocked` raises an advisory on the installed extension (and, by default, switches it
58
+ off), and `securityFix: true` on an entry marks its pinned commit as fixing a security problem in earlier ones
59
+ — the installed side promotes its update badge from ambient to loud, because there the OLD version is the
60
+ dangerous one. Both are asserted by pull request, like `trust`, and are worth exactly that review.
61
+
62
+ ## Tier, and what premium buys into
63
+
64
+ `tier` is the listing's price: `free` (the default, and the whole story for most rows) or `premium` — the
65
+ listing opts into the **creator pool**. A premium row needs an intentic membership, both surfaces badge it
66
+ before the click, and installing it **donates a published number of the member's credits to the publisher**
67
+ (once, deduped monthly — updates donate again at most monthly). No usage is metered or reported anywhere;
68
+ the deliberate install is the whole signal. The economics live in
69
+ [The creator pool](https://intentic.dev/earn/).
46
70
 
47
71
  ## The mark
48
72
 
@@ -75,6 +99,12 @@ because extension code runs trusted in the owner's browser and a branch name is
75
99
  break with a force-push. A registry entry without one still lists and still reads; it just can't be installed
76
100
  in a click.
77
101
 
102
+ The sha is necessary but does not confine the code. A browser entry runs in the app's JavaScript realm with
103
+ page, browser-storage and network access; server/process/agent/bin/environment contributions have their own
104
+ execution surfaces. The manifest gates cooperative calls through the extension API, not arbitrary browser
105
+ behavior. Deterministic scanning catches known dependency, secret and configuration findings; the agent audit
106
+ exists because neither those signatures nor manifest validation can answer malicious intent.
107
+
78
108
  ## Key files
79
109
 
80
110
  - [src/registry.ts](src/registry.ts) — the file format: what a registry repo contains.
@@ -1,14 +1,41 @@
1
1
  import { z } from "zod";
2
+ import { type RegistryInstall } from "./source.js";
2
3
  export declare const REGISTRY_FILE = ".claude-plugin/marketplace.json";
3
4
  export declare const REGISTRY_FACTS_FILE = ".claude-plugin/registry.generated.json";
4
5
  export declare const OFFICIAL_REGISTRY_URL = "https://github.com/intentic/registry";
5
6
  export declare const REGISTRY_TOPIC = "intentic-extension";
7
+ export declare const OFFICIAL_SECURITY_REVIEW_POLICY = "intentic-extension-security-v1";
8
+ export declare const OFFICIAL_SECURITY_REVIEWER = "intentic-agent-gate";
9
+ export declare const OFFICIAL_DETERMINISTIC_SCAN_POLICY = "intentic-extension-deterministic-v1";
10
+ export declare const OFFICIAL_DETERMINISTIC_SCANNER = "trivy";
11
+ export declare const OFFICIAL_DETERMINISTIC_SCANNER_VERSION = "0.72.0";
12
+ export declare const RegistrySecurityReviewSchema: z.ZodObject<{
13
+ sha: z.ZodString;
14
+ url: z.ZodString;
15
+ path: z.ZodOptional<z.ZodString>;
16
+ policy: z.ZodString;
17
+ reviewer: z.ZodString;
18
+ reviewedAt: z.ZodISODateTime;
19
+ runId: z.ZodString;
20
+ deterministic: z.ZodObject<{
21
+ policy: z.ZodString;
22
+ scanner: z.ZodString;
23
+ version: z.ZodString;
24
+ runId: z.ZodString;
25
+ }, z.core.$strip>;
26
+ }, z.core.$strip>;
27
+ export type RegistrySecurityReview = z.infer<typeof RegistrySecurityReviewSchema>;
6
28
  export declare const RegistryTrustSchema: z.ZodEnum<{
7
29
  blocked: "blocked";
8
30
  listed: "listed";
9
31
  verified: "verified";
10
32
  }>;
11
33
  export type RegistryTrust = z.infer<typeof RegistryTrustSchema>;
34
+ export declare const RegistryTierSchema: z.ZodEnum<{
35
+ free: "free";
36
+ premium: "premium";
37
+ }>;
38
+ export type RegistryTier = z.infer<typeof RegistryTierSchema>;
12
39
  export declare const RegistryFileSchema: z.ZodObject<{
13
40
  name: z.ZodString;
14
41
  metadata: z.ZodOptional<z.ZodObject<{
@@ -28,7 +55,28 @@ export declare const RegistryFileSchema: z.ZodObject<{
28
55
  verified: "verified";
29
56
  }>>;
30
57
  trustReason: z.ZodOptional<z.ZodString>;
58
+ securityReview: z.ZodOptional<z.ZodObject<{
59
+ sha: z.ZodString;
60
+ url: z.ZodString;
61
+ path: z.ZodOptional<z.ZodString>;
62
+ policy: z.ZodString;
63
+ reviewer: z.ZodString;
64
+ reviewedAt: z.ZodISODateTime;
65
+ runId: z.ZodString;
66
+ deterministic: z.ZodObject<{
67
+ policy: z.ZodString;
68
+ scanner: z.ZodString;
69
+ version: z.ZodString;
70
+ runId: z.ZodString;
71
+ }, z.core.$strip>;
72
+ }, z.core.$strip>>;
73
+ securityFix: z.ZodOptional<z.ZodBoolean>;
74
+ tier: z.ZodOptional<z.ZodEnum<{
75
+ free: "free";
76
+ premium: "premium";
77
+ }>>;
31
78
  category: z.ZodOptional<z.ZodString>;
79
+ art: z.ZodOptional<z.ZodString>;
32
80
  logo: z.ZodOptional<z.ZodString>;
33
81
  icon: z.ZodOptional<z.ZodString>;
34
82
  homepage: z.ZodOptional<z.ZodURL>;
@@ -36,12 +84,25 @@ export declare const RegistryFileSchema: z.ZodObject<{
36
84
  }, z.core.$strip>>;
37
85
  }, z.core.$strip>;
38
86
  export type RegistryFile = z.infer<typeof RegistryFileSchema>;
87
+ declare const RegistryChecksSchema: z.ZodObject<{
88
+ sha: z.ZodString;
89
+ manifest: z.ZodString;
90
+ bundle: z.ZodString;
91
+ engines: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>;
93
+ export type RegistryChecks = z.infer<typeof RegistryChecksSchema>;
39
94
  export declare const RegistryFactsSchema: z.ZodObject<{
40
95
  scannedAt: z.ZodString;
41
96
  entries: z.ZodArray<z.ZodObject<{
42
97
  name: z.ZodString;
43
98
  stars: z.ZodOptional<z.ZodNumber>;
44
99
  pushedAt: z.ZodOptional<z.ZodString>;
100
+ checks: z.ZodOptional<z.ZodObject<{
101
+ sha: z.ZodString;
102
+ manifest: z.ZodString;
103
+ bundle: z.ZodString;
104
+ engines: z.ZodOptional<z.ZodString>;
105
+ }, z.core.$strip>>;
45
106
  }, z.core.$strip>>;
46
107
  }, z.core.$strip>;
47
108
  export type RegistryFacts = z.infer<typeof RegistryFactsSchema>;
@@ -59,7 +120,29 @@ export declare const RegistryEntrySchema: z.ZodObject<{
59
120
  verified: "verified";
60
121
  }>;
61
122
  trustReason: z.ZodOptional<z.ZodString>;
123
+ securityReview: z.ZodOptional<z.ZodObject<{
124
+ sha: z.ZodString;
125
+ url: z.ZodString;
126
+ path: z.ZodOptional<z.ZodString>;
127
+ policy: z.ZodString;
128
+ reviewer: z.ZodString;
129
+ reviewedAt: z.ZodISODateTime;
130
+ runId: z.ZodString;
131
+ deterministic: z.ZodObject<{
132
+ policy: z.ZodString;
133
+ scanner: z.ZodString;
134
+ version: z.ZodString;
135
+ runId: z.ZodString;
136
+ }, z.core.$strip>;
137
+ }, z.core.$strip>>;
138
+ admitted: z.ZodBoolean;
139
+ securityFix: z.ZodOptional<z.ZodBoolean>;
140
+ tier: z.ZodEnum<{
141
+ free: "free";
142
+ premium: "premium";
143
+ }>;
62
144
  category: z.ZodOptional<z.ZodString>;
145
+ art: z.ZodOptional<z.ZodString>;
63
146
  logo: z.ZodOptional<z.ZodString>;
64
147
  icon: z.ZodOptional<z.ZodString>;
65
148
  homepage: z.ZodOptional<z.ZodString>;
@@ -70,8 +153,17 @@ export declare const RegistryEntrySchema: z.ZodObject<{
70
153
  }, z.core.$strip>>;
71
154
  stars: z.ZodOptional<z.ZodNumber>;
72
155
  pushedAt: z.ZodOptional<z.ZodString>;
156
+ checks: z.ZodOptional<z.ZodObject<{
157
+ sha: z.ZodString;
158
+ manifest: z.ZodString;
159
+ bundle: z.ZodString;
160
+ engines: z.ZodOptional<z.ZodString>;
161
+ }, z.core.$strip>>;
73
162
  }, z.core.$strip>;
74
163
  export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
164
+ export declare const isOfficialRegistryUrl: (value: string) => boolean;
165
+ export declare const isCurrentSecurityReview: (review: RegistrySecurityReview | undefined, install: RegistryInstall | undefined) => boolean;
75
166
  export declare const resolveRegistry: (file: RegistryFile, facts: RegistryFacts | undefined, registryUrl: string) => RegistryEntry[];
76
167
  export declare const compareEntries: (a: RegistryEntry, b: RegistryEntry) => number;
168
+ export {};
77
169
  //# sourceMappingURL=registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAmBxB,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAC/D,eAAO,MAAM,mBAAmB,2CAA2C,CAAC;AAI5E,eAAO,MAAM,qBAAqB,yCAAyC,CAAC;AAI5E,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAanD,eAAO,MAAM,mBAAmB;;;;EAA4C,CAAC;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AA6BhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;iBAI7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAa9D,eAAO,MAAM,mBAAmB;;;;;;;iBAG9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;iBAiB9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE,eAAO,MAAM,eAAe,SAAU,YAAY,SAAS,aAAa,GAAG,SAAS,eAAe,MAAM,KAAG,aAAa,EAqBxH,CAAC;AASF,eAAO,MAAM,cAAc,MAAO,aAAa,KAAK,aAAa,KAAG,MAWnE,CAAC"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,eAAe,EAAwC,MAAM,aAAa,CAAC;AAkBzF,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAC/D,eAAO,MAAM,mBAAmB,2CAA2C,CAAC;AAI5E,eAAO,MAAM,qBAAqB,yCAAyC,CAAC;AAI5E,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAOnD,eAAO,MAAM,+BAA+B,mCAAmC,CAAC;AAChF,eAAO,MAAM,0BAA0B,wBAAwB,CAAC;AAChE,eAAO,MAAM,kCAAkC,wCAAwC,CAAC;AACxF,eAAO,MAAM,8BAA8B,UAAU,CAAC;AACtD,eAAO,MAAM,sCAAsC,WAAW,CAAC;AAI/D,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;iBAcvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAalF,eAAO,MAAM,mBAAmB;;;;EAA4C,CAAC;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAQhE,eAAO,MAAM,kBAAkB;;;EAA8B,CAAC;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAgE9D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiBzB,CAAC;AACP,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAW9D,QAAA,MAAM,oBAAoB;;;;;iBASxB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAWlE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;iBAG9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2B9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAsBhE,eAAO,MAAM,qBAAqB,UAAW,MAAM,KAAG,OAAwF,CAAC;AAE/I,eAAO,MAAM,uBAAuB,WAAY,sBAAsB,GAAG,SAAS,WAAW,eAAe,GAAG,SAAS,KAAG,OAShD,CAAC;AAI5E,eAAO,MAAM,eAAe,SAAU,YAAY,SAAS,aAAa,GAAG,SAAS,eAAe,MAAM,KAAG,aAAa,EAkCxH,CAAC;AASF,eAAO,MAAM,cAAc,MAAO,aAAa,KAAK,aAAa,KAAG,MAWnE,CAAC"}
package/dist/registry.js CHANGED
@@ -4,29 +4,96 @@ export const REGISTRY_FILE = ".claude-plugin/marketplace.json";
4
4
  export const REGISTRY_FACTS_FILE = ".claude-plugin/registry.generated.json";
5
5
  export const OFFICIAL_REGISTRY_URL = "https://github.com/intentic/registry";
6
6
  export const REGISTRY_TOPIC = "intentic-extension";
7
+ export const OFFICIAL_SECURITY_REVIEW_POLICY = "intentic-extension-security-v1";
8
+ export const OFFICIAL_SECURITY_REVIEWER = "intentic-agent-gate";
9
+ export const OFFICIAL_DETERMINISTIC_SCAN_POLICY = "intentic-extension-deterministic-v1";
10
+ export const OFFICIAL_DETERMINISTIC_SCANNER = "trivy";
11
+ export const OFFICIAL_DETERMINISTIC_SCANNER_VERSION = "0.72.0";
12
+ const FULL_SHA = /^[0-9a-f]{40}$/;
13
+ export const RegistrySecurityReviewSchema = z.object({
14
+ sha: z.string().regex(FULL_SHA, "must be a full lowercase commit sha"),
15
+ url: z.string().min(1),
16
+ path: z.string().min(1).optional(),
17
+ policy: z.string().min(1),
18
+ reviewer: z.string().min(1),
19
+ reviewedAt: z.iso.datetime(),
20
+ runId: z.string().min(1),
21
+ deterministic: z.object({
22
+ policy: z.string().min(1),
23
+ scanner: z.string().min(1),
24
+ version: z.string().min(1),
25
+ runId: z.string().min(1),
26
+ }),
27
+ });
7
28
  export const RegistryTrustSchema = z.enum(["verified", "listed", "blocked"]);
8
- const RegistryFileEntrySchema = z.object({
29
+ export const RegistryTierSchema = z.enum(["free", "premium"]);
30
+ const RegistryFileEntrySchema = z
31
+ .object({
9
32
  name: z.string(),
10
33
  description: z.string().optional(),
11
34
  version: z.string().optional(),
12
35
  kind: z.enum(["plugin", "extension"]).optional(),
13
36
  trust: RegistryTrustSchema.optional(),
14
37
  trustReason: z.string().optional(),
38
+ securityReview: RegistrySecurityReviewSchema.optional(),
39
+ securityFix: z.boolean().optional(),
40
+ tier: RegistryTierSchema.optional(),
15
41
  category: z.string().optional(),
42
+ art: z.string().max(4096).optional(),
16
43
  logo: z.string().optional(),
17
44
  icon: z.string().optional(),
18
45
  homepage: z.url().optional(),
19
46
  source: z.unknown(),
47
+ })
48
+ .superRefine((entry, ctx) => {
49
+ const trust = entry.trust ?? "listed";
50
+ if (trust === "blocked" && (entry.trustReason === undefined || entry.trustReason.trim() === "")) {
51
+ ctx.addIssue({ code: "custom", path: ["trustReason"], message: "a blocked entry must say why" });
52
+ }
53
+ if (trust === "verified" && entry.securityReview === undefined) {
54
+ ctx.addIssue({ code: "custom", path: ["securityReview"], message: "a verified entry must carry its security review" });
55
+ }
56
+ if (entry.securityReview === undefined) {
57
+ return;
58
+ }
59
+ const install = resolveSource(entry.source, "", undefined);
60
+ if (install?.ref !== entry.securityReview.sha || install?.url !== entry.securityReview.url || install.path !== entry.securityReview.path) {
61
+ ctx.addIssue({
62
+ code: "custom",
63
+ path: ["securityReview"],
64
+ message: "must equal the exact repository, commit and subdirectory named by source",
65
+ });
66
+ }
20
67
  });
21
- export const RegistryFileSchema = z.object({
68
+ export const RegistryFileSchema = z
69
+ .object({
22
70
  name: z.string(),
23
71
  metadata: z.object({ pluginRoot: z.string().optional() }).optional(),
24
72
  plugins: z.array(RegistryFileEntrySchema),
73
+ })
74
+ .superRefine((file, ctx) => {
75
+ const seen = new Set();
76
+ for (let at = 0; at < file.plugins.length; at += 1) {
77
+ const name = file.plugins[at]?.name;
78
+ if (name !== undefined && seen.has(name)) {
79
+ ctx.addIssue({ code: "custom", path: ["plugins", at, "name"], message: "entry names must be unique" });
80
+ }
81
+ if (name !== undefined) {
82
+ seen.add(name);
83
+ }
84
+ }
85
+ });
86
+ const RegistryChecksSchema = z.object({
87
+ sha: z.string(),
88
+ manifest: z.string(),
89
+ bundle: z.string(),
90
+ engines: z.string().optional(),
25
91
  });
26
92
  const RegistryFactsEntrySchema = z.object({
27
93
  name: z.string(),
28
94
  stars: z.number().int().nonnegative().optional(),
29
95
  pushedAt: z.string().optional(),
96
+ checks: RegistryChecksSchema.optional(),
30
97
  });
31
98
  export const RegistryFactsSchema = z.object({
32
99
  scannedAt: z.string(),
@@ -39,33 +106,79 @@ export const RegistryEntrySchema = z.object({
39
106
  kind: z.enum(["plugin", "extension"]),
40
107
  trust: RegistryTrustSchema,
41
108
  trustReason: z.string().optional(),
109
+ securityReview: RegistrySecurityReviewSchema.optional(),
110
+ admitted: z.boolean(),
111
+ securityFix: z.boolean().optional(),
112
+ tier: RegistryTierSchema,
42
113
  category: z.string().optional(),
114
+ art: z.string().optional(),
43
115
  logo: z.string().optional(),
44
116
  icon: z.string().optional(),
45
117
  homepage: z.string().optional(),
46
118
  install: RegistryInstallSchema.optional(),
47
119
  stars: z.number().int().nonnegative().optional(),
48
120
  pushedAt: z.string().optional(),
121
+ checks: RegistryChecksSchema.optional(),
49
122
  });
123
+ const normalizedRegistryUrl = (value) => {
124
+ const scp = value.includes("://") ? null : /^(?:[^@]+@)?([^:]+):\/?(.+)$/u.exec(value);
125
+ if (scp !== null) {
126
+ return `${scp[1]?.toLowerCase()}/${scp[2]
127
+ ?.replace(/\.git\/?$/u, "")
128
+ .replace(/\/$/u, "")
129
+ .toLowerCase()}`;
130
+ }
131
+ try {
132
+ const url = new URL(value);
133
+ const path = url.pathname
134
+ .replace(/\.git\/?$/u, "")
135
+ .replace(/\/$/u, "")
136
+ .toLowerCase();
137
+ return `${url.host.toLowerCase()}${path}`;
138
+ }
139
+ catch {
140
+ return undefined;
141
+ }
142
+ };
143
+ export const isOfficialRegistryUrl = (value) => normalizedRegistryUrl(value) === normalizedRegistryUrl(OFFICIAL_REGISTRY_URL);
144
+ export const isCurrentSecurityReview = (review, install) => review !== undefined &&
145
+ review.sha === install?.ref &&
146
+ review.url === install.url &&
147
+ review.path === install.path &&
148
+ review.policy === OFFICIAL_SECURITY_REVIEW_POLICY &&
149
+ review.reviewer === OFFICIAL_SECURITY_REVIEWER &&
150
+ review.deterministic.policy === OFFICIAL_DETERMINISTIC_SCAN_POLICY &&
151
+ review.deterministic.scanner === OFFICIAL_DETERMINISTIC_SCANNER &&
152
+ review.deterministic.version === OFFICIAL_DETERMINISTIC_SCANNER_VERSION;
50
153
  export const resolveRegistry = (file, facts, registryUrl) => {
51
154
  const byName = new Map(facts?.entries.map((entry) => [entry.name, entry]) ?? []);
155
+ const official = isOfficialRegistryUrl(registryUrl);
52
156
  return file.plugins.map((plugin) => {
53
157
  const upstream = byName.get(plugin.name);
54
158
  const install = resolveSource(plugin.source, registryUrl, file.metadata?.pluginRoot);
159
+ const kind = plugin.kind ?? "plugin";
160
+ const trust = plugin.trust ?? "listed";
161
+ const admitted = trust !== "blocked" && (kind !== "extension" || !official || isCurrentSecurityReview(plugin.securityReview, install));
55
162
  return {
56
163
  name: plugin.name,
57
- kind: plugin.kind ?? "plugin",
58
- trust: plugin.trust ?? "listed",
164
+ kind,
165
+ trust,
166
+ admitted,
167
+ tier: plugin.tier ?? "free",
59
168
  ...(plugin.description !== undefined ? { description: plugin.description } : {}),
60
169
  ...(plugin.version !== undefined ? { version: plugin.version } : {}),
61
170
  ...(plugin.trustReason !== undefined ? { trustReason: plugin.trustReason } : {}),
171
+ ...(plugin.securityReview !== undefined ? { securityReview: plugin.securityReview } : {}),
172
+ ...(plugin.securityFix !== undefined ? { securityFix: plugin.securityFix } : {}),
62
173
  ...(plugin.category !== undefined ? { category: plugin.category } : {}),
174
+ ...(plugin.art !== undefined ? { art: plugin.art } : {}),
63
175
  ...(plugin.logo !== undefined ? { logo: plugin.logo } : {}),
64
176
  ...(plugin.icon !== undefined ? { icon: plugin.icon } : {}),
65
177
  ...(plugin.homepage !== undefined ? { homepage: plugin.homepage } : {}),
66
178
  ...(install !== undefined ? { install } : {}),
67
179
  ...(upstream?.stars !== undefined ? { stars: upstream.stars } : {}),
68
180
  ...(upstream?.pushedAt !== undefined ? { pushedAt: upstream.pushedAt } : {}),
181
+ ...(upstream?.checks !== undefined && upstream.checks.sha === install?.ref ? { checks: upstream.checks } : {}),
69
182
  };
70
183
  });
71
184
  };
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/registry",
3
- "version": "1.176.3",
3
+ "version": "1.209.1",
4
4
  "description": "The extension-registry file format — a git repo of sha-pinned pointers, shared by the daemon, the site gallery and the scanner",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,15 +1,53 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { compareEntries, type RegistryEntry, RegistryFactsSchema, RegistryFileSchema, resolveRegistry } from "./registry.js";
2
+ import {
3
+ compareEntries,
4
+ isOfficialRegistryUrl,
5
+ OFFICIAL_DETERMINISTIC_SCANNER,
6
+ OFFICIAL_DETERMINISTIC_SCANNER_VERSION,
7
+ OFFICIAL_DETERMINISTIC_SCAN_POLICY,
8
+ OFFICIAL_SECURITY_REVIEW_POLICY,
9
+ OFFICIAL_SECURITY_REVIEWER,
10
+ type RegistryEntry,
11
+ RegistryFactsSchema,
12
+ RegistryFileSchema,
13
+ resolveRegistry,
14
+ } from "./registry.js";
3
15
 
4
16
  const SHA = "9f2c1ab0d4e5f60718293a4b5c6d7e8f90a1b2c3";
5
17
  const REGISTRY = "https://github.com/intentic/registry";
18
+ const REVIEW = {
19
+ sha: SHA,
20
+ url: `https://github.com/acme/incidents.git`,
21
+ policy: OFFICIAL_SECURITY_REVIEW_POLICY,
22
+ reviewer: OFFICIAL_SECURITY_REVIEWER,
23
+ reviewedAt: "2026-08-01T00:00:00.000Z",
24
+ runId: "run-1",
25
+ deterministic: {
26
+ policy: OFFICIAL_DETERMINISTIC_SCAN_POLICY,
27
+ scanner: OFFICIAL_DETERMINISTIC_SCANNER,
28
+ version: OFFICIAL_DETERMINISTIC_SCANNER_VERSION,
29
+ runId: `workflow-1`,
30
+ },
31
+ };
6
32
 
7
33
  const file = RegistryFileSchema.parse({
8
34
  name: `intentic`,
9
35
  plugins: [
10
- { name: `incidents`, kind: `extension`, trust: `verified`, source: { source: `github`, repo: `acme/incidents`, sha: SHA } },
36
+ {
37
+ name: `incidents`,
38
+ kind: `extension`,
39
+ trust: `verified`,
40
+ securityReview: REVIEW,
41
+ source: { source: `github`, repo: `acme/incidents`, sha: SHA },
42
+ },
11
43
  { name: `linear`, kind: `extension`, source: { source: `github`, repo: `acme/linear`, sha: SHA } },
12
- { name: `evil`, kind: `extension`, trust: `blocked`, trustReason: `exfiltrates workspace secrets`, source: { source: `github`, repo: `bad/evil` } },
44
+ {
45
+ name: `evil`,
46
+ kind: `extension`,
47
+ trust: `blocked`,
48
+ trustReason: `exfiltrates workspace secrets`,
49
+ source: { source: `github`, repo: `bad/evil` },
50
+ },
13
51
  ],
14
52
  });
15
53
 
@@ -30,12 +68,68 @@ describe(`resolveRegistry`, () => {
30
68
  expect(resolveRegistry(file, undefined, REGISTRY)).toHaveLength(3);
31
69
  });
32
70
 
33
- it(`defaults an unstated trust to listed and an unstated kind to plugin`, () => {
71
+ it(`admits official extensions only when both automated checks match their complete source identity`, () => {
72
+ const entries = resolveRegistry(file, undefined, REGISTRY);
73
+ expect(entries.find((entry) => entry.name === `incidents`)?.admitted).toBe(true);
74
+ expect(entries.find((entry) => entry.name === `linear`)?.admitted).toBe(false);
75
+ expect(entries.find((entry) => entry.name === `evil`)?.admitted).toBe(false);
76
+ });
77
+
78
+ it(`does not admit deterministic evidence from an old policy or scanner version`, () => {
79
+ for (const deterministic of [
80
+ { ...REVIEW.deterministic, policy: `intentic-extension-deterministic-v0` },
81
+ { ...REVIEW.deterministic, version: `0.71.0` },
82
+ ]) {
83
+ const registry = RegistryFileSchema.parse({
84
+ name: `intentic`,
85
+ plugins: [
86
+ {
87
+ name: `incidents`,
88
+ kind: `extension`,
89
+ securityReview: { ...REVIEW, deterministic },
90
+ source: { source: `github`, repo: `acme/incidents`, sha: SHA },
91
+ },
92
+ ],
93
+ });
94
+ expect(resolveRegistry(registry, undefined, REGISTRY)[0]?.admitted).toBe(false);
95
+ }
96
+ });
97
+
98
+ it(`leaves a third-party registry as its own trust boundary`, () => {
99
+ const entries = resolveRegistry(file, undefined, `https://github.com/acme/internal-registry`);
100
+ expect(entries.find((entry) => entry.name === `linear`)?.admitted).toBe(true);
101
+ });
102
+
103
+ it(`defaults an unstated trust to listed, an unstated kind to plugin, and an unstated tier to free`, () => {
34
104
  const entries = resolveRegistry(RegistryFileSchema.parse({ name: `x`, plugins: [{ name: `a`, source: `./a` }] }), undefined, REGISTRY);
35
- expect(entries[0]).toMatchObject({ trust: `listed`, kind: `plugin` });
105
+ expect(entries[0]).toMatchObject({ trust: `listed`, kind: `plugin`, tier: `free` });
106
+ });
107
+
108
+ it(`carries a premium tier through the resolve`, () => {
109
+ const premium = RegistryFileSchema.parse({
110
+ name: `x`,
111
+ plugins: [{ name: `acme.research`, kind: `extension`, tier: `premium`, source: { source: `github`, repo: `acme/research`, sha: SHA } }],
112
+ });
113
+ expect(resolveRegistry(premium, undefined, REGISTRY)[0]?.tier).toBe(`premium`);
36
114
  });
37
115
 
38
116
  // Blocked rows must survive the resolve — deleting them is what hides a warning from the people who
117
+ /* The staleness rule the checks ride on: a fact is bound to the sha it was derived from, and a listing
118
+ * repointed since the last scan renders no checks at all — the honest gap — rather than yesterday's verdict
119
+ * describing today's pointer. */
120
+ it(`joins checks only when they were derived from the sha the listing still pins`, () => {
121
+ const facts = RegistryFactsSchema.parse({
122
+ scannedAt: `2026-08-01T00:00:00.000Z`,
123
+ entries: [
124
+ { name: `linear`, checks: { sha: SHA, manifest: `ok`, bundle: `ok`, engines: `^2.0.0` } },
125
+ { name: `incidents`, checks: { sha: `b`.repeat(40), manifest: `ok`, bundle: `ok` } },
126
+ ],
127
+ });
128
+ const entries = resolveRegistry(file, facts, REGISTRY);
129
+ expect(entries.find((entry) => entry.name === `linear`)?.checks).toEqual({ sha: SHA, manifest: `ok`, bundle: `ok`, engines: `^2.0.0` });
130
+ expect(entries.find((entry) => entry.name === `incidents`)?.checks).toBeUndefined();
131
+ });
132
+
39
133
  // already installed the thing.
40
134
  it(`keeps a blocked entry, with its reason`, () => {
41
135
  const blocked = resolveRegistry(file, undefined, REGISTRY).find((entry) => entry.name === `evil`);
@@ -44,7 +138,14 @@ describe(`resolveRegistry`, () => {
44
138
  });
45
139
 
46
140
  describe(`compareEntries`, () => {
47
- const entry = (over: Partial<RegistryEntry>): RegistryEntry => ({ name: `x`, kind: `extension`, trust: `listed`, ...over });
141
+ const entry = (over: Partial<RegistryEntry>): RegistryEntry => ({
142
+ name: `x`,
143
+ kind: `extension`,
144
+ trust: `listed`,
145
+ admitted: true,
146
+ tier: `free`,
147
+ ...over,
148
+ });
48
149
 
49
150
  it(`puts verified first even when it has fewer stars`, () => {
50
151
  const sorted = [entry({ name: `popular`, stars: 900 }), entry({ name: `checked`, trust: `verified`, stars: 1 })].toSorted(compareEntries);
@@ -66,3 +167,54 @@ describe(`compareEntries`, () => {
66
167
  expect(sorted.map((e) => e.name)).toEqual([`fresh`, `stale`, `undated`]);
67
168
  });
68
169
  });
170
+
171
+ describe(`security review invariants`, () => {
172
+ it(`refuses a verified row with no evidence and a blocked row with no reason`, () => {
173
+ expect(
174
+ RegistryFileSchema.safeParse({
175
+ name: `x`,
176
+ plugins: [{ name: `verified`, kind: `extension`, trust: `verified`, source: { source: `github`, repo: `x/v`, sha: SHA } }],
177
+ }).success,
178
+ ).toBe(false);
179
+ expect(
180
+ RegistryFileSchema.safeParse({
181
+ name: `x`,
182
+ plugins: [{ name: `blocked`, kind: `extension`, trust: `blocked`, source: { source: `github`, repo: `x/b`, sha: SHA } }],
183
+ }).success,
184
+ ).toBe(false);
185
+ });
186
+
187
+ it(`refuses evidence copied from another repository or subdirectory at the same sha`, () => {
188
+ for (const entry of [
189
+ {
190
+ name: `changed-repository`,
191
+ kind: `extension`,
192
+ trust: `listed`,
193
+ securityReview: REVIEW,
194
+ source: { source: `github`, repo: `x/changed`, sha: SHA },
195
+ },
196
+ {
197
+ name: `changed-path`,
198
+ kind: `extension`,
199
+ trust: `listed`,
200
+ securityReview: { ...REVIEW, url: `https://github.com/acme/monorepo.git`, path: `packages/other` },
201
+ source: {
202
+ source: `git-subdir`,
203
+ url: `https://github.com/acme/monorepo.git`,
204
+ path: `packages/changed`,
205
+ sha: SHA,
206
+ },
207
+ },
208
+ ]) {
209
+ expect(RegistryFileSchema.safeParse({ name: `x`, plugins: [entry] }).success).toBe(false);
210
+ }
211
+ });
212
+
213
+ it(`recognizes harmless spellings of the official registry so they cannot bypass admission`, () => {
214
+ expect(isOfficialRegistryUrl(`https://github.com/intentic/registry.git/`)).toBe(true);
215
+ expect(isOfficialRegistryUrl(`http://github.com/intentic/registry`)).toBe(true);
216
+ expect(isOfficialRegistryUrl(`git@github.com:intentic/registry.git`)).toBe(true);
217
+ expect(isOfficialRegistryUrl(`https://github.com/INTENTIC/REGISTRY`)).toBe(true);
218
+ expect(isOfficialRegistryUrl(`https://github.com/intentic/registry-copy`)).toBe(false);
219
+ });
220
+ });
package/src/registry.ts CHANGED
@@ -10,8 +10,8 @@ import { type RegistryInstall, RegistryInstallSchema, resolveSource } from "./so
10
10
  * own fields and Claude Code ignores what it doesn't know, so one repo serves both consumers.
11
11
  *
12
12
  * The split into TWO files is the load-bearing part. `marketplace.json` is hand-edited and is the only place a
13
- * human decision is recorded; `registry.generated.json` is bot-written and holds nothing but facts read back
14
- * off the source host. Keeping star counts out of the curated file is what stops every nightly refresh from
13
+ * curated decision and source-bound admission are recorded; `registry.generated.json` is bot-written and holds
14
+ * nothing but facts read back off the source host. Keeping star counts out of the curated file is what stops every nightly refresh from
15
15
  * being a merge conflict against an open pull request — and it keeps the review diff to the thing being
16
16
  * decided. A registry that carries no generated file is a registry with no stars, which renders fine. */
17
17
 
@@ -28,11 +28,41 @@ export const OFFICIAL_REGISTRY_URL = "https://github.com/intentic/registry";
28
28
  // request against the registry, never a listing.
29
29
  export const REGISTRY_TOPIC = "intentic-extension";
30
30
 
31
+ /* THE ADMISSION RECORD for the official registry. Full results live in Trivy and intentic; this is the small,
32
+ * durable fact a registry row needs in order to bind both verdicts to what an installer will actually clone.
33
+ * Policies and scanner version are explicit because changing any admission control must make yesterday's
34
+ * reviews visibly stale rather than silently pretending they ran under today's rules. Third-party registries
35
+ * may carry other records; only this exact combination admits an entry to the official registry. */
36
+ export const OFFICIAL_SECURITY_REVIEW_POLICY = "intentic-extension-security-v1";
37
+ export const OFFICIAL_SECURITY_REVIEWER = "intentic-agent-gate";
38
+ export const OFFICIAL_DETERMINISTIC_SCAN_POLICY = "intentic-extension-deterministic-v1";
39
+ export const OFFICIAL_DETERMINISTIC_SCANNER = "trivy";
40
+ export const OFFICIAL_DETERMINISTIC_SCANNER_VERSION = "0.72.0";
41
+
42
+ const FULL_SHA = /^[0-9a-f]{40}$/;
43
+
44
+ export const RegistrySecurityReviewSchema = z.object({
45
+ sha: z.string().regex(FULL_SHA, "must be a full lowercase commit sha"),
46
+ url: z.string().min(1),
47
+ path: z.string().min(1).optional(),
48
+ policy: z.string().min(1),
49
+ reviewer: z.string().min(1),
50
+ reviewedAt: z.iso.datetime(),
51
+ runId: z.string().min(1),
52
+ deterministic: z.object({
53
+ policy: z.string().min(1),
54
+ scanner: z.string().min(1),
55
+ version: z.string().min(1),
56
+ runId: z.string().min(1),
57
+ }),
58
+ });
59
+ export type RegistrySecurityReview = z.infer<typeof RegistrySecurityReviewSchema>;
60
+
31
61
  /* WHAT A LISTING CLAIMS, and precisely what it doesn't.
32
62
  *
33
- * - `listed` the pointer resolves, the manifest parses, the publisher owns the source repo. NO claim about
34
- * the code. This is the honest default and what the scanner's pull requests propose.
35
- * - `verified` a human read the source at that sha. Sorted first, badged in both surfaces.
63
+ * - `listed` no HUMAN source review is claimed. In the official registry the separate securityReview must
64
+ * still show that both automated checks passed; third-party registries remain their own boundary.
65
+ * - `verified` both automated checks passed and a human also read the source at that sha. Sorted first and badged.
36
66
  * - `blocked` known-malicious or known-broken. It STAYS in the file with a reason rather than being deleted:
37
67
  * removing the row hides it from people browsing and tells the people who already installed it
38
68
  * nothing, which is backwards — they are the ones at risk.
@@ -42,49 +72,125 @@ export const REGISTRY_TOPIC = "intentic-extension";
42
72
  export const RegistryTrustSchema = z.enum(["verified", "listed", "blocked"]);
43
73
  export type RegistryTrust = z.infer<typeof RegistryTrustSchema>;
44
74
 
45
- const RegistryFileEntrySchema = z.object({
46
- name: z.string(),
47
- description: z.string().optional(),
48
- version: z.string().optional(),
49
- // "extension" installs as the sha-pinned `extension` capability; absent/"plugin" is a Claude Code plugin.
50
- kind: z.enum(["plugin", "extension"]).optional(),
51
- trust: RegistryTrustSchema.optional(),
52
- // Why it is blocked, or what was checked to verify it — shown verbatim wherever the badge is, because a
53
- // trust state with no stated reason is an opinion the reader can't weigh.
54
- trustReason: z.string().optional(),
55
- category: z.string().optional(),
56
- /* The mark the row is drawn with, copied off the extension's manifest by the scanner exactly like the
57
- * description and the version — the same two tiers the manifest declares (a simple-icons slug, then a name
58
- * from the app's icon set), and neither is required.
59
- *
60
- * It rides the CURATED file rather than the generated one, which looks wrong for a derived value until you
61
- * ask what a registry is for: a listing is what a human decided to publish, and the mark is part of how it
62
- * presents itself, so it belongs in the row a reviewer reads and can strike out. The generated file holds
63
- * only what a bot re-reads nightly and nobody reviews. It also has to be here to be of any use at all —
64
- * this is what the gallery and the in-app browse list render, and neither of them has the manifest: the
65
- * whole point of the row is that the code has NOT been cloned yet. */
66
- logo: z.string().optional(),
67
- icon: z.string().optional(),
68
- homepage: z.url().optional(),
69
- source: z.unknown(),
70
- });
75
+ /* WHAT A LISTING COSTS. `free` is the default and the whole story for most rows. `premium` opts the listing
76
+ * into the creator pool: installing it requires an intentic membership and donates a published number of the
77
+ * member's credits to the publisher (once, deduped monthly — an update in a later month donates again), and
78
+ * both surfaces badge it so the price is visible before the click. No usage is ever metered or reported for
79
+ * this; the deliberate act of installing is the whole signal. On a third-party registry the field still
80
+ * parses but means nothing — the pool only pays listings the platform's members actually install. */
81
+ export const RegistryTierSchema = z.enum(["free", "premium"]);
82
+ export type RegistryTier = z.infer<typeof RegistryTierSchema>;
71
83
 
72
- export const RegistryFileSchema = z.object({
73
- name: z.string(),
74
- metadata: z.object({ pluginRoot: z.string().optional() }).optional(),
75
- plugins: z.array(RegistryFileEntrySchema),
76
- });
84
+ const RegistryFileEntrySchema = z
85
+ .object({
86
+ name: z.string(),
87
+ description: z.string().optional(),
88
+ version: z.string().optional(),
89
+ // "extension" installs as the sha-pinned `extension` capability; absent/"plugin" is a Claude Code plugin.
90
+ kind: z.enum(["plugin", "extension"]).optional(),
91
+ trust: RegistryTrustSchema.optional(),
92
+ // Why it is blocked, or what was checked to verify it — shown verbatim wherever the badge is, because a
93
+ // trust state with no stated reason is an opinion the reader can't weigh.
94
+ trustReason: z.string().optional(),
95
+ // Both automated checks' evidence, bound to the complete source identity below. It is deliberately
96
+ // distinct from `trust: verified`: passing admission admits code; verified additionally says a human
97
+ // read it. Keeping those claims apart stops automated verdicts being presented as human review.
98
+ securityReview: RegistrySecurityReviewSchema.optional(),
99
+ // This listing's pinned commit fixes a security problem in earlier commits — the fast lane: an installed
100
+ // sandbox promotes its "update available" badge from ambient to attention-demanding, because there the OLD
101
+ // version is the dangerous one. Asserted by the pull request like trust, and worth exactly that review.
102
+ securityFix: z.boolean().optional(),
103
+ tier: RegistryTierSchema.optional(),
104
+ category: z.string().optional(),
105
+ /* The mark the row is drawn with, copied off the extension's manifest exactly like the description and
106
+ * the version — the same three tiers the manifest declares (the author's own inline drawing, then a
107
+ * simple-icons slug, then a name from the app's icon set), and none is required.
108
+ *
109
+ * It rides the CURATED file rather than the generated one, which looks wrong for a derived value until you
110
+ * ask what a registry is for: a listing is what a human decided to publish, and the mark is part of how it
111
+ * presents itself, so it belongs in the row a reviewer reads and can strike out. The generated file holds
112
+ * only what a bot re-reads nightly and nobody reviews. It also has to be here to be of any use at all —
113
+ * this is what the gallery and the in-app browse list render, and neither of them has the manifest: the
114
+ * whole point of the row is that the code has NOT been cloned yet.
115
+ *
116
+ * `art` being reviewable is the reason it is capped and kept as SVG text: it is the one field here that
117
+ * is a picture, and a picture nobody can read in the diff is a picture nobody reviewed. Four kilobytes
118
+ * of readable shapes is a thing a reviewer can strike out; a base64 blob is not. */
119
+ art: z.string().max(4096).optional(),
120
+ logo: z.string().optional(),
121
+ icon: z.string().optional(),
122
+ homepage: z.url().optional(),
123
+ source: z.unknown(),
124
+ })
125
+ .superRefine((entry, ctx) => {
126
+ const trust = entry.trust ?? "listed";
127
+ if (trust === "blocked" && (entry.trustReason === undefined || entry.trustReason.trim() === "")) {
128
+ ctx.addIssue({ code: "custom", path: ["trustReason"], message: "a blocked entry must say why" });
129
+ }
130
+ if (trust === "verified" && entry.securityReview === undefined) {
131
+ ctx.addIssue({ code: "custom", path: ["securityReview"], message: "a verified entry must carry its security review" });
132
+ }
133
+ if (entry.securityReview === undefined) {
134
+ return;
135
+ }
136
+ const install = resolveSource(entry.source, "", undefined);
137
+ if (install?.ref !== entry.securityReview.sha || install?.url !== entry.securityReview.url || install.path !== entry.securityReview.path) {
138
+ ctx.addIssue({
139
+ code: "custom",
140
+ path: ["securityReview"],
141
+ message: "must equal the exact repository, commit and subdirectory named by source",
142
+ });
143
+ }
144
+ });
145
+
146
+ export const RegistryFileSchema = z
147
+ .object({
148
+ name: z.string(),
149
+ metadata: z.object({ pluginRoot: z.string().optional() }).optional(),
150
+ plugins: z.array(RegistryFileEntrySchema),
151
+ })
152
+ .superRefine((file, ctx) => {
153
+ const seen = new Set<string>();
154
+ for (let at = 0; at < file.plugins.length; at += 1) {
155
+ const name = file.plugins[at]?.name;
156
+ if (name !== undefined && seen.has(name)) {
157
+ ctx.addIssue({ code: "custom", path: ["plugins", at, "name"], message: "entry names must be unique" });
158
+ }
159
+ if (name !== undefined) {
160
+ seen.add(name);
161
+ }
162
+ }
163
+ });
77
164
  export type RegistryFile = z.infer<typeof RegistryFileSchema>;
78
165
 
79
166
  /* Facts read off the source host, keyed by the curated entry's name. Deliberately NOT the upstream head sha:
80
167
  * the approved sha is the one that runs, updating is a pull request, and a file that advertised "there's a
81
168
  * newer commit over there" would be inviting a click that skips the review the whole model rests on. */
169
+ /* What the scan re-derived COLD at the listing's pinned sha — the same questions the daemon's readiness check
170
+ * answers for an author before publishing, asked again by a stranger with nothing but the pointer. That
171
+ * re-derivation is the whole value: an author's own checks describe the directory they ran them in, and these
172
+ * describe what an installer actually gets. `sha` binds the answers to the commit they were read from, so a
173
+ * listing repointed since the last scan renders no stale verdicts — the join drops checks whose sha no longer
174
+ * matches rather than letting yesterday's answer describe today's pointer. */
175
+ const RegistryChecksSchema = z.object({
176
+ sha: z.string(),
177
+ // "ok", or the reason it is not, verbatim — a verdict with no stated reason is an opinion.
178
+ manifest: z.string(),
179
+ // "ok" / "none" (no UI bundle) / the reason the bundle cannot load where it is installed.
180
+ bundle: z.string(),
181
+ // The engines range the manifest declares at that sha. A raw fact rather than a verdict on purpose: the
182
+ // scan does not know which app version any particular reader runs, but every consumer of this file does.
183
+ engines: z.string().optional(),
184
+ });
185
+ export type RegistryChecks = z.infer<typeof RegistryChecksSchema>;
186
+
82
187
  const RegistryFactsEntrySchema = z.object({
83
188
  name: z.string(),
84
189
  stars: z.number().int().nonnegative().optional(),
85
190
  // ISO-8601, last push to the source repo's default branch — the tiebreaker that does the real work while
86
191
  // every listing still has single-digit stars.
87
192
  pushedAt: z.string().optional(),
193
+ checks: RegistryChecksSchema.optional(),
88
194
  });
89
195
 
90
196
  export const RegistryFactsSchema = z.object({
@@ -102,8 +208,17 @@ export const RegistryEntrySchema = z.object({
102
208
  kind: z.enum(["plugin", "extension"]),
103
209
  trust: RegistryTrustSchema,
104
210
  trustReason: z.string().optional(),
211
+ securityReview: RegistrySecurityReviewSchema.optional(),
212
+ // False only for an executable row the official registry has not put through its current checks (and for a
213
+ // blocked row). The row remains visible so a reader gets a reason instead of a mysteriously missing entry,
214
+ // but every install/update surface refuses to act on it. Third-party registries remain their own trust
215
+ // boundary and resolve their non-blocked rows as admitted without adopting intentic's policy.
216
+ admitted: z.boolean(),
217
+ securityFix: z.boolean().optional(),
218
+ tier: RegistryTierSchema,
105
219
  category: z.string().optional(),
106
220
  // The mark, as the gallery and the app's browse list draw it — see the curated file's fields above.
221
+ art: z.string().optional(),
107
222
  logo: z.string().optional(),
108
223
  icon: z.string().optional(),
109
224
  homepage: z.string().optional(),
@@ -112,30 +227,77 @@ export const RegistryEntrySchema = z.object({
112
227
  install: RegistryInstallSchema.optional(),
113
228
  stars: z.number().int().nonnegative().optional(),
114
229
  pushedAt: z.string().optional(),
230
+ checks: RegistryChecksSchema.optional(),
115
231
  });
116
232
  export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
117
233
 
234
+ const normalizedRegistryUrl = (value: string): string | undefined => {
235
+ const scp = value.includes("://") ? null : /^(?:[^@]+@)?([^:]+):\/?(.+)$/u.exec(value);
236
+ if (scp !== null) {
237
+ return `${scp[1]?.toLowerCase()}/${scp[2]
238
+ ?.replace(/\.git\/?$/u, "")
239
+ .replace(/\/$/u, "")
240
+ .toLowerCase()}`;
241
+ }
242
+ try {
243
+ const url = new URL(value);
244
+ const path = url.pathname
245
+ .replace(/\.git\/?$/u, "")
246
+ .replace(/\/$/u, "")
247
+ .toLowerCase();
248
+ return `${url.host.toLowerCase()}${path}`;
249
+ } catch {
250
+ return undefined;
251
+ }
252
+ };
253
+
254
+ export const isOfficialRegistryUrl = (value: string): boolean => normalizedRegistryUrl(value) === normalizedRegistryUrl(OFFICIAL_REGISTRY_URL);
255
+
256
+ export const isCurrentSecurityReview = (review: RegistrySecurityReview | undefined, install: RegistryInstall | undefined): boolean =>
257
+ review !== undefined &&
258
+ review.sha === install?.ref &&
259
+ review.url === install.url &&
260
+ review.path === install.path &&
261
+ review.policy === OFFICIAL_SECURITY_REVIEW_POLICY &&
262
+ review.reviewer === OFFICIAL_SECURITY_REVIEWER &&
263
+ review.deterministic.policy === OFFICIAL_DETERMINISTIC_SCAN_POLICY &&
264
+ review.deterministic.scanner === OFFICIAL_DETERMINISTIC_SCANNER &&
265
+ review.deterministic.version === OFFICIAL_DETERMINISTIC_SCANNER_VERSION;
266
+
118
267
  /* Join the curated file to the generated facts. `facts` is undefined for any registry that runs no scanner,
119
268
  * which is most of them — a private registry of six internal extensions wants the pointers and nothing else. */
120
269
  export const resolveRegistry = (file: RegistryFile, facts: RegistryFacts | undefined, registryUrl: string): RegistryEntry[] => {
121
270
  const byName = new Map(facts?.entries.map((entry) => [entry.name, entry]) ?? []);
271
+ const official = isOfficialRegistryUrl(registryUrl);
122
272
  return file.plugins.map((plugin) => {
123
273
  const upstream = byName.get(plugin.name);
124
274
  const install: RegistryInstall | undefined = resolveSource(plugin.source, registryUrl, file.metadata?.pluginRoot);
275
+ const kind = plugin.kind ?? "plugin";
276
+ const trust = plugin.trust ?? "listed";
277
+ const admitted = trust !== "blocked" && (kind !== "extension" || !official || isCurrentSecurityReview(plugin.securityReview, install));
125
278
  return {
126
279
  name: plugin.name,
127
- kind: plugin.kind ?? "plugin",
128
- trust: plugin.trust ?? "listed",
280
+ kind,
281
+ trust,
282
+ admitted,
283
+ // Absent ⇒ free, so a registry that has never heard of the pool keeps meaning what it always did.
284
+ tier: plugin.tier ?? "free",
129
285
  ...(plugin.description !== undefined ? { description: plugin.description } : {}),
130
286
  ...(plugin.version !== undefined ? { version: plugin.version } : {}),
131
287
  ...(plugin.trustReason !== undefined ? { trustReason: plugin.trustReason } : {}),
288
+ ...(plugin.securityReview !== undefined ? { securityReview: plugin.securityReview } : {}),
289
+ ...(plugin.securityFix !== undefined ? { securityFix: plugin.securityFix } : {}),
132
290
  ...(plugin.category !== undefined ? { category: plugin.category } : {}),
291
+ ...(plugin.art !== undefined ? { art: plugin.art } : {}),
133
292
  ...(plugin.logo !== undefined ? { logo: plugin.logo } : {}),
134
293
  ...(plugin.icon !== undefined ? { icon: plugin.icon } : {}),
135
294
  ...(plugin.homepage !== undefined ? { homepage: plugin.homepage } : {}),
136
295
  ...(install !== undefined ? { install } : {}),
137
296
  ...(upstream?.stars !== undefined ? { stars: upstream.stars } : {}),
138
297
  ...(upstream?.pushedAt !== undefined ? { pushedAt: upstream.pushedAt } : {}),
298
+ // Only when derived from the sha this row still points at — a repointed listing renders no checks
299
+ // until the next scan, which is the honest gap rather than yesterday's verdict on today's pointer.
300
+ ...(upstream?.checks !== undefined && upstream.checks.sha === install?.ref ? { checks: upstream.checks } : {}),
139
301
  };
140
302
  });
141
303
  };
@@ -22,7 +22,9 @@ describe(`resolveSource`, () => {
22
22
  url: `https://gitlab.com/acme/x.git`,
23
23
  ref: SHA,
24
24
  });
25
- expect(resolveSource({ source: `git-subdir`, url: `https://github.com/acme/tools.git`, path: `ext/a`, sha: SHA }, REGISTRY, undefined)).toEqual({
25
+ expect(
26
+ resolveSource({ source: `git-subdir`, url: `https://github.com/acme/tools.git`, path: `ext/a`, sha: SHA }, REGISTRY, undefined),
27
+ ).toEqual({
26
28
  url: `https://github.com/acme/tools.git`,
27
29
  path: `ext/a`,
28
30
  ref: SHA,