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.
Files changed (3) hide show
  1. package/README.md +102 -29
  2. package/allowfy-check.mjs +132 -2
  3. package/package.json +8 -3
package/README.md CHANGED
@@ -1,42 +1,115 @@
1
- # Allowfy Check: Personal Agent Protocol conformance, including sign-in and Operations
1
+ # Allowfy Check: the Poppy conformance tester
2
2
 
3
- The official validator (`test/vendor/poppy-kit`, draft 0.2) checks milestones 1 and 2. For milestone 3 and the extensions it says:
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
- > Milestone 3 (sign-in and accounts) checks aren't available in this version of the validator.
5
+ [![npm](https://img.shields.io/npm/v/allowfy-check)](https://www.npmjs.com/package/allowfy-check) [![license](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE) [![checks](https://img.shields.io/badge/checks-111-4f46e5)](#what-it-checks)
6
6
 
7
- This add-on checks them. It runs the official checks first, then 37 more:
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
- | Milestone | Checks |
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
- Each check follows the same rules as the official catalog: it has an id, a level (MUST fails, SHOULD warns), the section, and a quote copied word for word from the spec. `test/allowfy-check-catalog.test.ts` checks the quotes against `reference/`, which holds the spec text at upstream commit `39b35bd`. The results have the same shape and are printed with the official formatter.
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 check https://example.com --probe https://api.example.com/me \
19
- --propose 'POST https://api.example.com/orders/ord_1/exchanges {"item_id":"itm_1"}' \
20
- [--execute] [--decline] [--other-account] [--plain-identity ../second-agent] [--account-probe <GET url>] [--json]
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
- - **Test Personal Agent.** It uses the one in `.poppy/`, made with the validator's `keygen`. To check operations, its hosted `agent.json` must list `"extensions": { "operations": { "version": "1" } }`.
23
- - **Sign-in.** The validator prints the link and code, and you sign in in a browser.
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 sign in once with a second test account.
26
- - **Safety.** `--propose` only proposes and cancels, so nothing is performed. `--execute` confirms one proposal, so the Company performs the action. Use it only in a test environment.
27
- - **Agents without the extension.** `--plain-identity` names a second test agent whose `agent.json` doesn't list the extension. It checks that this agent gets no operations.
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
- In a test suite, pass a `SignInHelper` that drives your sign-in page (see `test/allowfy-full-conformance.test.ts`).
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
- ## Not yet
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
- ## Upstream
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
- Apache License 2.0, like the kit (`reference/LICENSE`).
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] !== "check" || !positionals[1]) fail("Usage: index.ts check <https://company-domain> [options]");
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.1",
4
- "description": "Check a business against the Personal Agent Protocol: discovery, Sessions, DPoP, APIs, sign-in, Account Tokens and the operations extension.",
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",