@maxceem/agw 0.1.4 → 0.1.6
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 +99 -31
- package/dist/agw.mjs +254 -47
- package/package.json +4 -4
- package/dist/release/0.1.4/console/.assetsignore +0 -1
- package/dist/release/0.1.4/console/assets/endpoints-BNTBSczQ.js +0 -12
- package/dist/release/0.1.4/console/assets/index-BGQQK35_.js +0 -98
- package/dist/release/0.1.4/console/assets/index-DUtKG6Od.css +0 -2
- package/dist/release/0.1.4/console/assets/usage-CzdJHSbn.js +0 -42
- package/dist/release/0.1.4/console/favicon.svg +0 -22
- package/dist/release/0.1.4/console/index.html +0 -34
- package/dist/release/0.1.4/manifest.json +0 -26
- package/dist/release/0.1.4/migrations/0000_initial.sql +0 -314
- package/dist/release/0.1.4/worker/index.js +0 -86803
- package/dist/release/0.1.4/wrangler.json +0 -82
- package/dist/release-integrity.json +0 -1
package/README.md
CHANGED
|
@@ -1,63 +1,131 @@
|
|
|
1
1
|
# App AI Gateway CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`agw` sets up and manages [App AI Gateway](https://github.com/maxceem/app-ai-gateway)
|
|
4
|
+
from a terminal or a coding agent: providers, apps, usage, and your own
|
|
5
|
+
deployment. Requires Node.js 22.19+.
|
|
4
6
|
|
|
5
7
|
```sh
|
|
6
|
-
|
|
7
|
-
npx @maxceem/agw app add --type ios --team-id ABCDE12345 --bundle-id com.example.app
|
|
8
|
+
npm install -g @maxceem/agw
|
|
8
9
|
```
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
## Quick start
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
```sh
|
|
14
|
+
# 1. Sign in. Create a management key in the console: user menu -> Management keys.
|
|
15
|
+
agw account login
|
|
16
|
+
|
|
17
|
+
# 2. Add a provider. Paste the API key into the hidden prompt.
|
|
18
|
+
agw provider add --type openai
|
|
19
|
+
|
|
20
|
+
# 3. Add an app. It uses every provider you have added.
|
|
21
|
+
agw app add --type ios --team-id ABCDE12345 --bundle-id com.example.app
|
|
22
|
+
|
|
23
|
+
# 4. Print the Swift code that calls the gateway from that app.
|
|
24
|
+
agw app snippet <app-id> --provider openai
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Self-hosted? Replace step 1 with a connect to your gateway. It asks for the
|
|
28
|
+
same kind of key:
|
|
13
29
|
|
|
14
30
|
```sh
|
|
15
|
-
agw
|
|
16
|
-
agw operation wait <operation-id> --timeout 300 --json
|
|
31
|
+
agw deployment connect --url https://ai.example.com
|
|
17
32
|
```
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
> **No account yet?** Skip step 1. The first `add` creates a free account on
|
|
35
|
+
> the hosted gateway for you. Attach a sign-in to it later with
|
|
36
|
+
> `agw account claim`. Help, status, lists, validation and dry runs never
|
|
37
|
+
> create an account.
|
|
38
|
+
|
|
39
|
+
## Self-host on Cloudflare
|
|
40
|
+
|
|
41
|
+
The package drives Wrangler and installs the gateway release of its own version.
|
|
42
|
+
No checkout needed. The gateway itself downloads once from the project's GitHub
|
|
43
|
+
release and is cached, after the CLI matches it against a digest built into the
|
|
44
|
+
executable.
|
|
20
45
|
|
|
21
|
-
|
|
46
|
+
```sh
|
|
47
|
+
agw deployment setup --name my-gateway --no-domain # deploy and connect
|
|
48
|
+
agw deployment update # move to this CLI's release
|
|
49
|
+
agw deployment domain --hostname ai.example.com # attach a custom domain
|
|
50
|
+
```
|
|
22
51
|
|
|
23
|
-
|
|
52
|
+
Setup signs in to Cloudflare through Wrangler, or uses `CLOUDFLARE_API_TOKEN`
|
|
53
|
+
if set. Add `--dry-run` to any of these to see what would happen. See the
|
|
54
|
+
[self-hosting guide](https://docs.appaigateway.com/self-hosting/deploy-to-cloudflare).
|
|
24
55
|
|
|
25
|
-
|
|
56
|
+
No route to GitHub from that machine? Fetch the `gateway-<version>.tar.gz` asset
|
|
57
|
+
elsewhere and pass `--release-archive <path>`.
|
|
26
58
|
|
|
27
|
-
##
|
|
59
|
+
## Commands
|
|
28
60
|
|
|
29
|
-
|
|
61
|
+
| Group | What it manages |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `provider` | Provider credentials: `add`, `list`, `show`, `update`, `rotate-key`, `remove` |
|
|
64
|
+
| `provider-gateway` | Cloudflare AI Gateway in front of a provider |
|
|
65
|
+
| `app` | Apps and their config: `add`, `list`, `show`, `update`, `validate`, `check`, `snippet`, `remove`, `key` |
|
|
66
|
+
| `usage` | Spend and requests: `show`, `breakdown` |
|
|
67
|
+
| `account` | `status`, `claim`, `login`, `logout` |
|
|
68
|
+
| `deployment` | `setup`, `connect`, `status`, `update`, `domain` |
|
|
69
|
+
| `operation` | `status` and `wait` for browser handoffs |
|
|
30
70
|
|
|
31
|
-
|
|
71
|
+
`agw --help` lists every command; `agw <group> <command> --help` lists its flags.
|
|
32
72
|
|
|
33
|
-
|
|
73
|
+
## Using `agw` from an agent
|
|
34
74
|
|
|
35
|
-
|
|
75
|
+
Full guide: [docs.appaigateway.com/automation/cli](https://docs.appaigateway.com/automation/cli).
|
|
76
|
+
The whole documentation is also published as
|
|
77
|
+
[agents.md](https://docs.appaigateway.com/agents.md).
|
|
36
78
|
|
|
37
|
-
|
|
79
|
+
**Output.** Add `--json` for exactly one JSON document on stdout; progress goes
|
|
80
|
+
to stderr. Add `--no-input` to fail instead of prompting.
|
|
38
81
|
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
agw deployment domain --hostname ai.example.com --dry-run
|
|
82
|
+
```json
|
|
83
|
+
{ "schemaVersion": 1, "ok": true, "context": { "url": "...", "accountId": "..." }, "result": { } }
|
|
84
|
+
{ "schemaVersion": 1, "ok": false, "error": { "code": "...", "message": "...", "nextAction": "...", "details": { } } }
|
|
43
85
|
```
|
|
44
86
|
|
|
45
|
-
|
|
87
|
+
Every error names a `code` and a `nextAction`. Follow the `nextAction`.
|
|
88
|
+
|
|
89
|
+
| Exit | Meaning |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `0` | Success, including a browser handoff that is still pending |
|
|
92
|
+
| `2` | Invalid input or validation failure |
|
|
93
|
+
| `3` | Gateway unreachable, remote error, or failed handoff |
|
|
94
|
+
| `4` | Local state or authentication problem |
|
|
95
|
+
| `5` | `operation wait` timed out; the operation is still pending |
|
|
46
96
|
|
|
47
|
-
|
|
97
|
+
**Secrets.** Never put a key in a command argument. Either pipe it with
|
|
98
|
+
`--key-stdin`, or hand the browser to a human:
|
|
48
99
|
|
|
49
|
-
|
|
100
|
+
```sh
|
|
101
|
+
agw provider add --type openai --browser --no-open --json # prints a URL
|
|
102
|
+
agw operation wait <operation-id> --timeout 300 --json # resumes when done
|
|
103
|
+
```
|
|
50
104
|
|
|
51
|
-
|
|
105
|
+
Share the URL with the person who holds the credential. Nothing typed on that
|
|
106
|
+
page reaches the CLI or its output.
|
|
52
107
|
|
|
53
|
-
|
|
108
|
+
**Editing apps.** Read before you write. `app show --json` returns the full
|
|
109
|
+
config; save `{name, config, status}` from `result.app`, edit it, and pass it
|
|
110
|
+
back with `app update <id> --file`. A concurrent change is rejected rather than
|
|
111
|
+
overwritten. `app validate --file` checks a file without touching the account.
|
|
54
112
|
|
|
55
|
-
|
|
113
|
+
**State.** Connection, credentials and pending operations live in
|
|
114
|
+
`~/.local/state/agw` (`$XDG_STATE_HOME/agw`, or `%LOCALAPPDATA%/agw` on
|
|
115
|
+
Windows), mode 0700. Do not delete it to fix an authentication error; run
|
|
116
|
+
`agw account login` instead. Output files are never overwritten.
|
|
56
117
|
|
|
57
|
-
|
|
118
|
+
## Contributing
|
|
58
119
|
|
|
59
|
-
|
|
120
|
+
The CLI lives in `cli/` of the
|
|
121
|
+
[main repository](https://github.com/maxceem/app-ai-gateway). From the repo root:
|
|
60
122
|
|
|
61
|
-
|
|
123
|
+
```sh
|
|
124
|
+
pnpm run cli:check # type-check and bundle
|
|
125
|
+
pnpm run cli:test # run the CLI tests
|
|
126
|
+
```
|
|
62
127
|
|
|
63
|
-
|
|
128
|
+
Both are part of `pnpm run verify`. Releasing is two steps in order:
|
|
129
|
+
`pnpm run cli:release:github` builds the gateway archive and uploads it to the
|
|
130
|
+
GitHub release, then `npm publish` from `cli/` ships the executable that
|
|
131
|
+
demands that exact archive. Licensed under Apache-2.0.
|
package/dist/agw.mjs
CHANGED
|
@@ -6,7 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// <define:__AGW_MANIFEST__>
|
|
9
|
-
var define_AGW_MANIFEST_default = { version: "0.1.
|
|
9
|
+
var define_AGW_MANIFEST_default = { version: "0.1.6", wrangler: "4.96.0", upgradeFrom: ["0.1.6", "0.1.5", "0.1.4", "0.1.3", "0.1.2", "0.1.1", "0.1.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.1.6/gateway-0.1.6.tar.gz", sha256: "991b01cceb651259fdd340c6010bd254f1f2c4d0363d6c20028dda40c03d01d9", manifest: "cd0e66ec52965108fddc50ee3e36e5fb39f5a2e868061d333279c3fdb609e57d" } };
|
|
10
10
|
|
|
11
11
|
// src/main.ts
|
|
12
12
|
import { Writable } from "node:stream";
|
|
@@ -7649,8 +7649,8 @@ function ko_default() {
|
|
|
7649
7649
|
}
|
|
7650
7650
|
|
|
7651
7651
|
// ../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/lt.js
|
|
7652
|
-
var capitalizeFirstCharacter = (
|
|
7653
|
-
return
|
|
7652
|
+
var capitalizeFirstCharacter = (text2) => {
|
|
7653
|
+
return text2.charAt(0).toUpperCase() + text2.slice(1);
|
|
7654
7654
|
};
|
|
7655
7655
|
function getUnitTypeFromNumber(number4) {
|
|
7656
7656
|
const abs = Math.abs(number4);
|
|
@@ -15511,7 +15511,6 @@ var CliOperationRequestSchema = external_exports.object({
|
|
|
15511
15511
|
var CliSubmissionRequestSchema = external_exports.object({
|
|
15512
15512
|
submissionToken: CliProofSchema,
|
|
15513
15513
|
approve: external_exports.literal(true).optional(),
|
|
15514
|
-
allowServiceAccess: external_exports.boolean().optional(),
|
|
15515
15514
|
email: external_exports.email().optional(),
|
|
15516
15515
|
password: external_exports.string().min(8).max(256).optional(),
|
|
15517
15516
|
name: external_exports.string().min(1).max(100).optional(),
|
|
@@ -15530,11 +15529,27 @@ var CliAccountSchema = external_exports.object({
|
|
|
15530
15529
|
claimed: external_exports.boolean(),
|
|
15531
15530
|
expiresAt: external_exports.string().nullable()
|
|
15532
15531
|
});
|
|
15532
|
+
var CliApprovalRefusalSchema = external_exports.enum([
|
|
15533
|
+
"registration_required",
|
|
15534
|
+
"sign_out_required"
|
|
15535
|
+
]);
|
|
15536
|
+
var CliViewerSchema = external_exports.object({
|
|
15537
|
+
name: external_exports.string().nullable(),
|
|
15538
|
+
email: external_exports.string().nullable()
|
|
15539
|
+
});
|
|
15533
15540
|
var CliBrowserDetailsResponseSchema = external_exports.object({
|
|
15534
15541
|
kind: CliOperationRequestSchema.shape.kind,
|
|
15535
15542
|
payload: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
15536
15543
|
account: CliAccountSchema,
|
|
15537
|
-
|
|
15544
|
+
viewer: CliViewerSchema.nullable(),
|
|
15545
|
+
/**
|
|
15546
|
+
* What stands between this browser and the Approve button, or null when
|
|
15547
|
+
* nothing does — which is every non-claim kind, since only a claim asks who
|
|
15548
|
+
* is holding the browser. One field decides the whole screen: each value
|
|
15549
|
+
* names the single thing the page may offer, so the page never has to
|
|
15550
|
+
* consult the handoff kind to know what to put in front of a person.
|
|
15551
|
+
*/
|
|
15552
|
+
blockedBy: CliApprovalRefusalSchema.nullable(),
|
|
15538
15553
|
googleEnabled: external_exports.boolean(),
|
|
15539
15554
|
expiresAt: external_exports.string()
|
|
15540
15555
|
});
|
|
@@ -15569,8 +15584,6 @@ var CliOperationResponseSchema = external_exports.object({
|
|
|
15569
15584
|
var CliOperationResultSchema = external_exports.object({
|
|
15570
15585
|
/** The account a claim acted on. */
|
|
15571
15586
|
accountId: external_exports.string().optional(),
|
|
15572
|
-
/** Whether the claiming human left the CLI's service access in place. */
|
|
15573
|
-
accessGranted: external_exports.boolean().optional(),
|
|
15574
15587
|
/** The stored row a provider submission created or rotated. */
|
|
15575
15588
|
provider: ProviderSummarySchema.optional(),
|
|
15576
15589
|
gateway: ProviderGatewaySummarySchema.optional()
|
|
@@ -15961,11 +15974,12 @@ function fromPackageJson() {
|
|
|
15961
15974
|
if (typeof packaged.version !== "string" || typeof wrangler !== "string") {
|
|
15962
15975
|
throw new Error("cli/package.json must declare a version and a pinned wrangler dependency");
|
|
15963
15976
|
}
|
|
15964
|
-
return { version: packaged.version, wrangler };
|
|
15977
|
+
return { version: packaged.version, wrangler, release: null };
|
|
15965
15978
|
}
|
|
15966
15979
|
var manifest = typeof define_AGW_MANIFEST_default === "object" && define_AGW_MANIFEST_default !== null ? define_AGW_MANIFEST_default : fromPackageJson();
|
|
15967
15980
|
var VERSION = manifest.version;
|
|
15968
15981
|
var WRANGLER_VERSION = manifest.wrangler;
|
|
15982
|
+
var RELEASE = manifest.release ?? null;
|
|
15969
15983
|
|
|
15970
15984
|
// src/common.ts
|
|
15971
15985
|
var CLOUD = "https://api.appaigateway.com";
|
|
@@ -16104,15 +16118,21 @@ var common = "json! no-input!";
|
|
|
16104
16118
|
var commands = {
|
|
16105
16119
|
"deployment setup": {
|
|
16106
16120
|
args: 0,
|
|
16107
|
-
flags: "name cloudflare-account-id domain no-domain! version dry-run! yes!"
|
|
16121
|
+
flags: "name cloudflare-account-id domain no-domain! version release-archive dry-run! yes!"
|
|
16108
16122
|
},
|
|
16109
16123
|
"deployment connect": {
|
|
16110
16124
|
args: 0,
|
|
16111
16125
|
flags: `url cloud! ${managementKey}`
|
|
16112
16126
|
},
|
|
16113
16127
|
"deployment status": { args: 0, flags: "" },
|
|
16114
|
-
"deployment update": {
|
|
16115
|
-
|
|
16128
|
+
"deployment update": {
|
|
16129
|
+
args: 0,
|
|
16130
|
+
flags: "version release-archive dry-run! yes!"
|
|
16131
|
+
},
|
|
16132
|
+
"deployment domain": {
|
|
16133
|
+
args: 0,
|
|
16134
|
+
flags: "hostname release-archive dry-run! yes!"
|
|
16135
|
+
},
|
|
16116
16136
|
"account status": { args: 0, flags: "" },
|
|
16117
16137
|
"account claim": { args: 0, flags: browser },
|
|
16118
16138
|
"account login": { args: 0, flags: managementKey },
|
|
@@ -16528,6 +16548,14 @@ function stateDirectory() {
|
|
|
16528
16548
|
"agw"
|
|
16529
16549
|
);
|
|
16530
16550
|
}
|
|
16551
|
+
function cacheDirectory() {
|
|
16552
|
+
if (platform() === "win32")
|
|
16553
|
+
return join(
|
|
16554
|
+
process.env["LOCALAPPDATA"] || join(homedir(), "AppData", "Local"),
|
|
16555
|
+
"agw"
|
|
16556
|
+
);
|
|
16557
|
+
return join(process.env["XDG_CACHE_HOME"] || join(homedir(), ".cache"), "agw");
|
|
16558
|
+
}
|
|
16531
16559
|
async function protectedDirectory(path) {
|
|
16532
16560
|
await mkdir(path, { recursive: true, mode: 448 });
|
|
16533
16561
|
const s = await lstat(path);
|
|
@@ -16566,14 +16594,14 @@ async function reserveOutput(path, { reservation, retain = false } = {}) {
|
|
|
16566
16594
|
},
|
|
16567
16595
|
async write(value) {
|
|
16568
16596
|
if (!handle) throw new Error("The reserved output is closed");
|
|
16569
|
-
const
|
|
16597
|
+
const text2 = typeof value === "string" ? value : JSON.stringify(value, null, 2) + "\n";
|
|
16570
16598
|
try {
|
|
16571
16599
|
if (retain) {
|
|
16572
16600
|
const existing = await handle.readFile("utf8");
|
|
16573
|
-
if (existing && !
|
|
16601
|
+
if (existing && !text2.startsWith(existing)) fail("output_changed", "The reserved output contains different data.", "Choose a new --key-output path; no existing data was overwritten.", 4);
|
|
16574
16602
|
await handle.truncate(0);
|
|
16575
|
-
await handle.write(
|
|
16576
|
-
} else await handle.writeFile(
|
|
16603
|
+
await handle.write(text2, 0, "utf8");
|
|
16604
|
+
} else await handle.writeFile(text2);
|
|
16577
16605
|
await handle.sync();
|
|
16578
16606
|
await handle.close();
|
|
16579
16607
|
handle = null;
|
|
@@ -17793,14 +17821,7 @@ Operation: ${data.id}
|
|
|
17793
17821
|
4
|
|
17794
17822
|
);
|
|
17795
17823
|
operation2.completed = true;
|
|
17796
|
-
if (this.active)
|
|
17797
|
-
if (data.account) this.active.account = data.account;
|
|
17798
|
-
if (data.result?.accessGranted === false) {
|
|
17799
|
-
delete this.active.credential;
|
|
17800
|
-
this.active.authenticated = false;
|
|
17801
|
-
this.state.generation = (this.state.generation ?? 0) + 1;
|
|
17802
|
-
}
|
|
17803
|
-
}
|
|
17824
|
+
if (this.active && data.account) this.active.account = data.account;
|
|
17804
17825
|
await this.save();
|
|
17805
17826
|
}
|
|
17806
17827
|
return data;
|
|
@@ -17863,42 +17884,226 @@ async function openBrowser(url2) {
|
|
|
17863
17884
|
import { spawn as spawn2 } from "node:child_process";
|
|
17864
17885
|
import { createRequire as createRequire2 } from "node:module";
|
|
17865
17886
|
import { randomUUID } from "node:crypto";
|
|
17866
|
-
import { cp, writeFile, unlink as unlink2 } from "node:fs/promises";
|
|
17887
|
+
import { cp, writeFile as writeFile2, unlink as unlink2 } from "node:fs/promises";
|
|
17867
17888
|
import { join as join3 } from "node:path";
|
|
17868
17889
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
17869
17890
|
|
|
17870
17891
|
// src/release.ts
|
|
17871
|
-
import { readFile as readFile3, lstat as lstat2 } from "node:fs/promises";
|
|
17892
|
+
import { readFile as readFile3, lstat as lstat2, mkdir as mkdir2, rename as rename2, rm, writeFile } from "node:fs/promises";
|
|
17872
17893
|
import { join as join2, dirname } from "node:path";
|
|
17873
17894
|
import { fileURLToPath } from "node:url";
|
|
17874
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
17875
|
-
|
|
17895
|
+
import { createHash as createHash4, randomBytes as randomBytes3 } from "node:crypto";
|
|
17896
|
+
|
|
17897
|
+
// src/archive.ts
|
|
17898
|
+
import { gunzipSync } from "node:zlib";
|
|
17899
|
+
var BLOCK = 512;
|
|
17900
|
+
var ARCHIVE_LIMIT = 64 * 1024 * 1024;
|
|
17901
|
+
function text(header, start, length) {
|
|
17902
|
+
const raw = header.subarray(start, start + length);
|
|
17903
|
+
const end = raw.indexOf(0);
|
|
17904
|
+
return raw.subarray(0, end === -1 ? raw.length : end).toString("latin1");
|
|
17905
|
+
}
|
|
17906
|
+
function octal(header, start, length, field) {
|
|
17907
|
+
const value = text(header, start, length).trim();
|
|
17908
|
+
if (!/^[0-7]+$/.test(value))
|
|
17909
|
+
fail("release_archive", `Release archive has an unreadable ${field} field.`);
|
|
17910
|
+
const parsed = Number.parseInt(value, 8);
|
|
17911
|
+
if (!Number.isSafeInteger(parsed))
|
|
17912
|
+
fail("release_archive", `Release archive has an out-of-range ${field} field.`);
|
|
17913
|
+
return parsed;
|
|
17914
|
+
}
|
|
17915
|
+
function checksumMatches(header) {
|
|
17916
|
+
const stored = octal(header, 148, 8, "checksum");
|
|
17917
|
+
let unsigned = 0;
|
|
17918
|
+
let signed = 0;
|
|
17919
|
+
for (let i = 0; i < BLOCK; i++) {
|
|
17920
|
+
const byte = i >= 148 && i < 156 ? 32 : header[i];
|
|
17921
|
+
unsigned += byte;
|
|
17922
|
+
signed += byte > 127 ? byte - 256 : byte;
|
|
17923
|
+
}
|
|
17924
|
+
return stored === unsigned || stored === signed;
|
|
17925
|
+
}
|
|
17926
|
+
function safePath(raw, directory) {
|
|
17927
|
+
const path = directory ? raw.replace(/\/+$/, "") : raw;
|
|
17928
|
+
if (!path || path.startsWith("/") || path.includes("\\") || path.split("/").some((segment) => segment === "" || segment === "." || segment === ".."))
|
|
17929
|
+
fail("release_archive", "Release archive contains an unsafe path.");
|
|
17930
|
+
return path;
|
|
17931
|
+
}
|
|
17932
|
+
function readArchive(gzipped, limit = ARCHIVE_LIMIT) {
|
|
17933
|
+
let tar;
|
|
17934
|
+
try {
|
|
17935
|
+
tar = gunzipSync(gzipped, { maxOutputLength: limit });
|
|
17936
|
+
} catch {
|
|
17937
|
+
fail("release_archive", "Release archive is not readable gzip within the size limit.");
|
|
17938
|
+
}
|
|
17939
|
+
if (tar.length % BLOCK !== 0)
|
|
17940
|
+
fail("release_archive", "Release archive is not a whole number of tar blocks.");
|
|
17941
|
+
const entries = [];
|
|
17942
|
+
let total = 0;
|
|
17943
|
+
let offset = 0;
|
|
17944
|
+
while (offset + BLOCK <= tar.length) {
|
|
17945
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
17946
|
+
if (header.every((byte) => byte === 0)) {
|
|
17947
|
+
if (!tar.subarray(offset).every((byte) => byte === 0))
|
|
17948
|
+
fail("release_archive", "Release archive continues past its end marker.");
|
|
17949
|
+
return entries;
|
|
17950
|
+
}
|
|
17951
|
+
if (text(header, 257, 6).trim() !== "ustar")
|
|
17952
|
+
fail("release_archive", "Release archive is not in ustar format.");
|
|
17953
|
+
if (!checksumMatches(header))
|
|
17954
|
+
fail("release_archive", "Release archive has a corrupt header.");
|
|
17955
|
+
const type = String.fromCharCode(header[156]);
|
|
17956
|
+
if (type === "L" || type === "K")
|
|
17957
|
+
fail("release_archive", "Release archive uses GNU long-name extensions.");
|
|
17958
|
+
if (type !== "0" && type !== "\0" && type !== "5")
|
|
17959
|
+
fail(
|
|
17960
|
+
"release_archive",
|
|
17961
|
+
"Release archives may contain only regular files and directories."
|
|
17962
|
+
);
|
|
17963
|
+
const directory = type === "5";
|
|
17964
|
+
const prefix = text(header, 345, 155);
|
|
17965
|
+
const name = text(header, 0, 100);
|
|
17966
|
+
const path = safePath(prefix ? `${prefix}/${name}` : name, directory);
|
|
17967
|
+
const size = octal(header, 124, 12, "size");
|
|
17968
|
+
if (directory && size !== 0)
|
|
17969
|
+
fail("release_archive", "Release archive has a directory entry with contents.");
|
|
17970
|
+
total += size;
|
|
17971
|
+
if (total > limit) fail("release_archive", "Release archive exceeds its size limit.");
|
|
17972
|
+
const start = offset + BLOCK;
|
|
17973
|
+
if (start + size > tar.length)
|
|
17974
|
+
fail("release_archive", "Release archive is truncated.");
|
|
17975
|
+
entries.push({
|
|
17976
|
+
path,
|
|
17977
|
+
directory,
|
|
17978
|
+
data: directory ? Buffer.alloc(0) : Buffer.from(tar.subarray(start, start + size))
|
|
17979
|
+
});
|
|
17980
|
+
offset = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
17981
|
+
}
|
|
17982
|
+
fail("release_archive", "Release archive has no end marker.");
|
|
17983
|
+
}
|
|
17984
|
+
|
|
17985
|
+
// src/release.ts
|
|
17986
|
+
var digest = (data) => createHash4("sha256").update(data).digest("hex");
|
|
17987
|
+
async function fetchArchive(url2) {
|
|
17988
|
+
let response;
|
|
17989
|
+
try {
|
|
17990
|
+
response = await fetch(url2, {
|
|
17991
|
+
redirect: "follow",
|
|
17992
|
+
signal: AbortSignal.timeout(3e5),
|
|
17993
|
+
headers: { "user-agent": `agw/${VERSION}` }
|
|
17994
|
+
});
|
|
17995
|
+
} catch {
|
|
17996
|
+
fail(
|
|
17997
|
+
"release_download",
|
|
17998
|
+
`Cannot download the gateway release from ${url2}.`,
|
|
17999
|
+
"Check network access, or pass --release-archive with a copy downloaded elsewhere.",
|
|
18000
|
+
3
|
|
18001
|
+
);
|
|
18002
|
+
}
|
|
18003
|
+
if (!response.ok)
|
|
18004
|
+
fail(
|
|
18005
|
+
"release_download",
|
|
18006
|
+
`Downloading the gateway release from ${url2} failed with HTTP ${response.status}.`,
|
|
18007
|
+
"Check network access, or pass --release-archive with a copy downloaded elsewhere.",
|
|
18008
|
+
3
|
|
18009
|
+
);
|
|
18010
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
18011
|
+
if (body.length > ARCHIVE_LIMIT)
|
|
18012
|
+
fail("release_download", "The downloaded gateway release exceeds its size limit.", void 0, 3);
|
|
18013
|
+
return body;
|
|
18014
|
+
}
|
|
18015
|
+
async function extract(archive, directory) {
|
|
18016
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
18017
|
+
for (const entry of readArchive(archive)) {
|
|
18018
|
+
const path = join2(directory, entry.path);
|
|
18019
|
+
if (entry.directory) {
|
|
18020
|
+
await mkdir2(path, { recursive: true, mode: 448 });
|
|
18021
|
+
continue;
|
|
18022
|
+
}
|
|
18023
|
+
await mkdir2(dirname(path), { recursive: true, mode: 448 });
|
|
18024
|
+
await writeFile(path, entry.data, { mode: 384, flag: "wx" });
|
|
18025
|
+
}
|
|
18026
|
+
}
|
|
18027
|
+
async function cachedRelease(version2, integrity, source) {
|
|
18028
|
+
const root = source.cache ?? join2(cacheDirectory(), "releases");
|
|
18029
|
+
const directory = join2(root, version2);
|
|
18030
|
+
const cached2 = await readFile3(join2(directory, "manifest.json")).catch(() => null);
|
|
18031
|
+
if (cached2 && digest(cached2) === integrity.manifest) return directory;
|
|
18032
|
+
const supplied = source.archive ?? process.env["AGW_RELEASE_ARCHIVE"] ?? void 0;
|
|
18033
|
+
let archive;
|
|
18034
|
+
if (supplied) {
|
|
18035
|
+
archive = await readFile3(supplied).catch(
|
|
18036
|
+
() => fail(
|
|
18037
|
+
"release_unavailable",
|
|
18038
|
+
`Cannot read the gateway release archive at ${supplied}.`,
|
|
18039
|
+
"Supply the gateway-<version>.tar.gz asset published with this CLI release.",
|
|
18040
|
+
4
|
|
18041
|
+
)
|
|
18042
|
+
);
|
|
18043
|
+
} else {
|
|
18044
|
+
archive = await (source.download ?? fetchArchive)(integrity.url);
|
|
18045
|
+
}
|
|
18046
|
+
if (digest(archive) !== integrity.sha256)
|
|
18047
|
+
fail(
|
|
18048
|
+
"release_integrity",
|
|
18049
|
+
"The gateway release archive does not match the digest built into this CLI.",
|
|
18050
|
+
"Nothing was extracted. Obtain the archive published with this CLI version.",
|
|
18051
|
+
4
|
|
18052
|
+
);
|
|
18053
|
+
await protectedDirectory(root);
|
|
18054
|
+
const staging = join2(root, `.${version2}-${randomBytes3(8).toString("hex")}`);
|
|
18055
|
+
try {
|
|
18056
|
+
await extract(archive, staging);
|
|
18057
|
+
const manifest2 = await readFile3(join2(staging, "manifest.json")).catch(() => null);
|
|
18058
|
+
if (!manifest2 || digest(manifest2) !== integrity.manifest)
|
|
18059
|
+
fail(
|
|
18060
|
+
"release_integrity",
|
|
18061
|
+
"The gateway release archive does not contain the expected manifest.",
|
|
18062
|
+
"Nothing was installed. Obtain the archive published with this CLI version.",
|
|
18063
|
+
4
|
|
18064
|
+
);
|
|
18065
|
+
await rm(directory, { recursive: true, force: true });
|
|
18066
|
+
await rename2(staging, directory);
|
|
18067
|
+
} finally {
|
|
18068
|
+
await rm(staging, { recursive: true, force: true });
|
|
18069
|
+
}
|
|
18070
|
+
return directory;
|
|
18071
|
+
}
|
|
18072
|
+
async function release(requested = VERSION, base = dirname(fileURLToPath(import.meta.url)), source = {}) {
|
|
17876
18073
|
if (requested !== VERSION)
|
|
17877
18074
|
fail(
|
|
17878
18075
|
"unsupported_release",
|
|
17879
|
-
`This CLI
|
|
18076
|
+
`This CLI deploys gateway ${VERSION} only.`,
|
|
17880
18077
|
`Install @maxceem/agw@${requested} to use its matching release.`
|
|
17881
18078
|
);
|
|
17882
18079
|
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
17883
18080
|
if (major === void 0 || minor === void 0 || major < 22 || major === 22 && minor < 19)
|
|
17884
18081
|
fail("unsupported_runtime", "Node.js 22.19.0 or newer is required.");
|
|
17885
|
-
const
|
|
18082
|
+
const integrity = source.integrity === void 0 ? RELEASE : source.integrity;
|
|
18083
|
+
const packaged = join2(base, "release", requested);
|
|
18084
|
+
const local = await lstat2(packaged).catch(() => null);
|
|
18085
|
+
let directory;
|
|
18086
|
+
if (local?.isDirectory()) directory = packaged;
|
|
18087
|
+
else if (integrity) directory = await cachedRelease(requested, integrity, source);
|
|
18088
|
+
else
|
|
18089
|
+
fail(
|
|
18090
|
+
"release_missing",
|
|
18091
|
+
"This CLI build does not know where to obtain its deployable release.",
|
|
18092
|
+
"Install a published release; repository contributors must run the CLI release build.",
|
|
18093
|
+
4
|
|
18094
|
+
);
|
|
17886
18095
|
let serialized;
|
|
17887
|
-
let integrity;
|
|
17888
18096
|
try {
|
|
17889
18097
|
serialized = await readFile3(join2(directory, "manifest.json"), "utf8");
|
|
17890
|
-
integrity = JSON.parse(
|
|
17891
|
-
await readFile3(join2(base, "release-integrity.json"), "utf8")
|
|
17892
|
-
);
|
|
17893
18098
|
} catch {
|
|
17894
18099
|
fail(
|
|
17895
18100
|
"release_missing",
|
|
17896
|
-
"The
|
|
17897
|
-
"Install a
|
|
18101
|
+
"The deployable release is missing its manifest.",
|
|
18102
|
+
"Install a published release; repository contributors must run the CLI release build.",
|
|
17898
18103
|
4
|
|
17899
18104
|
);
|
|
17900
18105
|
}
|
|
17901
|
-
if (
|
|
18106
|
+
if (integrity && digest(serialized) !== integrity.manifest)
|
|
17902
18107
|
fail("release_integrity", "Release manifest integrity check failed.");
|
|
17903
18108
|
const manifest2 = JSON.parse(serialized);
|
|
17904
18109
|
if (manifest2.format !== 1 || manifest2.version !== requested || manifest2.wrangler !== WRANGLER_VERSION || manifest2.schema !== 1)
|
|
@@ -17911,7 +18116,7 @@ async function release(requested = VERSION, base = dirname(fileURLToPath(import.
|
|
|
17911
18116
|
fail("release_integrity", "Release includes an unsafe path.");
|
|
17912
18117
|
const file2 = join2(directory, path);
|
|
17913
18118
|
const info = await lstat2(file2);
|
|
17914
|
-
if (!info.isFile() || info.isSymbolicLink() ||
|
|
18119
|
+
if (!info.isFile() || info.isSymbolicLink() || digest(await readFile3(file2)) !== hash2)
|
|
17915
18120
|
fail("release_integrity", "Release asset integrity check failed.");
|
|
17916
18121
|
}
|
|
17917
18122
|
return {
|
|
@@ -17927,7 +18132,7 @@ async function release(requested = VERSION, base = dirname(fileURLToPath(import.
|
|
|
17927
18132
|
var require2 = createRequire2(import.meta.url);
|
|
17928
18133
|
var OUTPUT_TAIL = 2e3;
|
|
17929
18134
|
function wranglerFailure(args, code, output, redact = []) {
|
|
17930
|
-
const tail = redact.filter((value) => value.length >= 8).reduce((
|
|
18135
|
+
const tail = redact.filter((value) => value.length >= 8).reduce((text2, value) => text2.split(value).join("[redacted]"), output).trim().slice(-OUTPUT_TAIL);
|
|
17931
18136
|
return new CliError(
|
|
17932
18137
|
"wrangler_failed",
|
|
17933
18138
|
`wrangler ${args[0] ?? ""} failed${code === null ? "" : ` (exit ${code})`}.`,
|
|
@@ -18114,7 +18319,7 @@ async function configFile(ctx, journal, artifact) {
|
|
|
18114
18319
|
} : {}
|
|
18115
18320
|
};
|
|
18116
18321
|
const path = join3(directory, "wrangler.json");
|
|
18117
|
-
await
|
|
18322
|
+
await writeFile2(path, JSON.stringify(config2, null, 2), { mode: 384 });
|
|
18118
18323
|
return { path, directory, config: config2 };
|
|
18119
18324
|
}
|
|
18120
18325
|
async function verifyDeployment(ctx, url2, id) {
|
|
@@ -18334,7 +18539,9 @@ async function matchExisting(ctx, cf, flags) {
|
|
|
18334
18539
|
return journal;
|
|
18335
18540
|
}
|
|
18336
18541
|
async function deploymentCommand(ctx, command, flags, cf = new Cloudflare(), loadRelease = release) {
|
|
18337
|
-
const artifact = await loadRelease(flags.version
|
|
18542
|
+
const artifact = await loadRelease(flags.version, void 0, {
|
|
18543
|
+
archive: flags["release-archive"]
|
|
18544
|
+
});
|
|
18338
18545
|
await cf.authenticate(flags);
|
|
18339
18546
|
if (command !== "deployment setup") {
|
|
18340
18547
|
const journal2 = await matchExisting(ctx, cf, flags);
|
|
@@ -18500,7 +18707,7 @@ async function deploymentCommand(ctx, command, flags, cf = new Cloudflare(), loa
|
|
|
18500
18707
|
SECRET_VAULT_LOCAL_KEK_V1: await ctx.store.vaultKey(journal.id)
|
|
18501
18708
|
};
|
|
18502
18709
|
const secretPath = join3(file2.directory, "secrets.json");
|
|
18503
|
-
await
|
|
18710
|
+
await writeFile2(secretPath, JSON.stringify(secrets), {
|
|
18504
18711
|
mode: 384
|
|
18505
18712
|
});
|
|
18506
18713
|
try {
|
|
@@ -18616,10 +18823,10 @@ function* strings(value) {
|
|
|
18616
18823
|
}
|
|
18617
18824
|
function reported(value, sent) {
|
|
18618
18825
|
if (typeof value !== "string") return "";
|
|
18619
|
-
let
|
|
18826
|
+
let text2 = value.replace(new RegExp("\\p{C}", "gu"), " ").replace(/\s+/g, " ").trim();
|
|
18620
18827
|
for (const secret3 of strings(sent))
|
|
18621
|
-
if (secret3.length >= 8)
|
|
18622
|
-
return
|
|
18828
|
+
if (secret3.length >= 8) text2 = text2.split(secret3).join("[redacted]");
|
|
18829
|
+
return text2.length > MESSAGE_LIMIT ? text2.slice(0, MESSAGE_LIMIT) + "\u2026" : text2;
|
|
18623
18830
|
}
|
|
18624
18831
|
var RECOVERABLE_RESOURCE_CODES = ["resource_receipt_expired", "resource_key_unavailable"];
|
|
18625
18832
|
var RECOVERABLE_RESOURCE_FIELDS = ["appId", "keyId", "providerId", "providerGatewayId"];
|
|
@@ -18814,16 +19021,16 @@ async function main(argv = process.argv.slice(2), {
|
|
|
18814
19021
|
};
|
|
18815
19022
|
const rendered = ctx.onboarding ? { ...result, ...ctx.onboarding } : result;
|
|
18816
19023
|
const output = { schemaVersion: 1, ok: true, context, result: rendered };
|
|
18817
|
-
const
|
|
19024
|
+
const text2 = json2 ? JSON.stringify(output) + "\n" : humanResult(parsed.command, rendered, context);
|
|
18818
19025
|
if (stdout instanceof Writable) {
|
|
18819
19026
|
await new Promise(
|
|
18820
19027
|
(resolve3, reject2) => stdout.write(
|
|
18821
|
-
|
|
19028
|
+
text2,
|
|
18822
19029
|
(error51) => error51 ? reject2(error51) : resolve3()
|
|
18823
19030
|
)
|
|
18824
19031
|
);
|
|
18825
19032
|
} else {
|
|
18826
|
-
stdout.write(
|
|
19033
|
+
stdout.write(text2);
|
|
18827
19034
|
}
|
|
18828
19035
|
await ctx.acknowledgeOutput();
|
|
18829
19036
|
return 0;
|