dsh-plugin-inspector 0.6.0 → 0.8.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/README.md +10 -8
- package/lib/attestation.js +374 -0
- package/lib/checks/tier-a.js +110 -1
- package/lib/checks/tier-b.js +341 -28
- package/lib/checks/tier-c.js +146 -18
- package/lib/index.js +2 -1
- package/lib/inspect.js +7 -2
- package/lib/knowledge.js +289 -25
- package/lib/npm.js +58 -5
- package/lib/registry.js +54 -0
- package/lib/report.js +58 -7
- package/lib/syntax.js +135 -0
- package/lib/types/attestation.d.ts +173 -0
- package/lib/types/checks/input.d.ts +7 -0
- package/lib/types/checks/tier-c.d.ts +5 -3
- package/lib/types/index.d.ts +2 -1
- package/lib/types/inspect.d.ts +5 -2
- package/lib/types/knowledge.d.ts +212 -4
- package/lib/types/model.d.ts +13 -1
- package/lib/types/npm.d.ts +12 -2
- package/lib/types/registry.d.ts +40 -0
- package/lib/types/syntax.d.ts +51 -0
- package/package.json +2 -1
package/lib/knowledge.js
CHANGED
|
@@ -11,9 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
13
|
* Harness version these tables were transcribed from — the version string in
|
|
14
|
-
* the
|
|
14
|
+
* the shipped bundles' own `package.json`, which is `dsh`'s own version.
|
|
15
|
+
*
|
|
16
|
+
* Re-verified against `0.1.2-rc.1`, the release npm tags `latest`, by
|
|
17
|
+
* extracting each table from the published packages and diffing it against the
|
|
18
|
+
* one here. What moved: thirteen row ids the bundles gained against three they
|
|
19
|
+
* dropped, four rows the base layer inserts that only the web bundle carried,
|
|
20
|
+
* eleven seam keys against one dropped, and two waterfall events against one
|
|
21
|
+
* dropped. What did not: the sandbox trap table, the teardown surfaces, and
|
|
22
|
+
* every `DECISION_EVENT_DEFAULTS` citation, all re-read at this release.
|
|
15
23
|
*/
|
|
16
|
-
export const HARNESS_REFERENCE = '0.1.
|
|
24
|
+
export const HARNESS_REFERENCE = '0.1.2-rc.1';
|
|
17
25
|
/**
|
|
18
26
|
* The three profile bundles the harness ships, mapped to what each one is.
|
|
19
27
|
* A package that *is* one of these composes the core rows rather than modifying
|
|
@@ -45,13 +53,11 @@ export const CORE_ROWS = new Map([
|
|
|
45
53
|
['agent-instructions', { module: '@deepseek-ai/dsh-agent-instructions', bundles: ['base'] }],
|
|
46
54
|
['agent-loop', { module: '@deepseek-ai/dsh-agent-loop', bundles: ['base'] }],
|
|
47
55
|
['agent-presets', { module: '@deepseek-ai/dsh-agent-presets', bundles: ['web-app'] }],
|
|
48
|
-
['api-gateway', { module: '@deepseek-ai/dsh-host-apiproxy', bundles: ['web-app'] }],
|
|
49
56
|
['api-remotes', { module: '@deepseek-ai/dsh-api-remotes', bundles: ['web-app'] }],
|
|
50
57
|
['approval', { module: '@deepseek-ai/dsh-user-approval', bundles: ['base'] }],
|
|
51
58
|
['attachment-local', { module: '@deepseek-ai/dsh-attachment-local', bundles: ['base'] }],
|
|
52
59
|
['bash-sandbox', { module: '@deepseek-ai/dsh-bash-sandbox', bundles: ['base'] }],
|
|
53
60
|
['client-hmr', { module: '@deepseek-ai/dsh-client-hmr', bundles: ['web-app'] }],
|
|
54
|
-
['client-runtime', { module: '@deepseek-ai/dsh-client-runtime', bundles: ['web-app'] }],
|
|
55
61
|
['code-runtime', { module: '@deepseek-ai/dsh-code-runtime-worker-thread', bundles: ['headless', 'web-app'] }],
|
|
56
62
|
['command-compact', { module: '@deepseek-ai/dsh-command-compact', bundles: ['base'] }],
|
|
57
63
|
['command-feedback', { module: '@deepseek-ai/dsh-command-feedback', bundles: ['base'] }],
|
|
@@ -62,7 +68,9 @@ export const CORE_ROWS = new Map([
|
|
|
62
68
|
['cordis-client-runner', { module: '@deepseek-ai/dsh-cordis-client-runner', bundles: ['web-app'] }],
|
|
63
69
|
['cordis-host-runner', { module: '@deepseek-ai/dsh-cordis-host-runner', bundles: ['web-app'] }],
|
|
64
70
|
['credentials', { module: '@deepseek-ai/dsh-credentials-local', bundles: ['base'] }],
|
|
71
|
+
['deepseek-llm-api-extensions', { module: '@deepseek-ai/dsh-deepseek-llm-api-extensions', bundles: ['base'] }],
|
|
65
72
|
['directory-picker', { module: '@deepseek-ai/dsh-host-directory-picker-auto', bundles: ['web-app'] }],
|
|
73
|
+
['file-reference-local', { module: '@deepseek-ai/dsh-file-reference-local', bundles: ['web-app'] }],
|
|
66
74
|
['fs-observation-policy', { module: '@deepseek-ai/dsh-fs-observation-policy', bundles: ['base'] }],
|
|
67
75
|
['fs-sandbox', { module: '@deepseek-ai/dsh-fs-sandbox', bundles: ['base'] }],
|
|
68
76
|
['goal', { module: '@deepseek-ai/dsh-goal', bundles: ['base'] }],
|
|
@@ -81,33 +89,40 @@ export const CORE_ROWS = new Map([
|
|
|
81
89
|
['permission', { module: '@deepseek-ai/dsh-permission-presets', bundles: ['base'] }],
|
|
82
90
|
['plan-mode', { module: '@deepseek-ai/dsh-plan-mode', bundles: ['base'] }],
|
|
83
91
|
['plugin-inventory', { module: '@deepseek-ai/dsh-host-plugin-inventory', bundles: ['web-app'] }],
|
|
92
|
+
['plugin-package-inventory-deepseek', { module: '@deepseek-ai/dsh-plugin-package-inventory-deepseek', bundles: ['base'] }],
|
|
84
93
|
['pwsh-sandbox', { module: '@deepseek-ai/dsh-pwsh-sandbox', bundles: ['base'] }],
|
|
85
94
|
['repeat-tool-reminder', { module: '@deepseek-ai/dsh-repeat-tool-reminder', bundles: ['base'] }],
|
|
86
95
|
['sandbox', { module: '@deepseek-ai/dsh-sandbox-local', bundles: ['base'] }],
|
|
87
96
|
['sandbox-policy', { module: '@deepseek-ai/dsh-sandbox-policy', bundles: ['base'] }],
|
|
88
97
|
['session', { module: '@deepseek-ai/dsh-session', bundles: ['base'] }],
|
|
89
98
|
['session-checkpoint-policy', { module: '@deepseek-ai/dsh-session-checkpoint-policy', bundles: ['base'] }],
|
|
99
|
+
['session-controller', { module: '@deepseek-ai/dsh-api-session-controller', bundles: ['web-app'] }],
|
|
100
|
+
['session-log-deepseek', { module: '@deepseek-ai/dsh-session-log-deepseek', bundles: ['base'] }],
|
|
90
101
|
['session-log-download', { module: '@deepseek-ai/dsh-session-log-export', bundles: ['web-app'] }],
|
|
91
102
|
['session-persistence-jsonl', { module: '@deepseek-ai/dsh-session-persistence-jsonl', bundles: ['base'] }],
|
|
92
103
|
['session-projection', { module: '@deepseek-ai/dsh-session-projection', bundles: ['base'] }],
|
|
93
|
-
['session-projection-cache', { module: '@deepseek-ai/dsh-session-projection-cache', bundles: ['
|
|
104
|
+
['session-projection-cache', { module: '@deepseek-ai/dsh-session-projection-cache', bundles: ['base'] }],
|
|
94
105
|
['session-query-sqlite', { module: '@deepseek-ai/dsh-session-query-sqlite', bundles: ['base'] }],
|
|
106
|
+
['session-reference', { module: '@deepseek-ai/dsh-session-reference', bundles: ['web-app'] }],
|
|
95
107
|
['session-stats', { module: '@deepseek-ai/dsh-session-stats', bundles: ['web-app'] }],
|
|
96
108
|
['session-telemetry-otel', { module: '@deepseek-ai/dsh-session-telemetry-otel', bundles: ['base'] }],
|
|
97
109
|
['session-title', { module: '@deepseek-ai/dsh-session-title', bundles: ['base'] }],
|
|
98
110
|
['session-title-llm', { module: '@deepseek-ai/dsh-session-title-first-prompt-llm', bundles: ['base'] }],
|
|
111
|
+
['session-turn-outline', { module: '@deepseek-ai/dsh-session-turn-outline', bundles: ['web-app'] }],
|
|
99
112
|
['settings', { module: '@deepseek-ai/dsh-settings-file', bundles: ['base'] }],
|
|
113
|
+
['settings-controller', { module: '@deepseek-ai/dsh-api-settings-controller', bundles: ['web-app'] }],
|
|
100
114
|
['shell-env', { module: '@deepseek-ai/dsh-shell-env', bundles: ['base'] }],
|
|
101
115
|
['skill', { module: '@deepseek-ai/dsh-skill', bundles: ['base'] }],
|
|
102
116
|
['skill-badge', { module: '@deepseek-ai/dsh-skill-badge', bundles: ['base'] }],
|
|
103
117
|
['skill-filesystem', { module: '@deepseek-ai/dsh-skill-filesystem', bundles: ['base'] }],
|
|
104
118
|
['spill-local', { module: '@deepseek-ai/dsh-spill-local', bundles: ['base'] }],
|
|
105
119
|
['spill-policy', { module: '@deepseek-ai/dsh-spill-policy', bundles: ['base'] }],
|
|
106
|
-
['storage', { module: '@deepseek-ai/dsh-storage', bundles: ['
|
|
107
|
-
['storage-domain', { module: '@deepseek-ai/dsh-storage-domain', bundles: ['
|
|
108
|
-
['storage-json', { module: '@deepseek-ai/dsh-storage-json', bundles: ['
|
|
120
|
+
['storage', { module: '@deepseek-ai/dsh-storage', bundles: ['base'] }],
|
|
121
|
+
['storage-domain', { module: '@deepseek-ai/dsh-storage-domain', bundles: ['base'] }],
|
|
122
|
+
['storage-json', { module: '@deepseek-ai/dsh-storage-json', bundles: ['base'] }],
|
|
109
123
|
['subagent', { module: '@deepseek-ai/dsh-subagent', bundles: ['base'] }],
|
|
110
124
|
['subagent-fork-in-process', { module: '@deepseek-ai/dsh-subagent-fork-in-process', bundles: ['base'] }],
|
|
125
|
+
['subagent-model-selection-settings', { module: '@deepseek-ai/dsh-tool-subagent/model-selection-settings', bundles: ['web-app'] }],
|
|
111
126
|
['subagent-spawn-in-process', { module: '@deepseek-ai/dsh-subagent-spawn-in-process', bundles: ['base'] }],
|
|
112
127
|
['subprocess', { module: '@deepseek-ai/dsh-subprocess-local', bundles: ['base'] }],
|
|
113
128
|
['system-prompt', { module: '@deepseek-ai/dsh-system-prompt', bundles: ['base'] }],
|
|
@@ -128,7 +143,6 @@ export const CORE_ROWS = new Map([
|
|
|
128
143
|
['tool-subagent-control', { module: '@deepseek-ai/dsh-tool-subagent-control', bundles: ['base'] }],
|
|
129
144
|
['tool-subagent-fork', { module: '@deepseek-ai/dsh-tool-subagent', bundles: ['base'] }],
|
|
130
145
|
['tool-subagent-list-agents', { module: '@deepseek-ai/dsh-tool-subagent-control/list-agents', bundles: ['base'] }],
|
|
131
|
-
['tool-subagent-report', { module: '@deepseek-ai/dsh-tool-subagent-report', bundles: ['base'] }],
|
|
132
146
|
['tool-todo', { module: '@deepseek-ai/dsh-tool-todo', bundles: ['base'] }],
|
|
133
147
|
['tool-web', { module: '@deepseek-ai/dsh-tool-web', bundles: ['base'] }],
|
|
134
148
|
['tool-workflow', { module: '@deepseek-ai/dsh-tool-workflow', bundles: ['base'] }],
|
|
@@ -137,6 +151,10 @@ export const CORE_ROWS = new Map([
|
|
|
137
151
|
['typert-gateway', { module: '@deepseek-ai/dsh-api-gateway', bundles: ['base'] }],
|
|
138
152
|
['typert-loader', { module: '@deepseek-ai/dsh-typert-loader', bundles: ['base'] }],
|
|
139
153
|
['ui-agent-preset', { module: '@deepseek-ai/dsh-client-ui-agent-preset', bundles: ['web-app'] }],
|
|
154
|
+
['ui-approval', { module: '@deepseek-ai/dsh-client-ui-approval', bundles: ['web-app'] }],
|
|
155
|
+
['ui-attachment', { module: '@deepseek-ai/dsh-client-ui-attachment', bundles: ['web-app'] }],
|
|
156
|
+
['ui-brand-official', { module: '@deepseek-ai/dsh-client-ui-brand-official', bundles: ['web-app'] }],
|
|
157
|
+
['ui-chat', { module: '@deepseek-ai/dsh-client-ui-chat', bundles: ['web-app'] }],
|
|
140
158
|
['ui-commands', { module: '@deepseek-ai/dsh-client-ui-commands', bundles: ['web-app'] }],
|
|
141
159
|
['ui-conversation', { module: '@deepseek-ai/dsh-client-ui-conversation', bundles: ['web-app'] }],
|
|
142
160
|
['ui-cordis', { module: '@deepseek-ai/dsh-client-ui-cordis', bundles: ['web-app'] }],
|
|
@@ -149,6 +167,10 @@ export const CORE_ROWS = new Map([
|
|
|
149
167
|
['ui-model-selection', { module: '@deepseek-ai/dsh-client-ui-model-selection', bundles: ['web-app'] }],
|
|
150
168
|
['ui-permission', { module: '@deepseek-ai/dsh-client-ui-permission-presets', bundles: ['web-app'] }],
|
|
151
169
|
['ui-plan', { module: '@deepseek-ai/dsh-client-ui-plan', bundles: ['web-app'] }],
|
|
170
|
+
['ui-reference', { module: '@deepseek-ai/dsh-client-ui-reference', bundles: ['web-app'] }],
|
|
171
|
+
['ui-renderer', { module: '@deepseek-ai/dsh-client-ui-renderer', bundles: ['web-app'] }],
|
|
172
|
+
['ui-schedule', { module: '@deepseek-ai/dsh-client-ui-schedule', bundles: ['web-app'] }],
|
|
173
|
+
['ui-session', { module: '@deepseek-ai/dsh-client-ui-session', bundles: ['web-app'] }],
|
|
152
174
|
['ui-settings', { module: '@deepseek-ai/dsh-client-ui-settings', bundles: ['web-app'] }],
|
|
153
175
|
['ui-settings-general', { module: '@deepseek-ai/dsh-client-ui-settings-general', bundles: ['web-app'] }],
|
|
154
176
|
['ui-settings-models', { module: '@deepseek-ai/dsh-client-ui-settings-models', bundles: ['web-app'] }],
|
|
@@ -165,12 +187,14 @@ export const CORE_ROWS = new Map([
|
|
|
165
187
|
['ui-workspace', { module: '@deepseek-ai/dsh-client-ui-workspace', bundles: ['web-app'] }],
|
|
166
188
|
['user-questions', { module: '@deepseek-ai/dsh-user-questions', bundles: ['base'] }],
|
|
167
189
|
['web', { module: '@deepseek-ai/dsh-web', bundles: ['base'] }],
|
|
190
|
+
['web-fetch-http', { module: '@deepseek-ai/dsh-web-fetch-http', bundles: ['base'] }],
|
|
168
191
|
['web-runtime', { module: '@deepseek-ai/dsh-web-app', bundles: ['web-app'] }],
|
|
169
192
|
['web-search-deepseek', { module: '@deepseek-ai/dsh-web-search-deepseek', bundles: ['base'] }],
|
|
170
193
|
['web-startup', { module: '@deepseek-ai/dsh-web-app/startup', bundles: ['web-app'] }],
|
|
171
194
|
['webserver', { module: '@deepseek-ai/dsh-host-webserver', bundles: ['web-app'] }],
|
|
172
195
|
['workflow-worker-thread', { module: '@deepseek-ai/dsh-workflow-worker-thread', bundles: ['base'] }],
|
|
173
196
|
['workspace', { module: '@deepseek-ai/dsh-workspace', bundles: ['web-app'] }],
|
|
197
|
+
['workspace-controller', { module: '@deepseek-ai/dsh-api-workspace-controller', bundles: ['web-app'] }],
|
|
174
198
|
]);
|
|
175
199
|
/** Row ids the shipped bundles define. */
|
|
176
200
|
export const CORE_ROW_IDS = new Set(CORE_ROWS.keys());
|
|
@@ -207,21 +231,116 @@ export const SECURITY_ROW_IDS = new Map([
|
|
|
207
231
|
* replaces a core service for every consumer in its scope.
|
|
208
232
|
*/
|
|
209
233
|
export const SEAM_KEYS = new Set([
|
|
210
|
-
'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', '
|
|
211
|
-
'
|
|
212
|
-
'
|
|
213
|
-
'
|
|
214
|
-
'
|
|
215
|
-
'
|
|
216
|
-
'
|
|
217
|
-
'
|
|
218
|
-
'
|
|
219
|
-
'
|
|
234
|
+
'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'agentTeams', 'approval', 'attachments',
|
|
235
|
+
'authorization', 'clientModules', 'codeRuntime', 'commands', 'compaction', 'credentials',
|
|
236
|
+
'credentialsController', 'deepseekLlmApiExtensions', 'directoryPicker', 'directoryPickerController',
|
|
237
|
+
'e2b', 'fileReferences', 'fs', 'goals', 'inspector', 'invariants', 'jobs', 'llm', 'lsp',
|
|
238
|
+
'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy', 'sessionController',
|
|
239
|
+
'sessionFileReferences', 'sessionPersistence', 'sessionProjectionCache', 'sessionProjections',
|
|
240
|
+
'sessionQuery', 'sessionReferenceResolver', 'sessions', 'sessionSkillCatalog', 'sessionTelemetry',
|
|
241
|
+
'sessionTitle', 'settings', 'settingsController', 'shell', 'shellEnv', 'skills', 'spillStore', 'storage',
|
|
242
|
+
'storageDomain', 'subagentModelSelection', 'subagents', 'subprocess', 'systemPrompt', 'terminals',
|
|
243
|
+
'timer', 'tokenMeter', 'toolResultPruner', 'tools', 'typert', 'typertGateway', 'userQuestions', 'web',
|
|
244
|
+
'webhookRuntime', 'webServer', 'workflowEngine', 'workspaceController', 'workspaceRegistry',
|
|
220
245
|
]);
|
|
221
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* The subset of {@link SEAM_KEYS} whose replacement removes a constraint.
|
|
248
|
+
*
|
|
249
|
+
* `authorization` is the registry of flows that obtain a credential through a
|
|
250
|
+
* conversation with the user, so providing it means owning that conversation.
|
|
251
|
+
* That is the same class of substitution as `credentials`, which this set
|
|
252
|
+
* already holds. `fileReferences` decides which paths are offered for
|
|
253
|
+
* completion and `agentTeams` is the team form of `subagents`, which is
|
|
254
|
+
* deliberately not here either, so neither of those is in this set.
|
|
255
|
+
*
|
|
256
|
+
* Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
|
|
257
|
+
* services that own one `ctx.remote.*` namespace the browser client calls
|
|
258
|
+
* across the wire. A controller belongs here only when the traffic a
|
|
259
|
+
* substitution redirects to it carries a secret, an execution boundary, or a
|
|
260
|
+
* decision. A controller that forwards its seam's own verbs and adds a wire
|
|
261
|
+
* failure vocabulary does not, because the seam it fronts is reachable from
|
|
262
|
+
* `ctx` without substituting anything.
|
|
263
|
+
*
|
|
264
|
+
* Included:
|
|
265
|
+
* - `credentialsController` is what a browser configuration page calls to store
|
|
266
|
+
* a credential. `set(ref, value)` receives the plaintext secret and hands it
|
|
267
|
+
* to `ctx.credentials`
|
|
268
|
+
* (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
|
|
269
|
+
* `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
|
|
270
|
+
* answer to the three fields `CredentialInfo` declares. A layer that provides
|
|
271
|
+
* it takes both halves: every secret typed into the settings page, and the
|
|
272
|
+
* freedom to answer a read with the stored value.
|
|
273
|
+
* - `settingsController` passes `redactSecrets: true` on every remote read
|
|
274
|
+
* (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
|
|
275
|
+
* which is what keeps a `role('secret')` field out of a settings response;
|
|
276
|
+
* `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
|
|
277
|
+
* `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
|
|
278
|
+
* value in plaintext from the configuration page. Providing it puts the layer
|
|
279
|
+
* on both directions of a secret's path.
|
|
280
|
+
* - `sessionController` resolves each new Session's cwd from the wire request
|
|
281
|
+
* and hands it to `ensureSession`
|
|
282
|
+
* (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
|
|
283
|
+
* `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
|
|
284
|
+
* `workspace-write` root the enforcing filesystem, bash and terminal backends
|
|
285
|
+
* fence against. Its `prompt` verb builds the message admitted to the agent
|
|
286
|
+
* under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
|
|
287
|
+
* sandbox root for every session created from the client, and the text that
|
|
288
|
+
* reaches the model under the user's own source label.
|
|
289
|
+
* - `webhookRuntime` is what a provider adapter such as
|
|
290
|
+
* `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
|
|
291
|
+
* one built-in action creates a Session from a rule result whose fields
|
|
292
|
+
* include `workspacePath`, `permissionPreset` and `prompt`
|
|
293
|
+
* (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
|
|
294
|
+
* `createWebhookSession` applies that preset through
|
|
295
|
+
* `ctx.permissionPresets.set` before admitting the prompt
|
|
296
|
+
* (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
|
|
297
|
+
* Providing it picks the approval and sandbox preset for an agent started by
|
|
298
|
+
* a remote delivery with no user present.
|
|
299
|
+
*
|
|
300
|
+
* Excluded:
|
|
301
|
+
* - `workspaceController` forwards `request.path` to
|
|
302
|
+
* `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
|
|
303
|
+
* error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
|
|
304
|
+
* to `:212`). The registry it fronts is `workspaceRegistry`, which is not
|
|
305
|
+
* here, so the substitution reaches nothing the seam does not already offer.
|
|
306
|
+
* - `directoryPickerController` is three delegations to
|
|
307
|
+
* `ctx.directoryPicker.capability()` behind a check that refuses a verb the
|
|
308
|
+
* composed backend does not serve
|
|
309
|
+
* (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
|
|
310
|
+
* Path fencing lives in the backend, and `directoryPicker` is not here.
|
|
311
|
+
* - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
|
|
312
|
+
* is excluded above for the same reason: the traffic is path candidates
|
|
313
|
+
* offered for completion.
|
|
314
|
+
* - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
|
|
315
|
+
* for one Session's human-facing composer
|
|
316
|
+
* (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
|
|
317
|
+
* `:227`), and its only consumer in this release is the client skill picker
|
|
318
|
+
* (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
|
|
319
|
+
* model through `ctx.skills.list()` in
|
|
320
|
+
* `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
|
|
321
|
+
* - `subagentModelSelection` is a settings owner answering `{ enabled,
|
|
322
|
+
* allowedModels }`, sampled when an Agent receives its delegation tools. Model
|
|
323
|
+
* routing is `llm` and delegation is `subagents`; neither is here.
|
|
324
|
+
* - `deepseekLlmApiExtensions` hands a substitute the serialized request body
|
|
325
|
+
* and merges the fields it returns, but
|
|
326
|
+
* `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
|
|
327
|
+
* field colliding with the base request, so `messages`, `tools` and `model`
|
|
328
|
+
* are not writable through it. The constraint is in the adapter, not the
|
|
329
|
+
* registry the substitution replaces.
|
|
330
|
+
* - `inspector` is declared by the catalogue and implemented by no shipped
|
|
331
|
+
* package: in `0.1.2-rc.1` the key, `InspectorJsonValue` and
|
|
332
|
+
* `CordisRuntimeTreeReader` occur only in
|
|
333
|
+
* `@deepseek-ai/dsh-tool-cordis/lib/index.js`, and nothing provides or reads
|
|
334
|
+
* `ctx.inspector`. No composed row enforces anything through it, so a
|
|
335
|
+
* substitution displaces nothing. This is the one entry decided from the
|
|
336
|
+
* catalogue rather than from an implementation; a release that ships one is a
|
|
337
|
+
* reason to decide it again.
|
|
338
|
+
*/
|
|
222
339
|
export const SECURITY_SEAM_KEYS = new Set([
|
|
223
|
-
'approval', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials',
|
|
224
|
-
'
|
|
340
|
+
'approval', 'authorization', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials',
|
|
341
|
+
'credentialsController', 'settingsController', 'sessionController', 'webhookRuntime',
|
|
342
|
+
'subprocess', 'shell', 'fs', 'tools', 'agentLoop', 'sessionPersistence', 'sessionTelemetry',
|
|
343
|
+
'invariants',
|
|
225
344
|
]);
|
|
226
345
|
/**
|
|
227
346
|
* Waterfall events, from `EVENT_API` in the api-catalog. A listener on one of
|
|
@@ -229,16 +348,32 @@ export const SECURITY_SEAM_KEYS = new Set([
|
|
|
229
348
|
* without calling it short-circuits the chain including the built-in behavior.
|
|
230
349
|
*
|
|
231
350
|
* Note there is no `fs/read-intent` — the intent family is write and edit only.
|
|
351
|
+
*
|
|
352
|
+
* `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
|
|
353
|
+
* and adds `user-questions/request`. Both replace content in a durable log copy
|
|
354
|
+
* or answer a pending request; neither is an `emit` event, so both hand a
|
|
355
|
+
* listener the trailing `next`.
|
|
232
356
|
*/
|
|
233
357
|
export const WATERFALL_EVENTS = new Set([
|
|
234
358
|
'agent/pre-step', 'agent/request', 'agent/request-error', 'approval/request',
|
|
235
359
|
'fs/edit-intent', 'fs/write-intent', 'llm/stream', 'session-telemetry/record',
|
|
236
|
-
'system-prompt/assemble', 'tools/
|
|
237
|
-
'tools/
|
|
360
|
+
'system-prompt/assemble', 'tools/execute', 'tools/post-execute',
|
|
361
|
+
'tools/pre-execute', 'tools/ptc-dispatch-log', 'user-questions/request',
|
|
238
362
|
]);
|
|
239
|
-
/**
|
|
363
|
+
/**
|
|
364
|
+
* Waterfall events whose short-circuit removes a decision the user would
|
|
365
|
+
* otherwise make.
|
|
366
|
+
*
|
|
367
|
+
* `user-questions/request` is here for the same reason `approval/request` is:
|
|
368
|
+
* `ctx.userQuestions` pauses a tool call until a human answers, and the
|
|
369
|
+
* answerers that put the question on a screen are listeners in the chain rather
|
|
370
|
+
* than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
|
|
371
|
+
* A listener that returns an answer without calling `next()` answers on the
|
|
372
|
+
* user's behalf and the question is never shown.
|
|
373
|
+
*/
|
|
240
374
|
export const DECISION_EVENTS = new Set([
|
|
241
375
|
'approval/request', 'tools/pre-execute', 'tools/execute', 'fs/write-intent', 'fs/edit-intent',
|
|
376
|
+
'user-questions/request',
|
|
242
377
|
]);
|
|
243
378
|
/**
|
|
244
379
|
* Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
|
|
@@ -252,6 +387,8 @@ export const SANDBOX_DENIED_GLOBALS = new Map([
|
|
|
252
387
|
['setTimeout', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
253
388
|
['setInterval', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
254
389
|
['setImmediate', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
390
|
+
['clearTimeout', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
391
|
+
['clearInterval', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
255
392
|
]);
|
|
256
393
|
/**
|
|
257
394
|
* Node builtins that start or evaluate code off the mediated path. A mounted
|
|
@@ -389,3 +526,130 @@ export const HARNESS_INERT_CALLS = new Set([
|
|
|
389
526
|
* in code, and it is plain YAML.
|
|
390
527
|
*/
|
|
391
528
|
export const SERVICE_REMAPPING_FIELDS = ['isolate', 'intercept'];
|
|
529
|
+
/**
|
|
530
|
+
* How a Cordis waterfall listener delegates, and what happens when it does not.
|
|
531
|
+
*
|
|
532
|
+
* Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
|
|
533
|
+
* `lib/index.js:317-327`:
|
|
534
|
+
*
|
|
535
|
+
* ```js
|
|
536
|
+
* waterfall(...args) {
|
|
537
|
+
* const cbs = this.dispatch("waterfall", args);
|
|
538
|
+
* const inner = args.pop();
|
|
539
|
+
* const next = () => { return (cbs.shift() ?? inner)(...args); };
|
|
540
|
+
* args.push(next);
|
|
541
|
+
* return next();
|
|
542
|
+
* }
|
|
543
|
+
* ```
|
|
544
|
+
*
|
|
545
|
+
* `next` is the trailing argument every listener receives, and `inner` is the
|
|
546
|
+
* harness's own built-in behavior. A listener that returns without calling
|
|
547
|
+
* `next()` therefore ends the chain: neither the listeners still in `cbs` nor
|
|
548
|
+
* `inner` run.
|
|
549
|
+
*
|
|
550
|
+
* The scope of that is one dispatch, not the registry. `dispatch()` builds
|
|
551
|
+
* `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
|
|
552
|
+
* never touched and every skipped listener is registered and runs normally on
|
|
553
|
+
* the next dispatch. The precise word is veto, not removal — Cordis's own
|
|
554
|
+
* JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
|
|
555
|
+
* the built-in behavior". Removal is a different capability with a different
|
|
556
|
+
* reach, and it has its own table below.
|
|
557
|
+
*/
|
|
558
|
+
export const WATERFALL_NEXT_PARAMETER = 'next';
|
|
559
|
+
/**
|
|
560
|
+
* What each decision waterfall's built-in `next` settles on when no listener
|
|
561
|
+
* claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
|
|
562
|
+
*
|
|
563
|
+
* This is what a listener that never calls `next()` replaces. The inner
|
|
564
|
+
* callback is the last argument at each site:
|
|
565
|
+
* - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
|
|
566
|
+
* `() => Promise.resolve({ kind: "allow" })`
|
|
567
|
+
* - `tools/execute` — `dsh-tools/lib/index.js:3214`,
|
|
568
|
+
* `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
|
|
569
|
+
* body of the tool call itself
|
|
570
|
+
* - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
|
|
571
|
+
* `() => Promise.resolve("unavailable")`, and the surface that would ask the
|
|
572
|
+
* user is one of the listeners in the chain rather than the inner callback
|
|
573
|
+
* - `user-questions/request` —
|
|
574
|
+
* `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
|
|
575
|
+
* callback passed at `:69`, which rejects with a `UserQuestionError` carrying
|
|
576
|
+
* code `NO_PROVIDER`
|
|
577
|
+
*
|
|
578
|
+
* The three tables in this module that name events (`WATERFALL_EVENTS`,
|
|
579
|
+
* `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
|
|
580
|
+
*/
|
|
581
|
+
export const DECISION_EVENT_DEFAULTS = new Map([
|
|
582
|
+
['approval/request', 'the request falls through to `"unavailable"` only after every composed answerer — '
|
|
583
|
+
+ 'including the surface that would ask the user — has had the dispatch'],
|
|
584
|
+
['tools/pre-execute', 'the gate settles on `{ kind: "allow" }` after every other listener, and only then are '
|
|
585
|
+
+ '`ctx.tools.guard()` denials consulted'],
|
|
586
|
+
['tools/execute', 'the tool body itself runs'],
|
|
587
|
+
['fs/write-intent', 'the write intent reaches the policy rows that decide it'],
|
|
588
|
+
['fs/edit-intent', 'the edit intent reaches the policy rows that decide it'],
|
|
589
|
+
['user-questions/request', 'the request rejects with `NO_PROVIDER` only after every composed answerer — '
|
|
590
|
+
+ 'including the one that puts the question on the user\'s screen — has had the dispatch'],
|
|
591
|
+
]);
|
|
592
|
+
/**
|
|
593
|
+
* Receivers whose members name a plugin context.
|
|
594
|
+
*
|
|
595
|
+
* The same set the Tier C detached-member check guards on, minus `process`:
|
|
596
|
+
* a seam is read off the context, never off `process`.
|
|
597
|
+
*/
|
|
598
|
+
export const CONTEXT_RECEIVERS = new Set([
|
|
599
|
+
'ctx', 'context', 'globalThis', 'global',
|
|
600
|
+
]);
|
|
601
|
+
/**
|
|
602
|
+
* Array and collection methods that change the receiver rather than reading it.
|
|
603
|
+
*
|
|
604
|
+
* Used to tell a write into a service's internals from a read of them. The
|
|
605
|
+
* distinction is not academic: `dsh-dlp` reads
|
|
606
|
+
* `ctx.events._hooks['approval/request']?.length` to decide whether an ask
|
|
607
|
+
* would reach a human, which is an honest use of the same property a hostile
|
|
608
|
+
* layer splices.
|
|
609
|
+
*/
|
|
610
|
+
export const MUTATING_METHODS = new Set([
|
|
611
|
+
'splice', 'push', 'pop', 'shift', 'unshift', 'fill', 'sort', 'reverse', 'copyWithin',
|
|
612
|
+
'clear', 'delete', 'set', 'add',
|
|
613
|
+
]);
|
|
614
|
+
/**
|
|
615
|
+
* Cordis internals through which one plugin removes another plugin's
|
|
616
|
+
* registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
|
|
617
|
+
*
|
|
618
|
+
* None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
|
|
619
|
+
* `ctx.reflect` are own properties of the root context inherited by every
|
|
620
|
+
* child, so no `inject` declaration is needed to reach any of them.
|
|
621
|
+
*/
|
|
622
|
+
export const TEARDOWN_SURFACES = [
|
|
623
|
+
{
|
|
624
|
+
service: 'events',
|
|
625
|
+
member: '_hooks',
|
|
626
|
+
readIsEnough: false,
|
|
627
|
+
effect: 'the listener table every layer\'s `ctx.on()` registration is stored in (`lib/index.js:230`, '
|
|
628
|
+
+ '`_hooks = {}`, appended to by `register` at `lib/index.js:336-345`). Splicing an entry out removes that '
|
|
629
|
+
+ 'listener permanently, and the owning layer\'s own disposer then silently does nothing. This is a stronger '
|
|
630
|
+
+ 'reach than a waterfall veto, which only skips listeners for one dispatch',
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
service: 'events',
|
|
634
|
+
member: 'unregister',
|
|
635
|
+
readIsEnough: true,
|
|
636
|
+
effect: 'the public removal path for one listener, by callback identity (`lib/index.js:353-359`). It takes the '
|
|
637
|
+
+ 'listener list and a callback and splices, with no check that the caller owns either',
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
service: 'registry',
|
|
641
|
+
member: 'delete',
|
|
642
|
+
readIsEnough: true,
|
|
643
|
+
effect: 'disposal of every fiber a plugin owns (`lib/index.js:1564-1571`: `for (const fiber of runtime.fibers) '
|
|
644
|
+
+ 'fiber.dispose();`). It takes no ownership check, so one layer can unload another layer outright — '
|
|
645
|
+
+ 'including a security layer whose guards and listeners then stop existing',
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
service: 'reflect',
|
|
649
|
+
member: 'store',
|
|
650
|
+
readIsEnough: false,
|
|
651
|
+
effect: 'the service implementation table keyed by isolate symbol (`lib/index.js:726`, written by `provide` at '
|
|
652
|
+
+ '`lib/index.js:813`). `provide` throws when a key is already taken and `set` throws across fibers; writing '
|
|
653
|
+
+ 'this object directly is the path around both throws',
|
|
654
|
+
},
|
|
655
|
+
];
|
package/lib/npm.js
CHANGED
|
@@ -7,16 +7,27 @@
|
|
|
7
7
|
* fixed and their order is the guarantee:
|
|
8
8
|
*
|
|
9
9
|
* 1. read the version document (~3 KB) — which already answers
|
|
10
|
-
* `hasInstallScript`, the install lifecycle scripts,
|
|
10
|
+
* `hasInstallScript`, the install lifecycle scripts, `dsh.bundle`, and
|
|
11
|
+
* whether the registry holds a provenance attestation at all;
|
|
11
12
|
* 2. download the tarball into memory;
|
|
12
13
|
* 3. verify `dist.integrity` **before** anything parses a byte of it;
|
|
13
|
-
* 4.
|
|
14
|
+
* 4. read the provenance attestation, when step 1 said there is one, and check
|
|
15
|
+
* it against the bytes step 3 vouched for;
|
|
16
|
+
* 5. decode in memory and analyse, exactly as the tarball path does.
|
|
17
|
+
*
|
|
18
|
+
* Step 4 is the only request this module makes that is not unconditional, and
|
|
19
|
+
* it is skipped for every package the version document says has no attestation
|
|
20
|
+
* — which on the measured corpus is 28 packages in 40. It never fails an
|
|
21
|
+
* analysis: an endpoint that is down or a bundle that does not decode leaves
|
|
22
|
+
* the provenance fact in state `unreadable`, which is a different answer from
|
|
23
|
+
* `absent` and is printed as one.
|
|
14
24
|
*
|
|
15
25
|
* No subprocess, no disk write, no lifecycle script, and no `npm pack`.
|
|
16
26
|
* @module dsh-plugin-inspector/npm
|
|
17
27
|
*/
|
|
28
|
+
import { provenanceAbsent, provenanceUnreadable, readProvenance, } from "./attestation.js";
|
|
18
29
|
import { analyze } from "./inspect.js";
|
|
19
|
-
import { DEFAULT_REGISTRY, fetchVerifiedTarball, parseSpec, resolvePackage, } from "./registry.js";
|
|
30
|
+
import { attestationUrl, DEFAULT_REGISTRY, fetchAttestation, fetchVerifiedTarball, parseSpec, RegistryError, resolvePackage, } from "./registry.js";
|
|
20
31
|
import { loadTarballBuffer } from "./source.js";
|
|
21
32
|
/**
|
|
22
33
|
* The metadata pre-check, which needs no tarball.
|
|
@@ -31,6 +42,46 @@ import { loadTarballBuffer } from "./source.js";
|
|
|
31
42
|
export async function precheck(spec, options = {}) {
|
|
32
43
|
return resolvePackage(parseSpec(spec), options);
|
|
33
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the registry's provenance attestation for a resolved package, when it
|
|
47
|
+
* has one, and check it against the bytes that were downloaded.
|
|
48
|
+
*
|
|
49
|
+
* Every failure on this path becomes a fact rather than a refusal. The tarball
|
|
50
|
+
* has already been checked against `dist.integrity`, so the analysis is sound
|
|
51
|
+
* whatever the attestation endpoint does, and turning a registry outage into
|
|
52
|
+
* exit code 2 would make provenance a precondition for reading a package
|
|
53
|
+
* instead of something reported about it.
|
|
54
|
+
* @param resolved - the packument reading for the version.
|
|
55
|
+
* @param registry - the registry base URL, without a trailing slash.
|
|
56
|
+
* @param tarball - the verified tarball bytes.
|
|
57
|
+
* @param options - where to fetch from.
|
|
58
|
+
* @returns what the registry says about the build origin, and what was checked.
|
|
59
|
+
*/
|
|
60
|
+
async function readRegistryProvenance(resolved, registry, tarball, options) {
|
|
61
|
+
if (resolved.provenancePredicateType === null)
|
|
62
|
+
return provenanceAbsent();
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = attestationUrl(registry, resolved.name, resolved.version);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
/* v8 ignore next -- `attestationUrl` refuses a name or version only with a RegistryError. */
|
|
69
|
+
if (!(error instanceof RegistryError))
|
|
70
|
+
throw error;
|
|
71
|
+
return provenanceUnreadable(null, error.message);
|
|
72
|
+
}
|
|
73
|
+
let body;
|
|
74
|
+
try {
|
|
75
|
+
body = await fetchAttestation(url, options);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
/* v8 ignore next -- `fetchAttestation` reports every refusal as a RegistryError. */
|
|
79
|
+
if (!(error instanceof RegistryError))
|
|
80
|
+
throw error;
|
|
81
|
+
return provenanceUnreadable(url, error.message);
|
|
82
|
+
}
|
|
83
|
+
return readProvenance(body, { name: resolved.name, version: resolved.version, tarball, url });
|
|
84
|
+
}
|
|
34
85
|
/**
|
|
35
86
|
* Fetch a published package and inspect it in memory.
|
|
36
87
|
* @param spec - `<name>` or `<name>@<version>`; no version means the `latest` tag.
|
|
@@ -42,9 +93,11 @@ export async function precheck(spec, options = {}) {
|
|
|
42
93
|
export async function inspectFromNpm(spec, options = {}) {
|
|
43
94
|
const resolved = await precheck(spec, options);
|
|
44
95
|
const verified = await fetchVerifiedTarball(resolved, options);
|
|
96
|
+
const registry = (options.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, '');
|
|
97
|
+
const attestation = await readRegistryProvenance(resolved, registry, verified.bytes, options);
|
|
45
98
|
const provenance = {
|
|
46
99
|
spec,
|
|
47
|
-
registry
|
|
100
|
+
registry,
|
|
48
101
|
resolvedVersion: resolved.version,
|
|
49
102
|
tarball: resolved.tarball,
|
|
50
103
|
digest: verified.digest,
|
|
@@ -54,5 +107,5 @@ export async function inspectFromNpm(spec, options = {}) {
|
|
|
54
107
|
tarballBytes: verified.bytes.byteLength,
|
|
55
108
|
};
|
|
56
109
|
const source = await loadTarballBuffer(verified.bytes, `npm:${resolved.name}@${resolved.version}`);
|
|
57
|
-
return analyze(source, provenance);
|
|
110
|
+
return analyze(source, provenance, attestation);
|
|
58
111
|
}
|
package/lib/registry.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* @module dsh-plugin-inspector/registry
|
|
20
20
|
*/
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
|
+
import { MAX_ATTESTATION_BYTES } from "./attestation.js";
|
|
22
23
|
import { INSTALL_LIFECYCLE_SCRIPTS } from "./knowledge.js";
|
|
23
24
|
import { MAX_TOTAL_BYTES } from "./source.js";
|
|
24
25
|
/** The public npm registry, used when no other is named. */
|
|
@@ -146,6 +147,7 @@ export async function resolvePackage(spec, options = {}) {
|
|
|
146
147
|
integrity: typeof dist.integrity === 'string' ? dist.integrity : null,
|
|
147
148
|
shasum: typeof dist.shasum === 'string' ? dist.shasum : null,
|
|
148
149
|
hasInstallScript: record.hasInstallScript === true,
|
|
150
|
+
provenancePredicateType: asString(asRecord(asRecord(dist.attestations).provenance), 'predicateType'),
|
|
149
151
|
lifecycleScripts: INSTALL_LIFECYCLE_SCRIPTS.filter(name => typeof scripts[name] === 'string'),
|
|
150
152
|
bundlePatch: typeof bundle.patch === 'string' ? bundle.patch : null,
|
|
151
153
|
metadataBytes: body.byteLength,
|
|
@@ -162,6 +164,16 @@ function asRecord(value) {
|
|
|
162
164
|
? value
|
|
163
165
|
: {};
|
|
164
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Read a string field, or `null` when it is absent or of another type.
|
|
169
|
+
* @param record - the containing record.
|
|
170
|
+
* @param key - the field name.
|
|
171
|
+
* @returns the string, or `null`.
|
|
172
|
+
*/
|
|
173
|
+
function asString(record, key) {
|
|
174
|
+
const value = record[key];
|
|
175
|
+
return typeof value === 'string' ? value : null;
|
|
176
|
+
}
|
|
165
177
|
/**
|
|
166
178
|
* Refuse a tarball URL that points somewhere other than the registry that
|
|
167
179
|
* described it.
|
|
@@ -260,3 +272,45 @@ export async function fetchVerifiedTarball(resolved, options = {}) {
|
|
|
260
272
|
const bytes = await readCapped(response, MAX_TARBALL_BYTES, `tarball ${resolved.tarball}`);
|
|
261
273
|
return verifyIntegrity(bytes, resolved);
|
|
262
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* The endpoint an npm-compatible registry serves a version's attestation
|
|
277
|
+
* bundle from.
|
|
278
|
+
*
|
|
279
|
+
* Built from the registry base URL rather than read out of
|
|
280
|
+
* `dist.attestations.url`, which is the opposite of how the tarball URL is
|
|
281
|
+
* handled and is deliberate: the tarball has to come from wherever the registry
|
|
282
|
+
* says because there is no other way to name it, so that URL is taken from the
|
|
283
|
+
* document and then refused unless it is same-origin. An attestation needs no
|
|
284
|
+
* such freedom. Constructing the path here means a doctored packument cannot
|
|
285
|
+
* redirect the request at all, not even to another path on the same host.
|
|
286
|
+
*
|
|
287
|
+
* The name and version are re-validated because both come out of the version
|
|
288
|
+
* document, which is registry-controlled: `name` is not necessarily the name
|
|
289
|
+
* that was asked for, and it is interpolated into a URL.
|
|
290
|
+
* @param registry - the registry base URL, without a trailing slash.
|
|
291
|
+
* @param name - the resolved package name.
|
|
292
|
+
* @param version - the resolved version.
|
|
293
|
+
* @returns the absolute URL.
|
|
294
|
+
* @throws RegistryError when the document's name or version would not address this endpoint.
|
|
295
|
+
*/
|
|
296
|
+
export function attestationUrl(registry, name, version) {
|
|
297
|
+
if (!PACKAGE_NAME.test(name))
|
|
298
|
+
throw new RegistryError(`the version document names no npm package: ${name}`);
|
|
299
|
+
if (!VERSION_OR_TAG.test(version))
|
|
300
|
+
throw new RegistryError(`the version document names no version: ${version}`);
|
|
301
|
+
return `${registry}/-/npm/v1/attestations/${name}@${version}`;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Download an attestation document.
|
|
305
|
+
*
|
|
306
|
+
* Only ever called when the version document said there is one, so a package
|
|
307
|
+
* without provenance costs no request at all.
|
|
308
|
+
* @param url - the endpoint, from {@link attestationUrl}.
|
|
309
|
+
* @param options - where to fetch from.
|
|
310
|
+
* @returns the document as served.
|
|
311
|
+
* @throws RegistryError on a transport failure, a non-2xx status, or an oversized body.
|
|
312
|
+
*/
|
|
313
|
+
export async function fetchAttestation(url, options = {}) {
|
|
314
|
+
const response = await get(url, 'application/json', options);
|
|
315
|
+
return readCapped(response, MAX_ATTESTATION_BYTES, 'attestation document');
|
|
316
|
+
}
|