@hasna/instructions 0.6.1 → 0.7.0
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 +123 -6
- package/dist/chunks/{apply-ynfn1av7.js → apply-7svah2qm.js} +5 -3
- package/dist/chunks/{apply-sfzja1f3.js → apply-jp13sa1e.js} +6 -3
- package/dist/chunks/database-wstmr56z.js +23 -0
- package/dist/chunks/{index-85n5wnd8.js → index-1cnyjnd7.js} +5 -7
- package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
- package/dist/chunks/{index-v7dkb384.js → index-4pfq42t5.js} +129 -77
- package/dist/chunks/index-5d7zdftc.js +186 -0
- package/dist/chunks/{index-1hpwskwd.js → index-7m4j8a33.js} +205 -329
- package/dist/chunks/index-8rb7ng2y.js +148 -0
- package/dist/chunks/index-arrrx0ed.js +344 -0
- package/dist/chunks/{index-9p9gn7gh.js → index-bzyxvwcd.js} +5 -7
- package/dist/chunks/index-fpeyzn3n.js +186 -0
- package/dist/chunks/{index-axhwg61p.js → index-fwbs6tma.js} +15 -4
- package/dist/chunks/index-n3fjspr0.js +119 -0
- package/dist/chunks/{index-e9epbsvs.js → index-z3ma4aqk.js} +15 -4
- package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
- package/dist/chunks/{local-a9nen65z.js → local-5wvv5xv3.js} +2 -2
- package/dist/chunks/{local-0d8qrc92.js → local-zxa5dpf4.js} +13 -188
- package/dist/chunks/s3-backup-n2z4fwyh.js +240 -0
- package/dist/chunks/s3-config-hhght91k.js +14 -0
- package/dist/chunks/s3-object-store-90tgbh6k.js +14 -0
- package/dist/chunks/{sync-m46fc257.js → sync-2075deve.js} +5 -4
- package/dist/chunks/{sync-vy1yjvs6.js → sync-r8t7zsp3.js} +4 -4
- package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
- package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
- package/dist/cli/index.js +1011 -247
- package/dist/data/config-store.d.ts +1 -0
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/index.js +840 -215
- package/dist/lib/apply.d.ts +7 -0
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/compact-output.d.ts +1 -0
- package/dist/lib/compact-output.d.ts.map +1 -1
- package/dist/lib/export.d.ts +22 -3
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/import.d.ts +18 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/legacy-store-migration.d.ts +39 -0
- package/dist/lib/legacy-store-migration.d.ts.map +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
- package/dist/lib/sync-dir.d.ts.map +1 -1
- package/dist/mcp/index.js +31 -20
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +205 -10
- package/dist/sdk/resolve.d.ts +1 -1
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/v1-client.d.ts +57 -0
- package/dist/sdk/v1-client.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +212 -21
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +13 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1180 -432
- package/dist/server/openapi.d.ts +1264 -92
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +25 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +57 -1
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +725 -0
- package/dist/storage/s3-backup.d.ts +76 -0
- package/dist/storage/s3-backup.d.ts.map +1 -0
- package/dist/storage/s3-config.d.ts +46 -0
- package/dist/storage/s3-config.d.ts.map +1 -0
- package/dist/storage/s3-object-store.d.ts +70 -0
- package/dist/storage/s3-object-store.d.ts.map +1 -0
- package/dist/types/index.d.ts +65 -3
- package/dist/types/index.d.ts.map +1 -1
- package/hasna.contract.json +110 -0
- package/migrations/0001_instructions.sql +63 -0
- package/migrations/0002_api_keys.sql +20 -0
- package/migrations/0003_idempotency_receipts.sql +24 -0
- package/package.json +22 -11
- package/assets/skills/inbox/SKILL.md +0 -86
package/README.md
CHANGED
|
@@ -18,9 +18,15 @@ bun add --global @hasna/instructions
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
instructions --help
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
# Hosted-by-default: resolves the approved key and uses
|
|
23
|
+
# https://api.hasna.com/instructions/v1.
|
|
24
|
+
instructions status
|
|
22
25
|
instructions list
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
# Explicit on-box SQLite mode; there is never an implicit fallback.
|
|
28
|
+
HASNA_INSTRUCTIONS_LOCAL=1 instructions init
|
|
29
|
+
HASNA_INSTRUCTIONS_LOCAL=1 instructions list
|
|
24
30
|
```
|
|
25
31
|
|
|
26
32
|
## Migrating from `@hasna/configs`
|
|
@@ -29,6 +35,19 @@ This package was formerly `@hasna/configs`. The `configs` CLI alias and
|
|
|
29
35
|
`configs-mcp` alias remain available for existing integrations; new usage can
|
|
30
36
|
use the `instructions` names.
|
|
31
37
|
|
|
38
|
+
Historical on-box rows can be migrated without overwriting the current store:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export HASNA_INSTRUCTIONS_LOCAL=1
|
|
42
|
+
instructions migrate-legacy --confirm-local --json # no-write plan
|
|
43
|
+
instructions migrate-legacy --confirm-local --apply --json # backup + transaction
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The source defaults to `~/.hasna/configs/configs.db`; the destination defaults
|
|
47
|
+
to the current `instructions.db`. A non-empty destination is refused unless
|
|
48
|
+
`--merge-preserve-destination` is explicit, and that merge never overwrites an
|
|
49
|
+
existing row.
|
|
50
|
+
|
|
32
51
|
## CLI Usage
|
|
33
52
|
|
|
34
53
|
```bash
|
|
@@ -109,8 +128,9 @@ instructions-mcp --http # http://127.0.0.1:8807/mcp
|
|
|
109
128
|
MCP_HTTP=1 instructions-mcp
|
|
110
129
|
```
|
|
111
130
|
|
|
112
|
-
Health: `GET http://127.0.0.1:8807/health`.
|
|
113
|
-
`instructions-serve`
|
|
131
|
+
Health: `GET http://127.0.0.1:8807/health`. The production
|
|
132
|
+
`instructions-serve` process deliberately does **not** mount MCP; local MCP and
|
|
133
|
+
the authenticated `/v1` service remain separate authorities.
|
|
114
134
|
|
|
115
135
|
## HTTP API server (`instructions-serve`)
|
|
116
136
|
|
|
@@ -120,14 +140,20 @@ instructions-serve
|
|
|
120
140
|
|
|
121
141
|
Surfaces:
|
|
122
142
|
|
|
123
|
-
- `GET /health`, `GET /ready`, `GET /version` → `{ status, version,
|
|
143
|
+
- `GET /health`, `GET /ready`, `GET /version` → `{ status, version, backend }`
|
|
124
144
|
- `GET /openapi.json`, `GET /v1/openapi.json` → the OpenAPI 3.1 document the SDK
|
|
125
145
|
is generated from.
|
|
126
146
|
- `/v1/*` — versioned cloud API (configs, profiles, snapshots, stats).
|
|
127
147
|
- No `/api/*` — the former local REST surface is not mounted (the removed
|
|
128
148
|
bundled dashboard was its only consumer).
|
|
129
149
|
|
|
130
|
-
### Server data backend (
|
|
150
|
+
### Server data backend (PostgreSQL, fail closed)
|
|
151
|
+
|
|
152
|
+
`instructions-serve` requires `HASNA_INSTRUCTIONS_DATABASE_URL` (or its documented
|
|
153
|
+
alias) for a usable `/v1` service. Without a DSN, `/health` remains a liveness
|
|
154
|
+
probe but reports `backend: "unconfigured"`; `/ready` returns 503 and `/v1`
|
|
155
|
+
returns a stable unavailable error. It never exposes local SQLite over the
|
|
156
|
+
service boundary.
|
|
131
157
|
|
|
132
158
|
When `HASNA_INSTRUCTIONS_DATABASE_URL` is set the `/v1` API reads/writes the
|
|
133
159
|
shared Postgres **directly** (no local sync/cache in the service) and every
|
|
@@ -150,6 +176,16 @@ they resolve `HASNA_INSTRUCTIONS_API_KEY` (or the Keychain / credentials-file
|
|
|
150
176
|
tiers) through the one `@hasna/contracts` client resolver, and the authority
|
|
151
177
|
defaults to the fleet gateway `https://api.hasna.com/instructions`.
|
|
152
178
|
|
|
179
|
+
Collection responses are producer-bounded with `limit`/`cursor` envelopes.
|
|
180
|
+
Current clients follow every page; `view=identity` returns an allowlisted
|
|
181
|
+
metadata-only projection for configs, profiles, and machines, without loading
|
|
182
|
+
instruction content or private profile fields. Retryable create and binding
|
|
183
|
+
writes may send `Idempotency-Key`; PostgreSQL stores the authenticated
|
|
184
|
+
principal, operation, canonical request digest, and first committed response in
|
|
185
|
+
the same transaction as the domain mutation. A same-body retry replays that
|
|
186
|
+
response, while key reuse with different bytes returns
|
|
187
|
+
`409 IDEMPOTENCY_KEY_REUSED`.
|
|
188
|
+
|
|
153
189
|
## SDK
|
|
154
190
|
|
|
155
191
|
The importable module ships INSIDE `@hasna/instructions` at the `./sdk` export
|
|
@@ -215,6 +251,87 @@ transport is decided by what resolves, never by a mode word.
|
|
|
215
251
|
Clients never hold a database DSN. The raw Postgres connection is a server-only
|
|
216
252
|
concern (`instructions-serve`), selected by `HASNA_INSTRUCTIONS_DATABASE_URL`.
|
|
217
253
|
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## Domain export and import
|
|
257
|
+
|
|
258
|
+
`instructions export` writes a restorable Instructions domain archive v2. The
|
|
259
|
+
archive contains config content and every retained config snapshot, profiles,
|
|
260
|
+
ordered profile membership and config-binding metadata, profile asset bindings,
|
|
261
|
+
and registered machines. Relationships are recorded by stable config/profile
|
|
262
|
+
slugs and machine hostnames, so import can map them safely onto destination IDs.
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
instructions export --output ./instructions-domain.tar.gz
|
|
266
|
+
instructions import ./instructions-domain.tar.gz # exact recovery into an empty destination
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
V2 is deliberately a recovery format, not a merge format. Import validates the
|
|
270
|
+
complete archive and requires a destination with zero configs, profiles, and
|
|
271
|
+
machines before the first mutation. `--overwrite` is rejected for v2 before
|
|
272
|
+
destination inspection or mutation. Any validation, mutation, or readback
|
|
273
|
+
failure throws and makes the CLI exit nonzero; discard that attempted
|
|
274
|
+
destination and retry from a newly empty database.
|
|
275
|
+
|
|
276
|
+
The manifest contains deterministic per-collection counts and SHA-256 logical
|
|
277
|
+
hashes for pre/post deployment comparison without placing instruction content
|
|
278
|
+
in logs. Exact deployment integrity includes config `created_at`, `updated_at`,
|
|
279
|
+
and `synced_at`; snapshot `created_at`; profile `created_at` and `updated_at`;
|
|
280
|
+
and machine `created_at` plus the exact `last_applied_at` value. After recovery,
|
|
281
|
+
import reads the complete domain back through `ConfigStore` and verifies every
|
|
282
|
+
field that interface can reproduce: config data and versions, retained snapshot
|
|
283
|
+
contents, profiles, ordered bindings, asset mappings, machines, and whether a
|
|
284
|
+
machine was ever applied.
|
|
285
|
+
|
|
286
|
+
`ConfigStore` cannot assign archived config/profile/snapshot/machine creation or
|
|
287
|
+
update timestamps, nor an exact machine `last_applied_at`; generated destination
|
|
288
|
+
IDs are also intentionally remapped by stable slugs and hostnames. These values
|
|
289
|
+
remain protected in the archive's exact integrity hashes but are not recreated
|
|
290
|
+
by recovery. ConfigStore also has no cross-entity transaction, so an operational
|
|
291
|
+
failure may leave a partial destination; the nonzero result is terminal and that
|
|
292
|
+
destination must not be reused. Legacy v1 config-only archives remain importable,
|
|
293
|
+
including their historical skip/overwrite conflict behavior.
|
|
294
|
+
|
|
295
|
+
API keys and idempotency receipts are intentionally excluded: they are security
|
|
296
|
+
and transport state that must be provisioned independently. Feedback is also
|
|
297
|
+
excluded because it is product telemetry, not part of the Instructions
|
|
298
|
+
configuration domain.
|
|
299
|
+
|
|
300
|
+
## Native S3 backup storage
|
|
301
|
+
|
|
302
|
+
S3 is an **adjunct immutable backup plane**, never a database selector. SQLite
|
|
303
|
+
remains the explicit local authority and PostgreSQL remains the hosted `/v1`
|
|
304
|
+
authority. Setting S3 variables alone never opens SQLite, never selects HTTP,
|
|
305
|
+
and never changes CRUD routing.
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
export HASNA_INSTRUCTIONS_S3_BUCKET=your-private-bucket
|
|
309
|
+
export HASNA_INSTRUCTIONS_AWS_REGION=us-east-1
|
|
310
|
+
# Optional: HASNA_INSTRUCTIONS_S3_PREFIX (default: instructions/)
|
|
311
|
+
# Optional local/S3-compatible endpoint and explicit static credentials.
|
|
312
|
+
|
|
313
|
+
instructions storage status --json
|
|
314
|
+
instructions export --output ./instructions-backup.tar.gz
|
|
315
|
+
instructions storage backup push ./instructions-backup.tar.gz --id 2026-09-15-pre-deploy --dry-run --json
|
|
316
|
+
instructions storage backup push ./instructions-backup.tar.gz --id 2026-09-15-pre-deploy --json
|
|
317
|
+
instructions storage backup verify 2026-09-15-pre-deploy --json
|
|
318
|
+
instructions storage backup pull 2026-09-15-pre-deploy --output ./restored-instructions.tar.gz --json
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Each backup uses traversal-safe deterministic keys, an atomically created
|
|
322
|
+
payload, and an atomically created manifest containing SHA-256, byte size,
|
|
323
|
+
content type, and creation time. Native S3 creation uses `If-None-Match: *`;
|
|
324
|
+
concurrent different-byte writers cannot replace the winner, identical replay
|
|
325
|
+
is idempotent, and an injected store without conditional-create support fails
|
|
326
|
+
closed. Pulls verify the payload before an owner-only local file is written.
|
|
327
|
+
Production backup buckets must enable versioning, encryption, public-access
|
|
328
|
+
blocking, and S3 Object Lock with a default retention period. AWS runtime
|
|
329
|
+
credentials may come from Bun's standard AWS chain (including an ECS task role);
|
|
330
|
+
explicit static credentials are optional and must be a complete pair. Status
|
|
331
|
+
never prints credential values or the bucket name.
|
|
332
|
+
|
|
333
|
+
The public importable surface is available at `@hasna/instructions/storage`.
|
|
334
|
+
|
|
218
335
|
## Data Directory
|
|
219
336
|
|
|
220
337
|
Local data (the SQLite store and backups) lives under the configs store home,
|
|
@@ -3,20 +3,22 @@ import {
|
|
|
3
3
|
applyConfig,
|
|
4
4
|
applyConfigs,
|
|
5
5
|
applyConfigsWithReport,
|
|
6
|
+
compactPathForConfigHome,
|
|
6
7
|
expandPath,
|
|
7
8
|
getConfigHome,
|
|
8
9
|
normalizeTargetPath,
|
|
9
10
|
previewConfigs
|
|
10
|
-
} from "./index-
|
|
11
|
-
import"./index-
|
|
11
|
+
} from "./index-fwbs6tma.js";
|
|
12
|
+
import"./index-4pfq42t5.js";
|
|
12
13
|
import"./index-35z65jsa.js";
|
|
13
|
-
import"./index-
|
|
14
|
+
import"./index-3djthcm6.js";
|
|
14
15
|
import"./index-nnwp92tk.js";
|
|
15
16
|
export {
|
|
16
17
|
previewConfigs,
|
|
17
18
|
normalizeTargetPath,
|
|
18
19
|
getConfigHome,
|
|
19
20
|
expandPath,
|
|
21
|
+
compactPathForConfigHome,
|
|
20
22
|
applyConfigsWithReport,
|
|
21
23
|
applyConfigs,
|
|
22
24
|
applyConfig
|
|
@@ -3,19 +3,22 @@ import {
|
|
|
3
3
|
applyConfig,
|
|
4
4
|
applyConfigs,
|
|
5
5
|
applyConfigsWithReport,
|
|
6
|
+
compactPathForConfigHome,
|
|
6
7
|
expandPath,
|
|
7
8
|
getConfigHome,
|
|
8
9
|
normalizeTargetPath,
|
|
9
10
|
previewConfigs
|
|
10
|
-
} from "./index-
|
|
11
|
-
import"./index-
|
|
12
|
-
import"./index-
|
|
11
|
+
} from "./index-z3ma4aqk.js";
|
|
12
|
+
import"./index-7m4j8a33.js";
|
|
13
|
+
import"./index-n3fjspr0.js";
|
|
14
|
+
import"./index-fpeyzn3n.js";
|
|
13
15
|
import"./index-bsan4c16.js";
|
|
14
16
|
export {
|
|
15
17
|
previewConfigs,
|
|
16
18
|
normalizeTargetPath,
|
|
17
19
|
getConfigHome,
|
|
18
20
|
expandPath,
|
|
21
|
+
compactPathForConfigHome,
|
|
19
22
|
applyConfigsWithReport,
|
|
20
23
|
applyConfigs,
|
|
21
24
|
applyConfig
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
getDatabase,
|
|
4
|
+
insertFeedback,
|
|
5
|
+
resetDatabase,
|
|
6
|
+
resetLocalDatabase
|
|
7
|
+
} from "./index-5d7zdftc.js";
|
|
8
|
+
import {
|
|
9
|
+
now,
|
|
10
|
+
slugify,
|
|
11
|
+
uuid
|
|
12
|
+
} from "./index-ma2xmp6c.js";
|
|
13
|
+
import"./index-fpeyzn3n.js";
|
|
14
|
+
import"./index-bsan4c16.js";
|
|
15
|
+
export {
|
|
16
|
+
uuid,
|
|
17
|
+
slugify,
|
|
18
|
+
resetLocalDatabase,
|
|
19
|
+
resetDatabase,
|
|
20
|
+
now,
|
|
21
|
+
insertFeedback,
|
|
22
|
+
getDatabase
|
|
23
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
applyConfigsWithReport,
|
|
4
|
+
compactPathForConfigHome,
|
|
4
5
|
expandPath,
|
|
5
6
|
getConfigHome,
|
|
6
7
|
normalizeTargetPath
|
|
7
|
-
} from "./index-
|
|
8
|
+
} from "./index-z3ma4aqk.js";
|
|
8
9
|
import {
|
|
9
10
|
applyTransform,
|
|
10
11
|
detectMachineContext,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
resolveConfigStore,
|
|
16
17
|
retiredOrUnsupportedAgentReason,
|
|
17
18
|
templateizeMachineContent
|
|
18
|
-
} from "./index-
|
|
19
|
+
} from "./index-7m4j8a33.js";
|
|
19
20
|
|
|
20
21
|
// src/lib/sync.ts
|
|
21
22
|
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
@@ -24,7 +25,6 @@ import { basename, extname, join as join2 } from "path";
|
|
|
24
25
|
// src/lib/sync-dir.ts
|
|
25
26
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
26
27
|
import { join, relative } from "path";
|
|
27
|
-
import { homedir } from "os";
|
|
28
28
|
var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
29
29
|
function shouldSkip(p) {
|
|
30
30
|
return SKIP.some((s) => p.includes(s));
|
|
@@ -36,7 +36,6 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
36
36
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
37
37
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join(absDir, f)).filter((f) => statSync(f).isFile());
|
|
38
38
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
39
|
-
const home = homedir();
|
|
40
39
|
const allConfigs = await store.listConfigs();
|
|
41
40
|
for (const file of files) {
|
|
42
41
|
if (shouldSkip(file)) {
|
|
@@ -49,7 +48,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
49
48
|
result.skipped.push(file + " (too large)");
|
|
50
49
|
continue;
|
|
51
50
|
}
|
|
52
|
-
const targetPath = file
|
|
51
|
+
const targetPath = compactPathForConfigHome(file);
|
|
53
52
|
const redacted = redactContent(content, redactFormatForTarget(targetPath, detectFormat(file)));
|
|
54
53
|
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
55
54
|
if (!existing) {
|
|
@@ -71,9 +70,8 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
71
70
|
}
|
|
72
71
|
async function syncToDir(dir, opts = {}) {
|
|
73
72
|
const store = opts.store ?? resolveConfigStore();
|
|
74
|
-
const home = homedir();
|
|
75
73
|
const absDir = expandPath(dir);
|
|
76
|
-
const normalized = dir.startsWith("~/") ? dir : absDir
|
|
74
|
+
const normalized = dir.startsWith("~/") ? dir : compactPathForConfigHome(absDir);
|
|
77
75
|
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
78
76
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
79
77
|
for (const config of configs) {
|
|
@@ -28,7 +28,6 @@ var ASSET_DESTINATION_STRATEGIES = ["emit-file", "install-local", "install-marke
|
|
|
28
28
|
var ASSET_SCOPES = ["global", "project", "session"];
|
|
29
29
|
var ASSET_UNINSTALL_POLICIES = ["remove-managed", "retain"];
|
|
30
30
|
var ASSET_ROLLBACK_POLICIES = ["snapshot", "installer-receipt", "none"];
|
|
31
|
-
|
|
32
31
|
class ConfigNotFoundError extends Error {
|
|
33
32
|
constructor(id) {
|
|
34
33
|
super(`Config not found: ${id}`);
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
isTemplate,
|
|
16
16
|
renderTemplate,
|
|
17
17
|
renderTemplatePreview
|
|
18
|
-
} from "./index-
|
|
18
|
+
} from "./index-3djthcm6.js";
|
|
19
19
|
import {
|
|
20
20
|
__export,
|
|
21
21
|
__require
|
|
@@ -203,6 +203,7 @@ import { randomUUID } from "crypto";
|
|
|
203
203
|
// src/lib/compact-output.ts
|
|
204
204
|
var DEFAULT_LIST_LIMIT = 20;
|
|
205
205
|
var MAX_LIST_LIMIT = 100;
|
|
206
|
+
var MAX_LIST_CURSOR = 1e5;
|
|
206
207
|
function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
|
|
207
208
|
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
208
209
|
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
@@ -213,7 +214,7 @@ function parseCursor(value) {
|
|
|
213
214
|
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
214
215
|
if (!Number.isFinite(parsed) || parsed < 0)
|
|
215
216
|
return 0;
|
|
216
|
-
return Math.floor(parsed);
|
|
217
|
+
return Math.min(Math.floor(parsed), MAX_LIST_CURSOR);
|
|
217
218
|
}
|
|
218
219
|
function paginate(items, opts = {}) {
|
|
219
220
|
const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
|
|
@@ -720,7 +721,7 @@ function optionalString(value, label) {
|
|
|
720
721
|
return requiredString(value, label);
|
|
721
722
|
}
|
|
722
723
|
|
|
723
|
-
//
|
|
724
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/transport.js
|
|
724
725
|
import { createRequire } from "module";
|
|
725
726
|
function envToken(name) {
|
|
726
727
|
return name.toUpperCase().replace(/-/g, "_");
|
|
@@ -754,7 +755,7 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
|
754
755
|
"x-original-host"
|
|
755
756
|
]);
|
|
756
757
|
|
|
757
|
-
//
|
|
758
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/storage.js
|
|
758
759
|
import { isIP } from "net";
|
|
759
760
|
import { spawnSync } from "child_process";
|
|
760
761
|
import { closeSync, fstatSync, openSync, readFileSync } from "fs";
|
|
@@ -2032,11 +2033,11 @@ function announceLocalInstructionsMode(write = (line) => process.stderr.write(`$
|
|
|
2032
2033
|
}
|
|
2033
2034
|
|
|
2034
2035
|
// src/data/config-store.ts
|
|
2035
|
-
function parseBoundedPagePayload(value, label) {
|
|
2036
|
+
function parseBoundedPagePayload(value, label, expected) {
|
|
2036
2037
|
const page = value;
|
|
2037
2038
|
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
2038
2039
|
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
2039
|
-
if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
|
|
2040
|
+
if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed) || expected !== undefined && Number(page.cursor) !== expected.cursor) {
|
|
2040
2041
|
throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
|
|
2041
2042
|
}
|
|
2042
2043
|
return {
|
|
@@ -2048,7 +2049,7 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
|
2048
2049
|
if (value && typeof value === "object") {
|
|
2049
2050
|
const candidate = value;
|
|
2050
2051
|
if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
|
|
2051
|
-
return parseBoundedPagePayload(value, label);
|
|
2052
|
+
return parseBoundedPagePayload(value, label, normalizeBoundedReadOptions(options));
|
|
2052
2053
|
}
|
|
2053
2054
|
}
|
|
2054
2055
|
if (!Array.isArray(legacyItems)) {
|
|
@@ -2058,13 +2059,72 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
|
2058
2059
|
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
2059
2060
|
return { ...page, source_bounded: false };
|
|
2060
2061
|
}
|
|
2062
|
+
|
|
2063
|
+
class CollectionChangedWhilePagingError extends Error {
|
|
2064
|
+
constructor(label, detail) {
|
|
2065
|
+
super(`${label} changed while paging: ${detail}`);
|
|
2066
|
+
this.name = "CollectionChangedWhilePagingError";
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
var COLLECTION_READ_ATTEMPTS = 2;
|
|
2070
|
+
async function aggregateBoundedCollection(label, readPage, identity, options = {}) {
|
|
2071
|
+
let lastError = null;
|
|
2072
|
+
for (let attempt = 0;attempt < COLLECTION_READ_ATTEMPTS; attempt += 1) {
|
|
2073
|
+
try {
|
|
2074
|
+
const items = [];
|
|
2075
|
+
const seen = new Set;
|
|
2076
|
+
let expectedTotal = null;
|
|
2077
|
+
let cursor = 0;
|
|
2078
|
+
let previousIdentity = null;
|
|
2079
|
+
while (true) {
|
|
2080
|
+
const page = await readPage(cursor);
|
|
2081
|
+
if (page.cursor !== cursor) {
|
|
2082
|
+
throw new CollectionChangedWhilePagingError(label, `server returned cursor ${page.cursor} for requested cursor ${cursor}`);
|
|
2083
|
+
}
|
|
2084
|
+
if (expectedTotal === null)
|
|
2085
|
+
expectedTotal = page.total;
|
|
2086
|
+
else if (page.total !== expectedTotal) {
|
|
2087
|
+
throw new CollectionChangedWhilePagingError(label, `total changed from ${expectedTotal} to ${page.total}`);
|
|
2088
|
+
}
|
|
2089
|
+
for (const item of page.items) {
|
|
2090
|
+
const key = identity(item);
|
|
2091
|
+
if (!key)
|
|
2092
|
+
throw new CollectionChangedWhilePagingError(label, "an item had no stable identity");
|
|
2093
|
+
if (seen.has(key))
|
|
2094
|
+
throw new CollectionChangedWhilePagingError(label, `duplicate identity ${key}`);
|
|
2095
|
+
if (options.requireAscendingIdentity && page.source_bounded && previousIdentity !== null && key <= previousIdentity) {
|
|
2096
|
+
throw new CollectionChangedWhilePagingError(label, `identity order was not strictly increasing at ${key}`);
|
|
2097
|
+
}
|
|
2098
|
+
seen.add(key);
|
|
2099
|
+
previousIdentity = key;
|
|
2100
|
+
items.push(item);
|
|
2101
|
+
}
|
|
2102
|
+
if (page.complete) {
|
|
2103
|
+
if (items.length !== expectedTotal) {
|
|
2104
|
+
throw new CollectionChangedWhilePagingError(label, `received ${items.length} unique rows for total ${expectedTotal}`);
|
|
2105
|
+
}
|
|
2106
|
+
return items;
|
|
2107
|
+
}
|
|
2108
|
+
if (page.next_cursor === null || page.next_cursor <= cursor) {
|
|
2109
|
+
throw new CollectionChangedWhilePagingError(label, `cursor did not advance from ${cursor}`);
|
|
2110
|
+
}
|
|
2111
|
+
cursor = page.next_cursor;
|
|
2112
|
+
}
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
if (!(error instanceof CollectionChangedWhilePagingError))
|
|
2115
|
+
throw error;
|
|
2116
|
+
lastError = error;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
throw lastError ?? new CollectionChangedWhilePagingError(label, "read did not stabilize");
|
|
2120
|
+
}
|
|
2061
2121
|
function isNotFoundHttpError2(err) {
|
|
2062
2122
|
return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && err.status === 404;
|
|
2063
2123
|
}
|
|
2064
2124
|
var localStoreModulePromise = null;
|
|
2065
2125
|
function localStoreModule() {
|
|
2066
2126
|
if (!localStoreModulePromise)
|
|
2067
|
-
localStoreModulePromise = import("./local-
|
|
2127
|
+
localStoreModulePromise = import("./local-5wvv5xv3.js");
|
|
2068
2128
|
return localStoreModulePromise;
|
|
2069
2129
|
}
|
|
2070
2130
|
|
|
@@ -2112,15 +2172,7 @@ class LocalConfigStore {
|
|
|
2112
2172
|
return (await localStoreModule()).pruneSnapshots(configId, keep, this.db);
|
|
2113
2173
|
}
|
|
2114
2174
|
async listProfiles() {
|
|
2115
|
-
|
|
2116
|
-
let cursor = 0;
|
|
2117
|
-
while (true) {
|
|
2118
|
-
const page = await this.listProfilesPage({ limit: 100, cursor });
|
|
2119
|
-
profiles.push(...page.items);
|
|
2120
|
-
if (page.complete)
|
|
2121
|
-
return profiles;
|
|
2122
|
-
cursor = page.next_cursor;
|
|
2123
|
-
}
|
|
2175
|
+
return aggregateBoundedCollection("profile list", (cursor) => this.listProfilesPage({ limit: 100, cursor }), (profile) => profile.id);
|
|
2124
2176
|
}
|
|
2125
2177
|
async listProfilesPage(options = {}) {
|
|
2126
2178
|
return (await localStoreModule()).listProfilesPage(options, this.db);
|
|
@@ -2129,15 +2181,7 @@ class LocalConfigStore {
|
|
|
2129
2181
|
return (await localStoreModule()).getProfile(idOrSlug, this.db);
|
|
2130
2182
|
}
|
|
2131
2183
|
async getProfileConfigs(idOrSlug) {
|
|
2132
|
-
|
|
2133
|
-
let cursor = 0;
|
|
2134
|
-
while (true) {
|
|
2135
|
-
const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
|
|
2136
|
-
configs.push(...page.items);
|
|
2137
|
-
if (page.complete)
|
|
2138
|
-
return configs;
|
|
2139
|
-
cursor = page.next_cursor;
|
|
2140
|
-
}
|
|
2184
|
+
return aggregateBoundedCollection("profile membership", (cursor) => this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor }), (config) => config.id);
|
|
2141
2185
|
}
|
|
2142
2186
|
async getProfileConfigsPage(idOrSlug, options = {}) {
|
|
2143
2187
|
return (await localStoreModule()).getProfileConfigsPage(idOrSlug, options, this.db);
|
|
@@ -2218,7 +2262,8 @@ class CloudConfigStore {
|
|
|
2218
2262
|
throw err;
|
|
2219
2263
|
}
|
|
2220
2264
|
}
|
|
2221
|
-
async
|
|
2265
|
+
async listConfigsPageRemote(filter, options) {
|
|
2266
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
2222
2267
|
const params = new URLSearchParams;
|
|
2223
2268
|
if (filter.category)
|
|
2224
2269
|
params.set("category", filter.category);
|
|
@@ -2228,16 +2273,21 @@ class CloudConfigStore {
|
|
|
2228
2273
|
params.set("kind", filter.kind);
|
|
2229
2274
|
if (filter.search)
|
|
2230
2275
|
params.set("search", filter.search);
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2276
|
+
params.set("limit", String(normalized.limit));
|
|
2277
|
+
params.set("cursor", String(normalized.cursor));
|
|
2278
|
+
const { data } = await this.request("GET", `/configs?${params.toString()}`);
|
|
2279
|
+
return parseBoundedOrLegacyPage(data, data?.configs, normalized, "config list");
|
|
2280
|
+
}
|
|
2281
|
+
async listConfigs(filter = {}) {
|
|
2282
|
+
const configs = await aggregateBoundedCollection("config list", (cursor) => this.listConfigsPageRemote(filter, { limit: 100, cursor }), (config) => config.id, { requireAscendingIdentity: true });
|
|
2283
|
+
let filtered = configs;
|
|
2234
2284
|
if (filter.tags && filter.tags.length > 0) {
|
|
2235
|
-
|
|
2285
|
+
filtered = filtered.filter((config) => filter.tags.every((tag) => config.tags.includes(tag)));
|
|
2236
2286
|
}
|
|
2237
2287
|
if (filter.is_template !== undefined) {
|
|
2238
|
-
|
|
2288
|
+
filtered = filtered.filter((config) => config.is_template === filter.is_template);
|
|
2239
2289
|
}
|
|
2240
|
-
return
|
|
2290
|
+
return filtered;
|
|
2241
2291
|
}
|
|
2242
2292
|
async getConfig(idOrSlug) {
|
|
2243
2293
|
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
@@ -2268,8 +2318,11 @@ class CloudConfigStore {
|
|
|
2268
2318
|
return data ?? { total: 0 };
|
|
2269
2319
|
}
|
|
2270
2320
|
async listSnapshots(configId) {
|
|
2271
|
-
|
|
2272
|
-
|
|
2321
|
+
return aggregateBoundedCollection("snapshot list", async (cursor) => {
|
|
2322
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
2323
|
+
const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots?limit=${normalized.limit}&cursor=${normalized.cursor}`);
|
|
2324
|
+
return parseBoundedOrLegacyPage(data, data?.snapshots, normalized, "snapshot list");
|
|
2325
|
+
}, (snapshot) => snapshot.id);
|
|
2273
2326
|
}
|
|
2274
2327
|
async getSnapshot(id) {
|
|
2275
2328
|
const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
@@ -2292,15 +2345,7 @@ class CloudConfigStore {
|
|
|
2292
2345
|
return data?.pruned ?? 0;
|
|
2293
2346
|
}
|
|
2294
2347
|
async listProfiles() {
|
|
2295
|
-
|
|
2296
|
-
let cursor = 0;
|
|
2297
|
-
while (true) {
|
|
2298
|
-
const page = await this.listProfilesPage({ limit: 100, cursor });
|
|
2299
|
-
profiles.push(...page.items);
|
|
2300
|
-
if (page.complete)
|
|
2301
|
-
return profiles;
|
|
2302
|
-
cursor = page.next_cursor;
|
|
2303
|
-
}
|
|
2348
|
+
return aggregateBoundedCollection("profile list", (cursor) => this.listProfilesPage({ limit: 100, cursor }), (profile) => profile.id, { requireAscendingIdentity: true });
|
|
2304
2349
|
}
|
|
2305
2350
|
async listProfilesPage(options = {}) {
|
|
2306
2351
|
const normalized = normalizeBoundedReadOptions(options);
|
|
@@ -2324,15 +2369,7 @@ class CloudConfigStore {
|
|
|
2324
2369
|
return profile;
|
|
2325
2370
|
}
|
|
2326
2371
|
async getProfileConfigs(idOrSlug) {
|
|
2327
|
-
|
|
2328
|
-
let cursor = 0;
|
|
2329
|
-
while (true) {
|
|
2330
|
-
const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
|
|
2331
|
-
configs.push(...page.items);
|
|
2332
|
-
if (page.complete)
|
|
2333
|
-
return configs;
|
|
2334
|
-
cursor = page.next_cursor;
|
|
2335
|
-
}
|
|
2372
|
+
return aggregateBoundedCollection("profile membership", (cursor) => this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor }), (config) => config.id);
|
|
2336
2373
|
}
|
|
2337
2374
|
async getProfileConfigsPage(idOrSlug, options = {}) {
|
|
2338
2375
|
const normalized = normalizeBoundedReadOptions(options);
|
|
@@ -2346,20 +2383,26 @@ class CloudConfigStore {
|
|
|
2346
2383
|
return parseBoundedOrLegacyPage(data.configs, data.profile.configs, normalized, "profile membership");
|
|
2347
2384
|
}
|
|
2348
2385
|
async getProfileConfigBindings(idOrSlug) {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
const
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2386
|
+
let routeMissing = false;
|
|
2387
|
+
const bindings = await aggregateBoundedCollection("profile config bindings", async (cursor) => {
|
|
2388
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
2389
|
+
const { status, data } = await this.requestProfileRoute(idOrSlug, (profileId) => `/profiles/${encodeURIComponent(profileId)}/bindings?limit=${normalized.limit}&cursor=${normalized.cursor}`, (value) => Boolean(value && (Array.isArray(value.bindings) || Array.isArray(value.items))));
|
|
2390
|
+
if (status === 404) {
|
|
2391
|
+
routeMissing = true;
|
|
2392
|
+
return { ...boundedReadPage([], 0, normalized), source_bounded: false };
|
|
2393
|
+
}
|
|
2394
|
+
return parseBoundedOrLegacyPage(data, data?.bindings, normalized, "profile config bindings");
|
|
2395
|
+
}, (binding) => `${binding.profile_id}\x00${binding.config_id}`);
|
|
2396
|
+
if (!routeMissing)
|
|
2397
|
+
return bindings;
|
|
2398
|
+
const profile = await this.getProfile(idOrSlug);
|
|
2399
|
+
const configs = await this.getProfileConfigs(idOrSlug);
|
|
2400
|
+
return configs.map((config, sort_order) => ({
|
|
2401
|
+
profile_id: profile.id,
|
|
2402
|
+
config_id: config.id,
|
|
2403
|
+
sort_order,
|
|
2404
|
+
binding: legacyProfileConfigBinding()
|
|
2405
|
+
}));
|
|
2363
2406
|
}
|
|
2364
2407
|
async requestProfileRoute(idOrSlug, pathForId, isUsable) {
|
|
2365
2408
|
const attempted = new Set;
|
|
@@ -2417,14 +2460,20 @@ class CloudConfigStore {
|
|
|
2417
2460
|
await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
|
|
2418
2461
|
}
|
|
2419
2462
|
async getProfileAssetBindings(profileIdOrSlug) {
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2463
|
+
let routeMissing = false;
|
|
2464
|
+
const assets = await aggregateBoundedCollection("profile asset bindings", async (cursor) => {
|
|
2465
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
2466
|
+
const { status, data } = await this.requestProfileRoute(profileIdOrSlug, (profileId) => `/profiles/${encodeURIComponent(profileId)}/assets?limit=${normalized.limit}&cursor=${normalized.cursor}`, (value) => Boolean(value && (Array.isArray(value.assets) || Array.isArray(value.items))));
|
|
2467
|
+
if (status === 404) {
|
|
2468
|
+
routeMissing = true;
|
|
2469
|
+
return { ...boundedReadPage([], 0, normalized), source_bounded: false };
|
|
2470
|
+
}
|
|
2471
|
+
return parseBoundedOrLegacyPage(data, data?.assets, normalized, "profile asset bindings");
|
|
2472
|
+
}, (asset) => `${asset.profile_id}\x00${asset.binding.assetKey}`);
|
|
2473
|
+
if (!routeMissing)
|
|
2474
|
+
return assets;
|
|
2475
|
+
await this.getProfile(profileIdOrSlug);
|
|
2476
|
+
return [];
|
|
2428
2477
|
}
|
|
2429
2478
|
async addAssetToProfile(profileIdOrSlug, sourceConfigId, binding) {
|
|
2430
2479
|
const { data } = await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/assets`, { source_config_id: sourceConfigId, binding }, { idempotent: true });
|
|
@@ -2494,8 +2543,11 @@ class CloudConfigStore {
|
|
|
2494
2543
|
await this.request("POST", "/machines/applied", { hostname });
|
|
2495
2544
|
}
|
|
2496
2545
|
async listMachines() {
|
|
2497
|
-
|
|
2498
|
-
|
|
2546
|
+
return aggregateBoundedCollection("machine list", async (cursor) => {
|
|
2547
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
2548
|
+
const { data } = await this.request("GET", `/machines?limit=${normalized.limit}&cursor=${normalized.cursor}`);
|
|
2549
|
+
return parseBoundedOrLegacyPage(data, data?.machines, normalized, "machine list");
|
|
2550
|
+
}, (machine) => machine.id, { requireAscendingIdentity: true });
|
|
2499
2551
|
}
|
|
2500
2552
|
async sendFeedback(input) {
|
|
2501
2553
|
await this.request("POST", "/feedback", {
|