@knolo/cli 5.2.2 → 5.2.3
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 -15
- package/bin/knolo.mjs +1 -1
- package/bin/registry/commands.mjs +96 -32
- package/bin/registry/http.mjs +61 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -57,28 +57,23 @@ knolo login --token kno_…
|
|
|
57
57
|
# write requests use: Authorization: Bearer kno_…
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
`knolo publish`
|
|
61
|
-
|
|
62
|
-
`
|
|
63
|
-
|
|
60
|
+
`knolo publish` asks Hub for a Blob PUT grant with the dashboard token, PUTs
|
|
61
|
+
bytes to that grant URL, then completes with the public pack URL from the PUT
|
|
62
|
+
response. A `kno_…` token is the only credential. Do not set
|
|
63
|
+
`PACKS_READ_WRITE_TOKEN` — that is Hub’s store secret, not a publisher token.
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
|
|
67
|
-
npx vercel env pull /tmp/knolo-env-prod --environment=production --yes
|
|
68
|
-
export PACKS_READ_WRITE_TOKEN="$(awk -F= '$1 == "PACKS_READ_WRITE_TOKEN" {sub(/^[^=]*=/, ""); gsub(/^\"|\"$/, ""); print; exit}' /tmp/knolo-env-prod)"
|
|
69
|
-
|
|
66
|
+
knolo login --token kno_…
|
|
70
67
|
knolo publish ./dist/knowledge.knolo \
|
|
71
68
|
--slug refund-policy --version 1.2.0 --license Apache-2.0
|
|
72
69
|
knolo yank acme/refund-policy@1.2.0
|
|
73
70
|
```
|
|
74
71
|
|
|
75
|
-
The
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
missing or the token was revoked; it does not mean the Hub write API requires
|
|
81
|
-
a GitHub browser session.
|
|
72
|
+
The grant pathname is `sha256/<64-lowercase-hex>.knolo`. Only public Blob URLs
|
|
73
|
+
(`*.public.blob.vercel-storage.com`) are sent to `/api/upload/complete`. The
|
|
74
|
+
CLI never sends the `kno_…` token to Blob, in a query string, cookie, or pack
|
|
75
|
+
bytes. A 401 usually means the `Bearer` word is missing or the token was
|
|
76
|
+
revoked; it does not mean the Hub write API requires a GitHub browser session.
|
|
82
77
|
|
|
83
78
|
The older `@knolo/cli@5.2.0` publish/yank command was an unimplemented stub;
|
|
84
79
|
current versions use the live Hub HTTP sequence. For a direct smoke test of a
|
package/bin/knolo.mjs
CHANGED
|
@@ -115,7 +115,7 @@ function printCommandHelp(command) {
|
|
|
115
115
|
login: 'Usage: knolo login [--token kno_…] [--stdin] [--registry <url>]\n\nStores the raw token locally. Hub write calls send Authorization: Bearer kno_….\nMint tokens at https://hub.knolo.dev/dashboard/tokens; login does not call POST /api/v1/tokens.',
|
|
116
116
|
whoami: 'Usage: knolo whoami',
|
|
117
117
|
logout: 'Usage: knolo logout',
|
|
118
|
-
publish: 'Usage: knolo publish <pack.knolo> --slug <slug> --version <version> --license <SPDX> [--publisher <handle>] [--readme <text>] [--sources <text>] [--intended-use <text>] [--json] [--registry <url>]\n\nUses the stored token as Authorization: Bearer kno_….
|
|
118
|
+
publish: 'Usage: knolo publish <pack.knolo> --slug <slug> --version <version> --license <SPDX> [--publisher <handle>] [--readme <text>] [--sources <text>] [--intended-use <text>] [--json] [--registry <url>]\n\nUses the stored dashboard token as Authorization: Bearer kno_…. Hub issues a public Blob PUT grant; the CLI never needs PACKS_READ_WRITE_TOKEN.\nUploads go to the grant URL without the kno_ token. Complete sends the public Blob URL, then Hub verifies before release.',
|
|
119
119
|
yank: 'Usage: knolo yank <publisher>/<slug>@<version> [--json] [--registry <url>]\n\nUses the stored token as Authorization: Bearer kno_…. Yank is owner-only and leaves the public Blob in place.',
|
|
120
120
|
build: 'Usage: knolo build',
|
|
121
121
|
query: 'Usage: knolo query <question> [--pack <path>] [--k <number>] [--receipt <file>] [--json]',
|
|
@@ -11,7 +11,6 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
11
11
|
import { isIP } from 'node:net';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { put as putBlob } from '@vercel/blob';
|
|
15
14
|
import {
|
|
16
15
|
normalizeRegistryUrl,
|
|
17
16
|
registryApiUrl,
|
|
@@ -24,7 +23,7 @@ import {
|
|
|
24
23
|
stageCachedArtifact,
|
|
25
24
|
} from './artifacts.mjs';
|
|
26
25
|
import { RegistryError } from './errors.mjs';
|
|
27
|
-
import { getJson, postJson } from './http.mjs';
|
|
26
|
+
import { getJson, postJson, putBytes } from './http.mjs';
|
|
28
27
|
import {
|
|
29
28
|
cachePath,
|
|
30
29
|
stageLockfileUpdate,
|
|
@@ -291,7 +290,6 @@ export async function runHubPublish(
|
|
|
291
290
|
homeDir,
|
|
292
291
|
print = console.log,
|
|
293
292
|
sleep = wait,
|
|
294
|
-
putImpl = putBlob,
|
|
295
293
|
pollIntervalMs = 1000,
|
|
296
294
|
maxPollAttempts = 120,
|
|
297
295
|
} = {}
|
|
@@ -329,28 +327,35 @@ export async function runHubPublish(
|
|
|
329
327
|
const sha256 = createSha256(bytes);
|
|
330
328
|
const pathname = `sha256/${sha256}.knolo`;
|
|
331
329
|
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
330
|
+
const grant = await postJson(
|
|
331
|
+
hubApiUrl(registry, ['upload', 'token']),
|
|
332
|
+
{ sha256, contentLength: bytes.length },
|
|
333
|
+
{ fetchImpl, headers: authHeaders }
|
|
334
|
+
);
|
|
335
|
+
const upload = resolveUploadGrant(grant, sha256);
|
|
336
|
+
let blobUrl;
|
|
337
|
+
if (upload.reused) {
|
|
338
|
+
blobUrl = assertPublicPackBlobUrl(upload.url, 'reused artifact').toString();
|
|
339
|
+
} else {
|
|
340
|
+
assertGrantPutUrl(upload.putUrl, 'upload grant');
|
|
341
|
+
const uploaded = await putBytes(upload.putUrl, bytes, {
|
|
342
|
+
fetchImpl,
|
|
343
|
+
headers: {
|
|
344
|
+
'content-type': 'application/octet-stream',
|
|
345
|
+
...(upload.putHeaders || {}),
|
|
346
|
+
'content-length': String(bytes.length),
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
if (uploaded.pathname !== undefined && uploaded.pathname !== pathname) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Public Blob upload pathname is not locked to ${pathname}.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
blobUrl = assertPublicPackBlobUrl(
|
|
355
|
+
uploaded.url,
|
|
356
|
+
'uploaded artifact'
|
|
357
|
+
).toString();
|
|
349
358
|
}
|
|
350
|
-
const blobUrl = assertPublicBlobUrl(
|
|
351
|
-
uploaded.url,
|
|
352
|
-
'uploaded artifact'
|
|
353
|
-
).toString();
|
|
354
359
|
|
|
355
360
|
await postJson(
|
|
356
361
|
hubApiUrl(registry, ['upload', 'complete']),
|
|
@@ -593,16 +598,59 @@ function createSha256(bytes) {
|
|
|
593
598
|
return createHash('sha256').update(bytes).digest('hex');
|
|
594
599
|
}
|
|
595
600
|
|
|
596
|
-
function
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
601
|
+
function resolveUploadGrant(body, sha256) {
|
|
602
|
+
const pathname = body?.pathname || body?.upload?.pathname;
|
|
603
|
+
const expectedPathname = `sha256/${sha256}.knolo`;
|
|
604
|
+
if (typeof pathname !== 'string' || pathname !== expectedPathname) {
|
|
605
|
+
throw new Error(`Hub upload pathname is not locked to ${expectedPathname}.`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const reusedUrl =
|
|
609
|
+
(body?.reused && (body?.artifact?.url || body?.url)) ||
|
|
610
|
+
body?.artifact?.url;
|
|
611
|
+
if (typeof reusedUrl === 'string' && reusedUrl) {
|
|
612
|
+
return { reused: true, pathname, url: reusedUrl };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const upload = body?.upload && typeof body.upload === 'object' ? body.upload : body;
|
|
616
|
+
if (!upload || typeof upload.url !== 'string' || !upload.url) {
|
|
617
|
+
if (body?.clientToken || upload?.clientToken) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
'Hub returned a Blob client token without upload.url. This CLI PUTs to upload.url; it does not use PACKS_READ_WRITE_TOKEN.'
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
throw new Error('Hub upload token response is missing upload.url.');
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
reused: false,
|
|
626
|
+
pathname,
|
|
627
|
+
putUrl: upload.url,
|
|
628
|
+
putHeaders:
|
|
629
|
+
upload.headers && typeof upload.headers === 'object' ? upload.headers : {},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function assertGrantPutUrl(value, label) {
|
|
634
|
+
let url;
|
|
635
|
+
try {
|
|
636
|
+
url = new URL(value);
|
|
637
|
+
} catch {
|
|
638
|
+
throw new Error(`Hub ${label} URL is invalid.`);
|
|
639
|
+
}
|
|
640
|
+
if (url.protocol !== 'https:')
|
|
641
|
+
throw new Error(`Hub ${label} URL must use HTTPS.`);
|
|
642
|
+
const hostname = url.hostname.toLowerCase();
|
|
643
|
+
if (
|
|
644
|
+
hostname.includes('.private.') ||
|
|
645
|
+
hostname.startsWith('private.') ||
|
|
646
|
+
isPrivateOrLocalIp(hostname) ||
|
|
647
|
+
isLocalOnlyHostname(hostname)
|
|
648
|
+
) {
|
|
602
649
|
throw new Error(
|
|
603
|
-
|
|
650
|
+
`Refusing private Blob URL for ${label}; Hub verification requires a public Blob URL.`
|
|
604
651
|
);
|
|
605
|
-
|
|
652
|
+
}
|
|
653
|
+
return url;
|
|
606
654
|
}
|
|
607
655
|
|
|
608
656
|
function extractPublisherHandle(account) {
|
|
@@ -611,8 +659,10 @@ function extractPublisherHandle(account) {
|
|
|
611
659
|
account?.publisher?.handle,
|
|
612
660
|
account?.publisher?.slug,
|
|
613
661
|
account?.handle,
|
|
662
|
+
account?.account?.handle,
|
|
614
663
|
account?.account?.publisherHandle,
|
|
615
664
|
account?.account?.publisher?.handle,
|
|
665
|
+
account?.user?.handle,
|
|
616
666
|
account?.user?.publisherHandle,
|
|
617
667
|
account?.publishers?.[0]?.handle,
|
|
618
668
|
account?.publishers?.[0]?.slug,
|
|
@@ -655,6 +705,20 @@ function assertPublicBlobUrl(value, label) {
|
|
|
655
705
|
return url;
|
|
656
706
|
}
|
|
657
707
|
|
|
708
|
+
function assertPublicPackBlobUrl(value, label) {
|
|
709
|
+
const url = assertPublicBlobUrl(value, label);
|
|
710
|
+
const hostname = url.hostname.toLowerCase();
|
|
711
|
+
const blobHost =
|
|
712
|
+
hostname === 'blob.vercel-storage.com' ||
|
|
713
|
+
hostname.endsWith('.blob.vercel-storage.com');
|
|
714
|
+
if (!blobHost || hostname.includes('.private.')) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
`Hub ${label} URL must be a public Vercel Blob pack URL (not a signed grant URL).`
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
return url;
|
|
720
|
+
}
|
|
721
|
+
|
|
658
722
|
function isLocalOnlyHostname(hostname) {
|
|
659
723
|
const normalized = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
660
724
|
return (
|
package/bin/registry/http.mjs
CHANGED
|
@@ -19,6 +19,67 @@ export async function postJson(
|
|
|
19
19
|
return requestJson(url, { fetchImpl, headers, method: 'POST', body });
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export async function putBytes(
|
|
23
|
+
url,
|
|
24
|
+
bytes,
|
|
25
|
+
{ fetchImpl = globalThis.fetch, headers = {} } = {}
|
|
26
|
+
) {
|
|
27
|
+
if (typeof fetchImpl !== 'function')
|
|
28
|
+
throw new Error('This Node runtime does not provide fetch.');
|
|
29
|
+
|
|
30
|
+
let response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetchImpl(url, {
|
|
33
|
+
method: 'PUT',
|
|
34
|
+
redirect: 'follow',
|
|
35
|
+
headers: { 'content-type': 'application/octet-stream', ...headers },
|
|
36
|
+
body: bytes,
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new RegistryNetworkError(
|
|
40
|
+
`Could not upload artifact to ${new URL(url).origin}.`,
|
|
41
|
+
{ url: String(url), cause: error }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const text = await readResponseText(response, url);
|
|
46
|
+
let parsed;
|
|
47
|
+
if (text) {
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(text);
|
|
50
|
+
} catch {
|
|
51
|
+
parsed = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const detail =
|
|
57
|
+
(parsed && typeof parsed.error === 'object' && parsed.error?.message) ||
|
|
58
|
+
(typeof parsed?.error === 'string' && parsed.error) ||
|
|
59
|
+
(typeof parsed?.message === 'string' && parsed.message) ||
|
|
60
|
+
(text ? text.slice(0, 240) : '');
|
|
61
|
+
throw new RegistryError(
|
|
62
|
+
`Blob upload failed with HTTP ${response.status}${detail ? `: ${detail}` : '.'}`,
|
|
63
|
+
{
|
|
64
|
+
status: response.status,
|
|
65
|
+
url: String(url),
|
|
66
|
+
body: parsed ?? text,
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const publicUrl =
|
|
72
|
+
(parsed && typeof parsed.url === 'string' && parsed.url) ||
|
|
73
|
+
response.url ||
|
|
74
|
+
String(url);
|
|
75
|
+
return {
|
|
76
|
+
status: response.status,
|
|
77
|
+
url: publicUrl,
|
|
78
|
+
pathname: parsed && typeof parsed.pathname === 'string' ? parsed.pathname : undefined,
|
|
79
|
+
body: parsed,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
22
83
|
async function requestJson(
|
|
23
84
|
url,
|
|
24
85
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knolo/cli",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"knolo": "bin/knolo.mjs"
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
"test:hub": "node --test test/hub.integration.test.mjs"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@knolo/core": "^5.1.0"
|
|
36
|
-
"@vercel/blob": "2.8.0"
|
|
35
|
+
"@knolo/core": "^5.1.0"
|
|
37
36
|
},
|
|
38
37
|
"private": false
|
|
39
38
|
}
|