@affix-io/cli 1.2.4
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 +48 -0
- package/certs/sectigo-r36.pem +36 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +49 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +93 -0
- package/dist/codes.d.ts +31 -0
- package/dist/codes.js +321 -0
- package/dist/commands-core.d.ts +11 -0
- package/dist/commands-core.js +55 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +36 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +78 -0
- package/dist/generate-local.d.ts +5 -0
- package/dist/generate-local.js +47 -0
- package/dist/http.d.ts +11 -0
- package/dist/http.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +267 -0
- package/dist/repl.d.ts +4 -0
- package/dist/repl.js +347 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +116 -0
- package/dist/site-api.d.ts +8 -0
- package/dist/site-api.js +57 -0
- package/dist/store.d.ts +43 -0
- package/dist/store.js +96 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# AffixIO CLI
|
|
2
|
+
|
|
3
|
+
Terminal client for AffixIO verification API with hub browser login.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
curl -fsSL https://www.affix-io.com/cli/install.sh | bash
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
npm (when published):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @affix-io/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
affix login # hub browser activation
|
|
21
|
+
affix # interactive mode
|
|
22
|
+
|
|
23
|
+
affix qr <text|url> # generate QR (works offline; Merkle needs login)
|
|
24
|
+
affix barcode <data> # generate barcode (works offline)
|
|
25
|
+
affix codes # list local vault
|
|
26
|
+
affix open <id> # open stored file
|
|
27
|
+
affix rm <id> # remove from vault
|
|
28
|
+
|
|
29
|
+
affix health # requires login or AFFIX_API_KEY
|
|
30
|
+
affix circuits
|
|
31
|
+
affix prove --circuit yesno
|
|
32
|
+
affix verify <proof> --circuit yesno
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Set `AFFIX_API_KEY` (and optional `AFFIX_API_BASE`) to use API commands without browser login.
|
|
36
|
+
|
|
37
|
+
Credentials: `~/.affix/credentials.json`
|
|
38
|
+
Code vault: `~/.affix/codes/`
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install
|
|
44
|
+
npm run build
|
|
45
|
+
npm run test:smoke
|
|
46
|
+
AFFIX_API_KEY=… npm run test:smoke
|
|
47
|
+
npm run pack:dist
|
|
48
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIGTDCCBDSgAwIBAgIQOXpmzCdWNi4NqofKbqvjsTANBgkqhkiG9w0BAQwFADBf
|
|
3
|
+
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
|
|
4
|
+
Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
|
|
5
|
+
HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBgMQswCQYDVQQGEwJHQjEY
|
|
6
|
+
MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFB1Ymxp
|
|
7
|
+
YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gQ0EgRFYgUjM2MIIBojANBgkqhkiG9w0B
|
|
8
|
+
AQEFAAOCAY8AMIIBigKCAYEAljZf2HIz7+SPUPQCQObZYcrxLTHYdf1ZtMRe7Yeq
|
|
9
|
+
RPSwygz16qJ9cAWtWNTcuICc++p8Dct7zNGxCpqmEtqifO7NvuB5dEVexXn9RFFH
|
|
10
|
+
12Hm+NtPRQgXIFjx6MSJcNWuVO3XGE57L1mHlcQYj+g4hny90aFh2SCZCDEVkAja
|
|
11
|
+
EMMfYPKuCjHuuF+bzHFb/9gV8P9+ekcHENF2nR1efGWSKwnfG5RawlkaQDpRtZTm
|
|
12
|
+
M64TIsv/r7cyFO4nSjs1jLdXYdz5q3a4L0NoabZfbdxVb+CUEHfB0bpulZQtH1Rv
|
|
13
|
+
38e/lIdP7OTTIlZh6OYL6NhxP8So0/sht/4J9mqIGxRFc0/pC8suja+wcIUna0HB
|
|
14
|
+
pXKfXTKpzgis+zmXDL06ASJf5E4A2/m+Hp6b84sfPAwQ766rI65mh50S0Di9E3Pn
|
|
15
|
+
2WcaJc+PILsBmYpgtmgWTR9eV9otfKRUBfzHUHcVgarub/XluEpRlTtZudU5xbFN
|
|
16
|
+
xx/DgMrXLUAPaI60fZ6wA+PTAgMBAAGjggGBMIIBfTAfBgNVHSMEGDAWgBRWc1hk
|
|
17
|
+
lfmSGrASKgRieaFAFYghSTAdBgNVHQ4EFgQUaMASFhgOr872h6YyV6NGUV3LBycw
|
|
18
|
+
DgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYI
|
|
19
|
+
KwYBBQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEw
|
|
20
|
+
VAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdv
|
|
21
|
+
UHVibGljU2VydmVyQXV0aGVudGljYXRpb25Sb290UjQ2LmNybDCBhAYIKwYBBQUH
|
|
22
|
+
AQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3Rp
|
|
23
|
+
Z29QdWJsaWNTZXJ2ZXJBdXRoZW50aWNhdGlvblJvb3RSNDYucDdjMCMGCCsGAQUF
|
|
24
|
+
BzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEA
|
|
25
|
+
YtOC9Fy+TqECFw40IospI92kLGgoSZGPOSQXMBqmsGWZUQ7rux7cj1du6d9rD6C8
|
|
26
|
+
ze1B2eQjkrGkIL/OF1s7vSmgYVafsRoZd/IHUrkoQvX8FZwUsmPu7amgBfaY3g+d
|
|
27
|
+
q1x0jNGKb6I6Bzdl6LgMD9qxp+3i7GQOnd9J8LFSietY6Z4jUBzVoOoz8iAU84OF
|
|
28
|
+
h2HhAuiPw1ai0VnY38RTI+8kepGWVfGxfBWzwH9uIjeooIeaosVFvE8cmYUB4TSH
|
|
29
|
+
5dUyD0jHct2+8ceKEtIoFU/FfHq/mDaVnvcDCZXtIgitdMFQdMZaVehmObyhRdDD
|
|
30
|
+
4NQCs0gaI9AAgFj4L9QtkARzhQLNyRf87Kln+YU0lgCGr9HLg3rGO8q+Y4ppLsOd
|
|
31
|
+
unQZ6ZxPNGIfOApbPVf5hCe58EZwiWdHIMn9lPP6+F404y8NNugbQixBber+x536
|
|
32
|
+
WrZhFZLjEkhp7fFXf9r32rNPfb74X/U90Bdy4lzp3+X1ukh1BuMxA/EEhDoTOS3l
|
|
33
|
+
7ABvc7BYSQubQ2490OcdkIzUh3ZwDrakMVrbaTxUM2p24N6dB+ns2zptWCva6jzW
|
|
34
|
+
r8IWKIMxzxLPv5Kt3ePKcUdvkBU/smqujSczTzzSjIoR5QqQA6lN1ZRSnuHIWCvh
|
|
35
|
+
JEltkYnTAH41QJ6SAWO66GrrUESwN/cgZzL4JLEqz1Y=
|
|
36
|
+
-----END CERTIFICATE-----
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AffixCredentials } from "./config.js";
|
|
2
|
+
export declare class AffixApi {
|
|
3
|
+
private readonly creds;
|
|
4
|
+
constructor(creds: AffixCredentials);
|
|
5
|
+
private request;
|
|
6
|
+
health(): Promise<unknown>;
|
|
7
|
+
circuits(): Promise<unknown>;
|
|
8
|
+
authCheck(): Promise<unknown>;
|
|
9
|
+
prove(circuitId: string, body: Record<string, unknown>): Promise<unknown>;
|
|
10
|
+
verify(circuitId: string, proof: string): Promise<unknown>;
|
|
11
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { requestJson } from "./http.js";
|
|
2
|
+
export class AffixApi {
|
|
3
|
+
creds;
|
|
4
|
+
constructor(creds) {
|
|
5
|
+
this.creds = creds;
|
|
6
|
+
}
|
|
7
|
+
async request(path, init) {
|
|
8
|
+
const base = this.creds.api_base.replace(/\/$/, "");
|
|
9
|
+
const res = await requestJson(`${base}${path}`, {
|
|
10
|
+
method: init?.method ?? "GET",
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${this.creds.api_key}`,
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
Accept: "application/json",
|
|
15
|
+
},
|
|
16
|
+
body: init?.body,
|
|
17
|
+
timeoutMs: 120_000,
|
|
18
|
+
});
|
|
19
|
+
const data = await res.json();
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const msg = data.message ??
|
|
22
|
+
data.error ??
|
|
23
|
+
`API error ${res.status}`;
|
|
24
|
+
throw new Error(msg);
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
health() {
|
|
29
|
+
return this.request("/api/health");
|
|
30
|
+
}
|
|
31
|
+
circuits() {
|
|
32
|
+
return this.request("/v1/circuits");
|
|
33
|
+
}
|
|
34
|
+
authCheck() {
|
|
35
|
+
return this.request("/v1/auth/check");
|
|
36
|
+
}
|
|
37
|
+
prove(circuitId, body) {
|
|
38
|
+
return this.request(`/v1/circuits/${encodeURIComponent(circuitId)}/prove`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
verify(circuitId, proof) {
|
|
44
|
+
return this.request(`/v1/circuits/${encodeURIComponent(circuitId)}/verify`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: JSON.stringify({ proof }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AffixCredentials } from "./config.js";
|
|
2
|
+
export interface DeviceAuthStart {
|
|
3
|
+
device_code: string;
|
|
4
|
+
user_code: string;
|
|
5
|
+
verification_uri: string;
|
|
6
|
+
expires_in: number;
|
|
7
|
+
interval: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DeviceTokenResponse {
|
|
10
|
+
status: "pending" | "approved" | "expired" | "denied";
|
|
11
|
+
api_key?: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function startDeviceAuth(hubBase?: string): Promise<DeviceAuthStart>;
|
|
16
|
+
export declare function pollDeviceToken(hubBase: string, deviceCode: string): Promise<DeviceTokenResponse>;
|
|
17
|
+
export declare function loginViaHub(hubBase?: string): Promise<AffixCredentials>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { hostname, platform } from "node:os";
|
|
3
|
+
import { DEFAULT_API_BASE, DEFAULT_HUB_BASE, saveCredentials, } from "./config.js";
|
|
4
|
+
import { requestJson } from "./http.js";
|
|
5
|
+
function cliApi(siteBase, path) {
|
|
6
|
+
const base = siteBase.replace(/\/$/, "");
|
|
7
|
+
return `${base}/cli/api/cli${path}`;
|
|
8
|
+
}
|
|
9
|
+
export async function startDeviceAuth(hubBase = DEFAULT_HUB_BASE) {
|
|
10
|
+
const res = await requestJson(cliApi(hubBase, "/device"), {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
client_name: "affix-cli",
|
|
15
|
+
hostname: hostname(),
|
|
16
|
+
platform: platform(),
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
const err = await res.json();
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
throw new Error(err.message ?? `Device auth failed (${res.status})`);
|
|
22
|
+
}
|
|
23
|
+
return err;
|
|
24
|
+
}
|
|
25
|
+
export async function pollDeviceToken(hubBase, deviceCode) {
|
|
26
|
+
const res = await requestJson(cliApi(hubBase, "/token"), {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
29
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
30
|
+
});
|
|
31
|
+
const err = await res.json();
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(err.message ?? `Token poll failed (${res.status})`);
|
|
34
|
+
}
|
|
35
|
+
return err;
|
|
36
|
+
}
|
|
37
|
+
function openBrowser(url) {
|
|
38
|
+
const cmds = {
|
|
39
|
+
darwin: `open "${url}"`,
|
|
40
|
+
win32: `start "" "${url}"`,
|
|
41
|
+
linux: `xdg-open "${url}"`,
|
|
42
|
+
};
|
|
43
|
+
const cmd = cmds[platform()];
|
|
44
|
+
if (!cmd)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
execSync(cmd, { stdio: "ignore" });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* browser open is best-effort */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function loginViaHub(hubBase = DEFAULT_HUB_BASE) {
|
|
54
|
+
const device = await startDeviceAuth(hubBase);
|
|
55
|
+
const uri = device.verification_uri;
|
|
56
|
+
process.stderr.write("\n");
|
|
57
|
+
process.stderr.write(" AffixIO CLI activation\n");
|
|
58
|
+
process.stderr.write(" ──────────────────────\n\n");
|
|
59
|
+
process.stderr.write(` Open ${uri}\n`);
|
|
60
|
+
process.stderr.write(` Enter code: ${device.user_code}\n\n`);
|
|
61
|
+
process.stderr.write(" Waiting for sign-in at affix-io.com/cli/verify…\n");
|
|
62
|
+
openBrowser(uri);
|
|
63
|
+
const deadline = Date.now() + device.expires_in * 1000;
|
|
64
|
+
const intervalMs = Math.max(device.interval, 2) * 1000;
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
await sleep(intervalMs);
|
|
67
|
+
const token = await pollDeviceToken(hubBase, device.device_code);
|
|
68
|
+
if (token.status === "approved" && token.api_key && token.email) {
|
|
69
|
+
const creds = {
|
|
70
|
+
api_key: token.api_key,
|
|
71
|
+
email: token.email,
|
|
72
|
+
api_base: DEFAULT_API_BASE,
|
|
73
|
+
hub_base: hubBase,
|
|
74
|
+
activated_at: new Date().toISOString(),
|
|
75
|
+
client_name: "affix-cli",
|
|
76
|
+
};
|
|
77
|
+
saveCredentials(creds);
|
|
78
|
+
process.stderr.write(`\n Activated as ${token.email}\n\n`);
|
|
79
|
+
return creds;
|
|
80
|
+
}
|
|
81
|
+
if (token.status === "expired" || token.status === "denied") {
|
|
82
|
+
const msg = token.message ?? `Activation ${token.status}`;
|
|
83
|
+
if (msg.includes("not found") || msg.includes("Unknown device")) {
|
|
84
|
+
throw new Error("Device session expired. Run affix login again and approve promptly.");
|
|
85
|
+
}
|
|
86
|
+
throw new Error(msg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw new Error("Activation timed out. Run affix login and try again.");
|
|
90
|
+
}
|
|
91
|
+
function sleep(ms) {
|
|
92
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
}
|
package/dist/codes.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AffixCredentials } from "./config.js";
|
|
2
|
+
import { type CodeFormat, type StoredCode } from "./store.js";
|
|
3
|
+
export interface GenerateOptions {
|
|
4
|
+
label?: string;
|
|
5
|
+
format?: CodeFormat;
|
|
6
|
+
symbology?: string;
|
|
7
|
+
both?: boolean;
|
|
8
|
+
maxUses?: number;
|
|
9
|
+
location?: string;
|
|
10
|
+
out?: string;
|
|
11
|
+
event?: string;
|
|
12
|
+
holder?: string;
|
|
13
|
+
seat?: string;
|
|
14
|
+
tier?: string;
|
|
15
|
+
/** Use Affix short redirect link (/q/…) instead of encoding the URL directly */
|
|
16
|
+
shortLink?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function generateQr(creds: AffixCredentials, content: string, opts?: GenerateOptions): Promise<StoredCode>;
|
|
19
|
+
export declare function generateBarcode(creds: AffixCredentials, content: string, opts?: GenerateOptions): Promise<StoredCode>;
|
|
20
|
+
export declare function generateTicketCode(creds: AffixCredentials, fields: Record<string, unknown>, opts?: GenerateOptions): Promise<StoredCode>;
|
|
21
|
+
export declare function scanCode(creds: AffixCredentials, target: string, opts?: {
|
|
22
|
+
event?: string;
|
|
23
|
+
entryPoint?: string;
|
|
24
|
+
id?: string;
|
|
25
|
+
}): Promise<Record<string, unknown>>;
|
|
26
|
+
export declare function printCodeEntry(entry: StoredCode): void;
|
|
27
|
+
export declare function printCodesList(): void;
|
|
28
|
+
export declare function showCode(id: string): void;
|
|
29
|
+
export declare function removeCode(id: string): void;
|
|
30
|
+
export declare function openCode(id: string): void;
|
|
31
|
+
export declare function listSymbologies(creds: AffixCredentials): Promise<string[]>;
|
package/dist/codes.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { fetchBarcodeSymbologies, generateBarcodeMerkle, generateQrMerkle, performCliScan, } from "./site-api.js";
|
|
4
|
+
import { DEFAULT_SYMBOLOGIES, renderBarcodePng, renderBarcodeSvg, renderQrPng, renderQrSvg, } from "./generate-local.js";
|
|
5
|
+
import { deleteStoredCode, getStoredCode, listStoredCodes, newCodeId, saveStoredCode, updateScanCounts, vaultSummary, writeCodeFile, } from "./store.js";
|
|
6
|
+
function defaultLabel(content) {
|
|
7
|
+
const trimmed = content.trim();
|
|
8
|
+
if (trimmed.length <= 48)
|
|
9
|
+
return trimmed;
|
|
10
|
+
return trimmed.slice(0, 45) + "…";
|
|
11
|
+
}
|
|
12
|
+
function requireAuth(creds) {
|
|
13
|
+
if (!creds.api_key) {
|
|
14
|
+
throw new Error("Sign in required for Merkle-backed codes. Run: affix login");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function parseMaxUses(opts) {
|
|
18
|
+
if (opts.maxUses == null)
|
|
19
|
+
return undefined;
|
|
20
|
+
const n = Number(opts.maxUses);
|
|
21
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
22
|
+
throw new Error("Scans must be a positive integer.");
|
|
23
|
+
}
|
|
24
|
+
return Math.floor(n);
|
|
25
|
+
}
|
|
26
|
+
function extractScansFromPayload(data) {
|
|
27
|
+
const payload = data.payload;
|
|
28
|
+
const controls = payload?.controls;
|
|
29
|
+
const link = data.link;
|
|
30
|
+
const linkControls = link?.meta?.controls;
|
|
31
|
+
const total = linkControls?.maxUses ??
|
|
32
|
+
controls?.maxUses ??
|
|
33
|
+
payload?.maxUses ??
|
|
34
|
+
null;
|
|
35
|
+
const remaining = payload?.usesRemaining ??
|
|
36
|
+
(total != null ? total : null);
|
|
37
|
+
return { total, remaining };
|
|
38
|
+
}
|
|
39
|
+
function merkleMeta(data) {
|
|
40
|
+
return {
|
|
41
|
+
source: "merkle",
|
|
42
|
+
mode: data.mode,
|
|
43
|
+
proofDigest: data.proofDigest,
|
|
44
|
+
merkle_leaf_hash: data.merkle_leaf_hash,
|
|
45
|
+
merkle_root: data.merkle_root,
|
|
46
|
+
merkle_proof: data.merkle_proof,
|
|
47
|
+
merkle: data.merkle,
|
|
48
|
+
token: data.token,
|
|
49
|
+
link: data.link,
|
|
50
|
+
url: data.url,
|
|
51
|
+
payload: data.payload,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function persistFromApi(creds, kind, data, opts, content, symbology) {
|
|
55
|
+
const id = newCodeId();
|
|
56
|
+
const asset = kind === "qr"
|
|
57
|
+
? data.qr
|
|
58
|
+
: data.barcode;
|
|
59
|
+
if (!asset?.svg) {
|
|
60
|
+
throw new Error("API did not return SVG output.");
|
|
61
|
+
}
|
|
62
|
+
const writeOpts = { location: opts.location, out: opts.out };
|
|
63
|
+
const svgWrite = writeCodeFile(id, "svg", asset.svg, writeOpts);
|
|
64
|
+
const files = { svg: svgWrite.path };
|
|
65
|
+
const wantPng = opts.both || opts.format === "png";
|
|
66
|
+
if (wantPng) {
|
|
67
|
+
const pngBody = kind === "qr"
|
|
68
|
+
? await renderQrPng(content)
|
|
69
|
+
: await renderBarcodePng(symbology ?? "code128", content);
|
|
70
|
+
files.png = writeCodeFile(id, "png", pngBody, writeOpts).path;
|
|
71
|
+
}
|
|
72
|
+
const scans = extractScansFromPayload(data);
|
|
73
|
+
const entry = {
|
|
74
|
+
id,
|
|
75
|
+
kind,
|
|
76
|
+
label: opts.label || defaultLabel(content),
|
|
77
|
+
content,
|
|
78
|
+
symbology,
|
|
79
|
+
format: "svg",
|
|
80
|
+
created_at: new Date().toISOString(),
|
|
81
|
+
output_location: svgWrite.locationName,
|
|
82
|
+
output_path: svgWrite.locationPath,
|
|
83
|
+
scans_total: scans.total,
|
|
84
|
+
scans_remaining: scans.remaining,
|
|
85
|
+
files,
|
|
86
|
+
meta: merkleMeta(data),
|
|
87
|
+
};
|
|
88
|
+
return saveStoredCode(entry);
|
|
89
|
+
}
|
|
90
|
+
function buildQrBody(content, opts) {
|
|
91
|
+
const maxUses = parseMaxUses(opts);
|
|
92
|
+
if (maxUses != null) {
|
|
93
|
+
if (!opts.event) {
|
|
94
|
+
throw new Error("Scans require ticket mode. Use --event <slug> or: affix qr ticket --event <slug> --scans N");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
event: opts.event,
|
|
98
|
+
holder: opts.holder,
|
|
99
|
+
seat: opts.seat,
|
|
100
|
+
tier: opts.tier,
|
|
101
|
+
maxUses,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (/^https?:\/\//i.test(content)) {
|
|
105
|
+
if (opts.shortLink) {
|
|
106
|
+
return { url: content };
|
|
107
|
+
}
|
|
108
|
+
return { data: content };
|
|
109
|
+
}
|
|
110
|
+
return { data: content };
|
|
111
|
+
}
|
|
112
|
+
function buildBarcodeBody(content, opts) {
|
|
113
|
+
const symbology = opts.symbology ?? "code128";
|
|
114
|
+
const maxUses = parseMaxUses(opts);
|
|
115
|
+
if (maxUses != null) {
|
|
116
|
+
if (!opts.event) {
|
|
117
|
+
throw new Error("Scans require ticket mode. Use --event <slug> --scans N");
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
event: opts.event,
|
|
121
|
+
holder: opts.holder,
|
|
122
|
+
seat: opts.seat,
|
|
123
|
+
tier: opts.tier,
|
|
124
|
+
maxUses,
|
|
125
|
+
symbology,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return { data: content, symbology };
|
|
129
|
+
}
|
|
130
|
+
async function persistLocal(kind, content, opts, symbology) {
|
|
131
|
+
const id = newCodeId();
|
|
132
|
+
const writeOpts = { location: opts.location, out: opts.out };
|
|
133
|
+
const svg = kind === "qr"
|
|
134
|
+
? await renderQrSvg(content)
|
|
135
|
+
: await renderBarcodeSvg(symbology ?? "code128", content);
|
|
136
|
+
const svgWrite = writeCodeFile(id, "svg", svg, writeOpts);
|
|
137
|
+
const files = { svg: svgWrite.path };
|
|
138
|
+
const wantPng = opts.both || opts.format === "png";
|
|
139
|
+
if (wantPng) {
|
|
140
|
+
const pngBody = kind === "qr"
|
|
141
|
+
? await renderQrPng(content)
|
|
142
|
+
: await renderBarcodePng(symbology ?? "code128", content);
|
|
143
|
+
const pngWrite = writeCodeFile(id, "png", pngBody, writeOpts);
|
|
144
|
+
files.png = pngWrite.path;
|
|
145
|
+
}
|
|
146
|
+
const entry = {
|
|
147
|
+
id,
|
|
148
|
+
kind,
|
|
149
|
+
label: opts.label || defaultLabel(content),
|
|
150
|
+
content,
|
|
151
|
+
symbology: kind === "barcode" ? symbology ?? "code128" : undefined,
|
|
152
|
+
format: files.png ? "png" : "svg",
|
|
153
|
+
created_at: new Date().toISOString(),
|
|
154
|
+
output_location: svgWrite.locationName,
|
|
155
|
+
output_path: svgWrite.locationPath,
|
|
156
|
+
scans_total: null,
|
|
157
|
+
scans_remaining: null,
|
|
158
|
+
files,
|
|
159
|
+
meta: { source: "local", offline: true },
|
|
160
|
+
};
|
|
161
|
+
saveStoredCode(entry);
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
export async function generateQr(creds, content, opts = {}) {
|
|
165
|
+
const needsMerkle = Boolean(opts.maxUses || opts.shortLink || opts.event);
|
|
166
|
+
if (!creds.api_key) {
|
|
167
|
+
if (needsMerkle) {
|
|
168
|
+
requireAuth(creds);
|
|
169
|
+
}
|
|
170
|
+
return persistLocal("qr", content, opts);
|
|
171
|
+
}
|
|
172
|
+
const body = buildQrBody(content, opts);
|
|
173
|
+
const data = await generateQrMerkle(creds, body);
|
|
174
|
+
const resolvedContent = String(data.content ?? content);
|
|
175
|
+
return persistFromApi(creds, "qr", data, opts, resolvedContent);
|
|
176
|
+
}
|
|
177
|
+
export async function generateBarcode(creds, content, opts = {}) {
|
|
178
|
+
const symbology = opts.symbology ?? "code128";
|
|
179
|
+
const needsMerkle = Boolean(opts.maxUses || opts.event);
|
|
180
|
+
if (!creds.api_key) {
|
|
181
|
+
if (needsMerkle) {
|
|
182
|
+
requireAuth(creds);
|
|
183
|
+
}
|
|
184
|
+
return persistLocal("barcode", content, { ...opts, symbology }, symbology);
|
|
185
|
+
}
|
|
186
|
+
const body = buildBarcodeBody(content, opts);
|
|
187
|
+
const data = await generateBarcodeMerkle(creds, body);
|
|
188
|
+
const resolvedContent = String(data.content ?? content);
|
|
189
|
+
return persistFromApi(creds, "barcode", data, { ...opts, symbology }, resolvedContent, symbology);
|
|
190
|
+
}
|
|
191
|
+
export async function generateTicketCode(creds, fields, opts = {}) {
|
|
192
|
+
requireAuth(creds);
|
|
193
|
+
const body = { ...fields };
|
|
194
|
+
if (opts.maxUses != null)
|
|
195
|
+
body.maxUses = parseMaxUses(opts);
|
|
196
|
+
const data = await generateQrMerkle(creds, body);
|
|
197
|
+
const content = String(data.content ?? fields.event ?? "ticket");
|
|
198
|
+
return persistFromApi(creds, "qr", data, { ...opts, label: opts.label || `ticket:${String(fields.event ?? content)}` }, content);
|
|
199
|
+
}
|
|
200
|
+
export async function scanCode(creds, target, opts = {}) {
|
|
201
|
+
requireAuth(creds);
|
|
202
|
+
const entry = opts.id ? getStoredCode(opts.id) : null;
|
|
203
|
+
const meta = entry?.meta ?? {};
|
|
204
|
+
const link = meta.link;
|
|
205
|
+
const body = {
|
|
206
|
+
event: opts.event,
|
|
207
|
+
entryPoint: opts.entryPoint,
|
|
208
|
+
};
|
|
209
|
+
if (link?.code) {
|
|
210
|
+
body.code = link.code;
|
|
211
|
+
}
|
|
212
|
+
else if (meta.token) {
|
|
213
|
+
body.token = meta.token;
|
|
214
|
+
}
|
|
215
|
+
else if (/^[a-f0-9]{8,}$/i.test(target)) {
|
|
216
|
+
body.code = target;
|
|
217
|
+
}
|
|
218
|
+
else if (target.length >= 16) {
|
|
219
|
+
body.token = target;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
body.data = target;
|
|
223
|
+
}
|
|
224
|
+
const result = await performCliScan(creds, body);
|
|
225
|
+
if (entry) {
|
|
226
|
+
const remaining = result.usesRemaining ??
|
|
227
|
+
result.verification?.usesRemaining ??
|
|
228
|
+
null;
|
|
229
|
+
const total = result.maxUses ??
|
|
230
|
+
entry.scans_total ??
|
|
231
|
+
null;
|
|
232
|
+
updateScanCounts(entry.id, remaining, total);
|
|
233
|
+
entry.meta = {
|
|
234
|
+
...entry.meta,
|
|
235
|
+
last_scan: {
|
|
236
|
+
scan_id: result.scan_id,
|
|
237
|
+
admitted: result.admitted,
|
|
238
|
+
decision: result.decision,
|
|
239
|
+
merkle_leaf_hash: result.merkle_leaf_hash,
|
|
240
|
+
merkle_root: result.merkle_root,
|
|
241
|
+
at: new Date().toISOString(),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
saveStoredCode(entry);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
export function printCodeEntry(entry) {
|
|
249
|
+
console.log(JSON.stringify({
|
|
250
|
+
id: entry.id,
|
|
251
|
+
kind: entry.kind,
|
|
252
|
+
label: entry.label,
|
|
253
|
+
content: entry.content,
|
|
254
|
+
symbology: entry.symbology,
|
|
255
|
+
created_at: entry.created_at,
|
|
256
|
+
output_location: entry.output_location,
|
|
257
|
+
output_path: entry.output_path,
|
|
258
|
+
scans_total: entry.scans_total,
|
|
259
|
+
scans_remaining: entry.scans_remaining,
|
|
260
|
+
files: entry.files,
|
|
261
|
+
meta: entry.meta,
|
|
262
|
+
}, null, 2));
|
|
263
|
+
}
|
|
264
|
+
export function printCodesList() {
|
|
265
|
+
const summary = vaultSummary();
|
|
266
|
+
const items = listStoredCodes();
|
|
267
|
+
console.log(`Vault index: ${summary.dir} (${summary.total} items, ${summary.qr} QR, ${summary.barcode} barcodes)\n`);
|
|
268
|
+
if (!items.length) {
|
|
269
|
+
console.log("No codes stored yet. Try: affix qr <text> or affix barcode <data>");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (const item of items) {
|
|
273
|
+
const path = item.files.svg || item.files.png || "—";
|
|
274
|
+
const sym = item.symbology ? ` · ${item.symbology}` : "";
|
|
275
|
+
const loc = item.output_location ? ` · @${item.output_location}` : "";
|
|
276
|
+
const scans = item.scans_total != null
|
|
277
|
+
? ` · ${item.scans_remaining ?? "?"} / ${item.scans_total} scans`
|
|
278
|
+
: "";
|
|
279
|
+
console.log(`${item.id} ${item.kind}${sym}${loc}${scans} ${item.label}`);
|
|
280
|
+
console.log(` ${path}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export function showCode(id) {
|
|
284
|
+
const entry = getStoredCode(id);
|
|
285
|
+
if (!entry) {
|
|
286
|
+
throw new Error(`No code matching "${id}" in local vault.`);
|
|
287
|
+
}
|
|
288
|
+
printCodeEntry(entry);
|
|
289
|
+
}
|
|
290
|
+
export function removeCode(id) {
|
|
291
|
+
if (!deleteStoredCode(id)) {
|
|
292
|
+
throw new Error(`No code matching "${id}" in local vault.`);
|
|
293
|
+
}
|
|
294
|
+
console.log(`Removed ${id} from local vault.`);
|
|
295
|
+
}
|
|
296
|
+
export function openCode(id) {
|
|
297
|
+
const entry = getStoredCode(id);
|
|
298
|
+
if (!entry)
|
|
299
|
+
throw new Error(`No code matching "${id}" in local vault.`);
|
|
300
|
+
const filePath = entry.files.svg || entry.files.png;
|
|
301
|
+
if (!filePath)
|
|
302
|
+
throw new Error("No file on disk for this entry.");
|
|
303
|
+
const cmds = {
|
|
304
|
+
darwin: `open "${filePath}"`,
|
|
305
|
+
win32: `start "" "${filePath}"`,
|
|
306
|
+
linux: `xdg-open "${filePath}"`,
|
|
307
|
+
};
|
|
308
|
+
const cmd = cmds[platform()];
|
|
309
|
+
if (!cmd) {
|
|
310
|
+
console.log(filePath);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
execSync(cmd, { stdio: "ignore" });
|
|
314
|
+
console.log(`Opened ${filePath}`);
|
|
315
|
+
}
|
|
316
|
+
export async function listSymbologies(creds) {
|
|
317
|
+
if (!creds.api_key) {
|
|
318
|
+
return [...DEFAULT_SYMBOLOGIES];
|
|
319
|
+
}
|
|
320
|
+
return fetchBarcodeSymbologies(creds);
|
|
321
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AffixApi } from "./api.js";
|
|
2
|
+
import { type AffixCredentials } from "./config.js";
|
|
3
|
+
export declare function ensureAuth(force?: boolean): Promise<AffixCredentials>;
|
|
4
|
+
export declare function runLogin(): Promise<void>;
|
|
5
|
+
export declare function runLogout(): void;
|
|
6
|
+
export declare function runWhoami(creds: AffixCredentials): void;
|
|
7
|
+
export declare function runHealth(api: AffixApi): Promise<void>;
|
|
8
|
+
export declare function runCircuits(api: AffixApi): Promise<void>;
|
|
9
|
+
export declare function runAuthCheck(api: AffixApi): Promise<void>;
|
|
10
|
+
export declare function runProve(api: AffixApi, circuitId: string): Promise<void>;
|
|
11
|
+
export declare function runVerify(api: AffixApi, circuitId: string, proof: string): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { loginViaHub } from "./auth.js";
|
|
3
|
+
import { clearCredentials, credentialsPath, loadCredentials, } from "./config.js";
|
|
4
|
+
export async function ensureAuth(force = false) {
|
|
5
|
+
if (!force) {
|
|
6
|
+
const existing = loadCredentials();
|
|
7
|
+
if (existing)
|
|
8
|
+
return existing;
|
|
9
|
+
}
|
|
10
|
+
return loginViaHub();
|
|
11
|
+
}
|
|
12
|
+
export async function runLogin() {
|
|
13
|
+
await loginViaHub();
|
|
14
|
+
console.log("Signed in. Credentials stored at", credentialsPath());
|
|
15
|
+
}
|
|
16
|
+
export function runLogout() {
|
|
17
|
+
clearCredentials();
|
|
18
|
+
console.log("Signed out. Local credentials removed.");
|
|
19
|
+
}
|
|
20
|
+
export function runWhoami(creds) {
|
|
21
|
+
console.log(JSON.stringify({ email: creds.email, api_base: creds.api_base, activated_at: creds.activated_at }, null, 2));
|
|
22
|
+
}
|
|
23
|
+
export async function runHealth(api) {
|
|
24
|
+
console.log(JSON.stringify(await api.health(), null, 2));
|
|
25
|
+
}
|
|
26
|
+
export async function runCircuits(api) {
|
|
27
|
+
console.log(JSON.stringify(await api.circuits(), null, 2));
|
|
28
|
+
}
|
|
29
|
+
export async function runAuthCheck(api) {
|
|
30
|
+
console.log(JSON.stringify(await api.authCheck(), null, 2));
|
|
31
|
+
}
|
|
32
|
+
export async function runProve(api, circuitId) {
|
|
33
|
+
const now = Math.floor(Date.now() / 1000);
|
|
34
|
+
const field = () => "0x" + randomBytes(31).toString("hex");
|
|
35
|
+
const body = {
|
|
36
|
+
credential: {
|
|
37
|
+
schema_id: "cli_demo_v1",
|
|
38
|
+
issuer_id: "cli",
|
|
39
|
+
issuer_pubkey_hash: field(),
|
|
40
|
+
credential_id: field(),
|
|
41
|
+
claim_value: "local_resident",
|
|
42
|
+
valid_from: now - 86400,
|
|
43
|
+
valid_until: now + 31536000,
|
|
44
|
+
fields: { region: "cli" },
|
|
45
|
+
},
|
|
46
|
+
context: {
|
|
47
|
+
region_hash: "cli",
|
|
48
|
+
as_of_timestamp: now,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
console.log(JSON.stringify(await api.prove(circuitId, body), null, 2));
|
|
52
|
+
}
|
|
53
|
+
export async function runVerify(api, circuitId, proof) {
|
|
54
|
+
console.log(JSON.stringify(await api.verify(circuitId, proof), null, 2));
|
|
55
|
+
}
|