From 8129a70fb2d7c5970ccc9d0cc8f52e94baa79503 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 18 Oct 2026 19:58:41 +0530 Subject: [PATCH] fix: read PRIOMPT_TOKEN and re-sync the vendored proto The client ignored PRIOMPT_TOKEN, which the CLI and docs treat as the client credential. The token is now, in order: the explicit token, PRIOMPT_TOKEN, then the token in the url / PRIOMPT_URL, matching the priompt CLI and the web UI. Re-sync proto/priompt/v1/prompt.proto from priompt/proto main (e7dce07); the only change is the go_package path. Adds an npm test suite (node:test) and runs it in CI. Fixes #7 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 1 + README.md | 3 +- client.js | 9 ++++-- package.json | 3 +- proto/priompt/v1/prompt.proto | 2 +- test/token.test.js | 52 +++++++++++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 test/token.test.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72e9309..ac09b02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,5 +15,6 @@ jobs: node-version: 24 - run: npm install - run: node --check client.js + - run: npm test # construct the client so the vendored proto is parsed at load time - run: node -e "new (require('./client.js').PromptClient)({host:'localhost:1'})" diff --git a/README.md b/README.md index 6fb9a58..dc11b57 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ What comes back from `get()`: One environment variable configures everything: `PRIOMPT_URL=priompt://@host:port` carries the address *and* the credential, so `new PromptClient()` with no arguments just works. +`PRIOMPT_TOKEN`, as the CLI reads it, wins over the token in the url. ```js new PromptClient({ host, token, tls = false, natsUrl, natsToken, url }) @@ -58,7 +59,7 @@ new PromptClient({ host, token, tls = false, natsUrl, natsToken, url }) | --- | --- | | `host` | `address:port` of the server. Optional if `url` or `PRIOMPT_URL` is set | | `url` | a full `priompt://@host:port` string; defaults to `PRIOMPT_URL` | -| `token` | sent as `authorization: Bearer `; explicit value wins over the url's token | +| `token` | sent as `authorization: Bearer `; defaults to `PRIOMPT_TOKEN`, then the url's token | | `tls` | use TLS to reach the server | | `caCert` | path to the CA certificate that signed the server's cert — required for a private CA, which is the usual self-hosted case | | `clientCert` / `clientKey` | paths to a client certificate and key, for a server started with `-client-ca` (mTLS). Both or neither | diff --git a/client.js b/client.js index 159d35a..596d256 100644 --- a/client.js +++ b/client.js @@ -48,17 +48,20 @@ function parseNatsUrl(raw) { class PromptClient { // One connection string covers local/self-host/cloud: pass url or set - // PRIOMPT_URL (priompt://@host:port). An explicit host wins; an - // explicit token overrides the URL's token. + // PRIOMPT_URL (priompt://@host:port). An explicit host wins. The + // token is, in order: the explicit token, PRIOMPT_TOKEN (as the CLI reads + // it), then the URL's token. constructor({ host, token, tls = false, caCert, clientCert, clientKey, natsUrl, natsToken, url } = {}) { + let urlToken; if (!host) { const raw = url || process.env.PRIOMPT_URL; if (raw) { const parsed = parseUrl(raw); host = parsed.host; - if (token === undefined) token = parsed.token; + urlToken = parsed.token; } } + if (token === undefined) token = process.env.PRIOMPT_TOKEN || urlToken; if (!host) throw new Error("PromptClient needs host, url, or PRIOMPT_URL"); const pkg = loadService(); // caCert, clientCert and clientKey are file paths. diff --git a/package.json b/package.json index bb7e867..dd8326b 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "access": "public" }, "scripts": { - "sync-proto": "node -e \"const fs=require('fs');fs.mkdirSync('proto/priompt/v1',{recursive:true});fs.copyFileSync('../proto/proto/priompt/v1/prompt.proto','proto/priompt/v1/prompt.proto')\"" + "sync-proto": "node -e \"const fs=require('fs');fs.mkdirSync('proto/priompt/v1',{recursive:true});fs.copyFileSync('../proto/proto/priompt/v1/prompt.proto','proto/priompt/v1/prompt.proto')\"", + "test": "node --test \"test/*.test.js\"" }, "dependencies": { "@grpc/grpc-js": "^1.10.0", diff --git a/proto/priompt/v1/prompt.proto b/proto/priompt/v1/prompt.proto index 76f5b2d..5c06201 100644 --- a/proto/priompt/v1/prompt.proto +++ b/proto/priompt/v1/prompt.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package priompt.v1; -option go_package = "priomptproto/gen/priompt/v1;priomptv1"; +option go_package = "github.com/priompt/proto/gen/priompt/v1;priomptv1"; // PromptService serves validated prompts. Phase 1 is read-only over the wire; // writes happen via the `priompt put` CLI until promptctl lands (Phase 3). diff --git a/test/token.test.js b/test/token.test.js new file mode 100644 index 0000000..63a6f9f --- /dev/null +++ b/test/token.test.js @@ -0,0 +1,52 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const fs = require("fs"); +const path = require("path"); +const { PromptClient } = require("../client"); + +function bearer(opts, env) { + const saved = { PRIOMPT_URL: process.env.PRIOMPT_URL, PRIOMPT_TOKEN: process.env.PRIOMPT_TOKEN }; + for (const k of Object.keys(saved)) { + if (k in env) process.env[k] = env[k]; + else delete process.env[k]; + } + try { + const c = new PromptClient(opts); + c.close(); + const v = c._meta.get("authorization"); + return v.length ? v[0] : undefined; + } finally { + for (const [k, v] of Object.entries(saved)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + } +} + +test("PRIOMPT_TOKEN is sent as the bearer token", () => { + assert.strictEqual(bearer({ host: "localhost:1" }, { PRIOMPT_TOKEN: "envtok" }), "Bearer envtok"); +}); + +test("PRIOMPT_TOKEN wins over the token in PRIOMPT_URL", () => { + assert.strictEqual( + bearer({}, { PRIOMPT_URL: "priompt://urltok@localhost:1", PRIOMPT_TOKEN: "envtok" }), + "Bearer envtok" + ); +}); + +test("an explicit token wins over PRIOMPT_TOKEN", () => { + assert.strictEqual(bearer({ host: "localhost:1", token: "argtok" }, { PRIOMPT_TOKEN: "envtok" }), "Bearer argtok"); +}); + +test("the token in PRIOMPT_URL is used when PRIOMPT_TOKEN is unset", () => { + assert.strictEqual(bearer({}, { PRIOMPT_URL: "priompt://urltok@localhost:1" }), "Bearer urltok"); +}); + +test("no token configured sends no authorization header", () => { + assert.strictEqual(bearer({ host: "localhost:1" }, {}), undefined); +}); + +test("vendored proto carries the published go_package", () => { + const proto = fs.readFileSync(path.join(__dirname, "../proto/priompt/v1/prompt.proto"), "utf8"); + assert.match(proto, /option go_package = "github\.com\/priompt\/proto\/gen\/priompt\/v1;priomptv1";/); +});