allowfy-check 0.1.1 → 0.1.2
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 +102 -29
- package/allowfy-check.mjs +132 -2
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,42 +1,115 @@
|
|
|
1
|
-
# Allowfy Check:
|
|
1
|
+
# Allowfy Check: the Poppy conformance tester
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Is your site ready for AI agents?** Test any domain against **Poppy**, the [Personal Agent Protocol](https://personalagentprotocol.org), in one command.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/allowfy-check) [](LICENSE) [](#what-it-checks)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```
|
|
8
|
+
npx allowfy-check check https://your-domain.com
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Customers are starting to send AI assistants to book, buy and change orders for them. Poppy is the open protocol for how those agents do it:
|
|
12
|
+
- they find a business through `/.well-known/poppy.json`;
|
|
13
|
+
- they prove who they are with signed assertions;
|
|
14
|
+
- they hold DPoP-bound tokens;
|
|
15
|
+
- they sign customers in;
|
|
16
|
+
- they propose changes that customers approve.
|
|
17
|
+
|
|
18
|
+
Allowfy Check tells you, requirement by requirement, what your implementation gets right and what it doesn't. It shows exactly what was sent, what came back, and the line of the spec it's held to.
|
|
19
|
+
|
|
20
|
+
**No terminal?** Run the first 20 checks in your browser at **[check.allowfy.com](https://check.allowfy.com)**.
|
|
21
|
+
|
|
22
|
+
## What it checks
|
|
23
|
+
|
|
24
|
+
111 checks: all of the official validator's, plus 37 for the parts it doesn't cover yet.
|
|
25
|
+
|
|
26
|
+
| Milestone | Checks | Covers |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| 1. Foundation | 42 | `poppy.json` discovery, OAuth server metadata, Sessions, DPoP (replay, key binding, `ath`, nonces) |
|
|
29
|
+
| 2. Interfaces | 32 | OpenAPI, MCP servers (Bearer and DPoP), conversations |
|
|
30
|
+
| 3. Sign-in and accounts | 17 | **Device Sign-In, Account Tokens, sign-out:** `invalid_scope`, `account_mismatch`, narrower scopes only, immediate sign-out |
|
|
31
|
+
| 4. Operations extension | 20 | **Propose, confirm, cancel:** exact-revision approval, `terms_changed`, at most once, ownership by account, `Retry-After` |
|
|
32
|
+
|
|
33
|
+
Milestones 3 and 4 come from this project. Every check quotes the requirement it tests, word for word, and links to it.
|
|
34
|
+
|
|
35
|
+
## What you see
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
✗ discovery.https poppy.json is served over HTTPS at the well-known path spec 3
|
|
39
|
+
Requirement: "A Company MUST publish its poppy.json document at
|
|
40
|
+
https://{domain}/.well-known/poppy.json over HTTPS"
|
|
41
|
+
Sent: GET https://example.com/.well-known/poppy.json
|
|
42
|
+
Got: 404 Not Found
|
|
43
|
+
Expected: 200 with the poppy.json document
|
|
44
|
+
Likely cause: Nothing is published at https://example.com/.well-known/poppy.json.
|
|
45
|
+
Some frameworks and static hosts skip dot-directories like
|
|
46
|
+
.well-known unless configured to serve them.
|
|
47
|
+
✓ discovery.redirects-https Every redirect of the well-known URL is to an HTTPS URL
|
|
48
|
+
- discovery.json poppy.json is a JSON object
|
|
49
|
+
poppy.json couldn't be loaded (see discovery.https).
|
|
50
|
+
```
|
|
8
51
|
|
|
9
|
-
|
|
10
|
-
|---|---|
|
|
11
|
-
| 3. Sign-in and accounts | **Device Sign-In (4.6):** the request carries no proof and no Session; `invalid_scope`; the link's host; pending; opening the link doesn't approve; tokens; granted scopes; declined; `account_mismatch`.<br>**Account Tokens (4.8):** the same Session; a new Session; narrower scope only; same User only; not accepted at APIs.<br>**Signing out (4.9):** revoking; `invalid_grant`; immediate effect. |
|
|
12
|
-
| 4. Operations extension v1 | Advertised; `operation_not_found`; proposing with 202; fields; nothing performed before confirming; readable; responses are the operation itself; reuse; agents without the extension; `terms_changed`; `user_approval_required`; account ownership (other Sessions, signed out, other Users); cancel; final; `result.summary`; confirm; at most once; `Retry-After`. |
|
|
52
|
+
## Deeper checks: Sessions, sign-in and operations
|
|
13
53
|
|
|
14
|
-
|
|
54
|
+
These need a test Personal Agent: a key pair, plus two small files you host anywhere over HTTPS.
|
|
15
55
|
|
|
16
|
-
## Run
|
|
17
56
|
```
|
|
18
|
-
npx allowfy-check
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
npx allowfy-check keygen --base-url https://staging.example.com/poppy-test
|
|
58
|
+
# host .poppy/agent.json and .poppy/jwks.json at the URLs it prints, then:
|
|
59
|
+
npx allowfy-check check https://staging.example.com \
|
|
60
|
+
--probe https://api.staging.example.com/me \
|
|
61
|
+
--propose 'POST https://api.staging.example.com/orders/ord_1/exchanges {"item_id":"itm_1"}'
|
|
21
62
|
```
|
|
22
|
-
|
|
23
|
-
- **Sign-in.** The
|
|
63
|
+
|
|
64
|
+
- **Sign-in.** The tool shows you the sign-in link and code, and you approve in a browser.
|
|
24
65
|
- `--decline` also asks you to decline one sign-in.
|
|
25
|
-
- `--other-account` asks you to
|
|
26
|
-
- **
|
|
27
|
-
-
|
|
66
|
+
- `--other-account` asks you to use a second test account.
|
|
67
|
+
- **Proposing is safe.** `--propose` only proposes and cancels, so nothing is performed.
|
|
68
|
+
- **`--execute`** also confirms one proposal, so your system performs it. Use it in a test environment only.
|
|
69
|
+
- **Agents without the extension.** `--plain-identity <dir>` adds a second agent whose `agent.json` doesn't list the operations extension, to check it gets no operations.
|
|
70
|
+
- **`--json`** prints machine-readable results.
|
|
71
|
+
|
|
72
|
+
## In CI
|
|
73
|
+
|
|
74
|
+
The exit code is 1 when a MUST check fails, so conformance can't silently break:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
# .github/workflows/poppy.yml
|
|
78
|
+
name: Poppy conformance
|
|
79
|
+
on: [push, pull_request]
|
|
80
|
+
jobs:
|
|
81
|
+
poppy:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/setup-node@v4
|
|
85
|
+
with: { node-version: 22 }
|
|
86
|
+
- run: npx allowfy-check@latest check https://staging.example.com --milestone interfaces
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How it relates to the official validator
|
|
90
|
+
|
|
91
|
+
The Poppy project publishes an official company validator. It covers milestones 1 and 2, and says that sign-in and extensions aren't checked yet. Allowfy Check:
|
|
92
|
+
- runs that validator **unmodified** (vendored under `vendor/`, Apache 2.0);
|
|
93
|
+
- adds the sign-in, account and operations checks in the same style, with the same result format.
|
|
94
|
+
|
|
95
|
+
It's independent: **not affiliated with the protocol's authors.** Where a result looks wrong, each failure links to the spec's feedback form.
|
|
96
|
+
|
|
97
|
+
## Develop
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
npm install
|
|
101
|
+
npm test # every quote in the catalog appears word for word in reference/
|
|
102
|
+
npm run build # bundles dist/allowfy-check.mjs
|
|
103
|
+
node src/index.ts check https://example.com
|
|
104
|
+
```
|
|
28
105
|
|
|
29
|
-
|
|
106
|
+
Node 22.18 or later runs the TypeScript sources directly. The checks live in `src/`:
|
|
107
|
+
- `catalog.ts`: ids, levels and quotes;
|
|
108
|
+
- `sign-in.ts`;
|
|
109
|
+
- `operations.ts`.
|
|
30
110
|
|
|
31
|
-
|
|
32
|
-
- Direct Sign-In (4.5) and Mediated Sign-In (4.7).
|
|
33
|
-
- Operations through MCP and conversations (4.2, 4.3).
|
|
34
|
-
- Checking that every change is described in `result.summary` beyond it being present.
|
|
111
|
+
Issues and pull requests are welcome.
|
|
35
112
|
|
|
36
|
-
|
|
37
|
-
The code is written to move into the official kit:
|
|
38
|
-
- the entries in `catalog.ts` go into its catalog;
|
|
39
|
-
- `sign-in.ts` and `operations.ts` go into `checks/`;
|
|
40
|
-
- `run.ts` is wired into its `runChecks`.
|
|
113
|
+
---
|
|
41
114
|
|
|
42
|
-
|
|
115
|
+
Made by **[Allowfy](https://allowfy.com)**, the door between AI agents and your business: your rules decide what agents may do, customers approve, risky actions wait for your team, and every step is on a signed record. Allowfy passes every check that applies to it.
|
package/allowfy-check.mjs
CHANGED
|
@@ -143,6 +143,10 @@ async function getFollowing(http, url, options = {}) {
|
|
|
143
143
|
return { chain, final: chain[chain.length - 1], problem: "too many redirects" };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// tools/allowfy-check/index.ts
|
|
147
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
148
|
+
import { join as join3 } from "node:path";
|
|
149
|
+
|
|
146
150
|
// test/vendor/poppy-kit/validate/identity.ts
|
|
147
151
|
import {
|
|
148
152
|
existsSync,
|
|
@@ -471,6 +475,9 @@ function ath(accessToken) {
|
|
|
471
475
|
function publicPart(jwk) {
|
|
472
476
|
return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
|
|
473
477
|
}
|
|
478
|
+
function thumbprint(jwk) {
|
|
479
|
+
return sha256(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
|
|
480
|
+
}
|
|
474
481
|
async function generateKey(extractable = false) {
|
|
475
482
|
const pair = await crypto.subtle.generateKey(ALGORITHM, extractable, [
|
|
476
483
|
"sign",
|
|
@@ -551,6 +558,101 @@ var POPPY_DIR = ".poppy";
|
|
|
551
558
|
function poppyDir(cwd = process.cwd()) {
|
|
552
559
|
return join2(cwd, POPPY_DIR);
|
|
553
560
|
}
|
|
561
|
+
function normalizeBaseUrl(input) {
|
|
562
|
+
let url;
|
|
563
|
+
try {
|
|
564
|
+
url = new URL(input);
|
|
565
|
+
} catch {
|
|
566
|
+
throw new UsageError(
|
|
567
|
+
`--base-url must be a full URL, such as https://staging.example.com/poppy-test (got "${input}").`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (url.protocol !== "https:") {
|
|
571
|
+
throw new UsageError(
|
|
572
|
+
"--base-url must be an HTTPS URL. Companies fetch agent.json and jwks.json over HTTPS."
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (url.search || url.hash) throw new UsageError("--base-url can't have a query or fragment.");
|
|
576
|
+
return url.toString().replace(/\/+$/, "");
|
|
577
|
+
}
|
|
578
|
+
var UsageError = class extends Error {
|
|
579
|
+
};
|
|
580
|
+
async function keygen(baseUrlInput, options = {}) {
|
|
581
|
+
const cwd = options.cwd ?? process.cwd();
|
|
582
|
+
const dir = poppyDir(cwd);
|
|
583
|
+
const baseUrl = normalizeBaseUrl(baseUrlInput);
|
|
584
|
+
const keyFile = join2(dir, "private-key.jwk");
|
|
585
|
+
if (existsSync(keyFile) && !options.force) {
|
|
586
|
+
throw new UsageError(
|
|
587
|
+
`${keyFile} already exists. Run keygen with --force to replace it; you'll then need to host the new agent.json and jwks.json.`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
mkdirSync(dir, { recursive: true });
|
|
591
|
+
const generated = await generateKey(true);
|
|
592
|
+
const kid = (await thumbprint(generated.publicJwk)).slice(0, 16);
|
|
593
|
+
const privateJwk = { ...generated.privateJwk, kid, alg: "ES256", use: "sig" };
|
|
594
|
+
const publicJwk = { ...generated.publicJwk, kid, alg: "ES256", use: "sig" };
|
|
595
|
+
const clientId = `${baseUrl}/agent.json`;
|
|
596
|
+
const agent = {
|
|
597
|
+
client_id: clientId,
|
|
598
|
+
client_name: "Poppy validator (test)",
|
|
599
|
+
jwks_uri: `${baseUrl}/jwks.json`,
|
|
600
|
+
redirect_uris: [`${baseUrl}/callback`],
|
|
601
|
+
token_endpoint_auth_method: "private_key_jwt"
|
|
602
|
+
};
|
|
603
|
+
const jwks = { keys: [publicJwk] };
|
|
604
|
+
writeFileSync(keyFile, JSON.stringify(privateJwk, null, 2) + "\n", { mode: 384 });
|
|
605
|
+
chmodSync(keyFile, 384);
|
|
606
|
+
writeFileSync(join2(dir, "agent.json"), JSON.stringify(agent, null, 2) + "\n");
|
|
607
|
+
writeFileSync(join2(dir, "jwks.json"), JSON.stringify(jwks, null, 2) + "\n");
|
|
608
|
+
const configFile = join2(dir, "config.json");
|
|
609
|
+
let userIds = {};
|
|
610
|
+
if (existsSync(configFile)) {
|
|
611
|
+
try {
|
|
612
|
+
const old = JSON.parse(readFileSync2(configFile, "utf8"));
|
|
613
|
+
if (old.client_id === clientId) userIds = old.user_ids ?? {};
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const config = { client_id: clientId, base_url: baseUrl, user_ids: userIds };
|
|
618
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
|
|
619
|
+
return {
|
|
620
|
+
dir,
|
|
621
|
+
clientId,
|
|
622
|
+
files: [
|
|
623
|
+
{ local: join2(dir, "agent.json"), url: clientId },
|
|
624
|
+
{ local: join2(dir, "jwks.json"), url: agent.jwks_uri }
|
|
625
|
+
],
|
|
626
|
+
gitignore: ensureGitignored(cwd)
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function ensureGitignored(cwd) {
|
|
630
|
+
let root;
|
|
631
|
+
for (let d = resolve(cwd); ; d = dirname(d)) {
|
|
632
|
+
if (existsSync(join2(d, ".git"))) {
|
|
633
|
+
root = d;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
if (dirname(d) === d) break;
|
|
637
|
+
}
|
|
638
|
+
if (!root) return void 0;
|
|
639
|
+
const ignored = /^\s*\/?\.poppy\/?\s*$/m;
|
|
640
|
+
for (let d = resolve(cwd); ; d = dirname(d)) {
|
|
641
|
+
const file2 = join2(d, ".gitignore");
|
|
642
|
+
if (existsSync(file2) && ignored.test(readFileSync2(file2, "utf8"))) return void 0;
|
|
643
|
+
if (d === root || dirname(d) === d) break;
|
|
644
|
+
}
|
|
645
|
+
const file = join2(cwd, ".gitignore");
|
|
646
|
+
const existing = existsSync(file) ? readFileSync2(file, "utf8") : "";
|
|
647
|
+
const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
648
|
+
appendFileSync(
|
|
649
|
+
file,
|
|
650
|
+
`${prefix}# Poppy validator test identity and results (private key)
|
|
651
|
+
.poppy/
|
|
652
|
+
`
|
|
653
|
+
);
|
|
654
|
+
return file;
|
|
655
|
+
}
|
|
554
656
|
async function loadIdentity(cwd = process.cwd()) {
|
|
555
657
|
const dir = poppyDir(cwd);
|
|
556
658
|
const keyFile = join2(dir, "private-key.jwk");
|
|
@@ -4316,10 +4418,15 @@ async function main(argv) {
|
|
|
4316
4418
|
"other-account": { type: "boolean" },
|
|
4317
4419
|
milestone: { type: "string" },
|
|
4318
4420
|
only: { type: "string", multiple: true },
|
|
4319
|
-
json: { type: "boolean" }
|
|
4421
|
+
json: { type: "boolean" },
|
|
4422
|
+
"base-url": { type: "string" },
|
|
4423
|
+
force: { type: "boolean" }
|
|
4320
4424
|
}
|
|
4321
4425
|
});
|
|
4322
|
-
if (positionals[0]
|
|
4426
|
+
if (positionals[0] === "keygen") return keygenCommand(values["base-url"], !!values.force);
|
|
4427
|
+
if (positionals[0] !== "check" || !positionals[1]) {
|
|
4428
|
+
fail("Usage:\n allowfy-check check <https://company-domain> [options]\n allowfy-check keygen --base-url <https://where-you-host-agent.json>");
|
|
4429
|
+
}
|
|
4323
4430
|
const target = new URL(positionals[1]).origin;
|
|
4324
4431
|
const milestones = parseMilestones(values.milestone ?? "all");
|
|
4325
4432
|
if (!milestones) fail("--milestone must be foundation, interfaces, sign-in, extensions, all, or 1-4.");
|
|
@@ -4352,6 +4459,29 @@ async function main(argv) {
|
|
|
4352
4459
|
}
|
|
4353
4460
|
return summarize(results).total.fail > 0 ? 1 : 0;
|
|
4354
4461
|
}
|
|
4462
|
+
async function keygenCommand(baseUrl, force) {
|
|
4463
|
+
if (!baseUrl) fail("keygen needs --base-url, the HTTPS URL where you'll host agent.json and jwks.json.");
|
|
4464
|
+
const result = await keygen(baseUrl, { force });
|
|
4465
|
+
const agentFile = join3(result.dir, "agent.json");
|
|
4466
|
+
const agent = JSON.parse(readFileSync3(agentFile, "utf8"));
|
|
4467
|
+
writeFileSync2(agentFile, JSON.stringify({ ...agent, extensions: { operations: { version: "1" } } }, null, 2) + "\n");
|
|
4468
|
+
console.log(
|
|
4469
|
+
[
|
|
4470
|
+
`Created a test Personal Agent in ${result.dir}.`,
|
|
4471
|
+
"",
|
|
4472
|
+
"Host these two files, unchanged, at exactly these HTTPS URLs (no redirects):",
|
|
4473
|
+
...result.files.map((f) => ` ${f.local}
|
|
4474
|
+
\u2192 ${f.url}`),
|
|
4475
|
+
"",
|
|
4476
|
+
`client_id: ${result.clientId}`,
|
|
4477
|
+
"The private key stays in .poppy/private-key.jwk. Never host or commit it.",
|
|
4478
|
+
result.gitignore ? `Added .poppy/ to ${result.gitignore}.` : "",
|
|
4479
|
+
"Its agent.json lists the operations extension, so the operations checks can run.",
|
|
4480
|
+
"This identity is for testing only. Don't add it to a production allowlist."
|
|
4481
|
+
].filter((l, i, a) => l !== "" || a[i - 1] !== "").join("\n")
|
|
4482
|
+
);
|
|
4483
|
+
return 0;
|
|
4484
|
+
}
|
|
4355
4485
|
main(process.argv.slice(2)).then(
|
|
4356
4486
|
(code) => process.exitCode = code,
|
|
4357
4487
|
(error) => {
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "allowfy-check",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Is your site ready for AI agents? Test it against Poppy (the Personal Agent Protocol): 111 checks for poppy.json, OAuth & DPoP, sign-in and operations.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"personal-agent-protocol",
|
|
7
6
|
"poppy",
|
|
7
|
+
"poppy-json",
|
|
8
|
+
"personal-agent-protocol",
|
|
8
9
|
"ai-agents",
|
|
10
|
+
"agentic-commerce",
|
|
11
|
+
"agent-interoperability",
|
|
9
12
|
"conformance",
|
|
13
|
+
"validator",
|
|
10
14
|
"oauth",
|
|
11
15
|
"dpop",
|
|
16
|
+
"cli",
|
|
12
17
|
"allowfy"
|
|
13
18
|
],
|
|
14
19
|
"homepage": "https://check.allowfy.com",
|