@nyte-ai/host 0.0.0-stage → 0.0.18
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/LICENSE +21 -0
- package/README.md +6 -2
- package/THIRD-PARTY-NOTICES.md +800 -0
- package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
- package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
- package/dist/chunks/canvas-preview-0nrnbt76.js +497 -0
- package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
- package/dist/chunks/canvas-preview-1ta3avah.js +56 -0
- package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
- package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
- package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
- package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
- package/dist/chunks/canvas-preview-518dh8by.js +169 -0
- package/dist/chunks/canvas-preview-5hxat7hh.js +239 -0
- package/dist/chunks/canvas-preview-5mex725c.js +14 -0
- package/dist/chunks/canvas-preview-5z63am63.js +295 -0
- package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
- package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
- package/dist/chunks/canvas-preview-75db60xy.js +457 -0
- package/dist/chunks/canvas-preview-76fwvd6e.js +272 -0
- package/dist/chunks/canvas-preview-8xgt67ka.js +598 -0
- package/dist/chunks/canvas-preview-98aj5p34.js +176 -0
- package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
- package/dist/chunks/canvas-preview-a3yw0j70.js +72 -0
- package/dist/chunks/canvas-preview-azg56359.js +68 -0
- package/dist/chunks/canvas-preview-d9wwhbtz.js +240 -0
- package/dist/chunks/canvas-preview-ehf3vp31.js +411 -0
- package/dist/chunks/canvas-preview-evh92ywd.js +369 -0
- package/dist/chunks/canvas-preview-ewgspmsd.js +81 -0
- package/dist/chunks/canvas-preview-g3qj3y3k.js +156 -0
- package/dist/chunks/canvas-preview-h8hshnff.js +63 -0
- package/dist/chunks/canvas-preview-j1tnf51w.js +36 -0
- package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
- package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
- package/dist/chunks/canvas-preview-m02v4yy6.js +386 -0
- package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
- package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
- package/dist/chunks/canvas-preview-rp6fz0w5.js +130 -0
- package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
- package/dist/chunks/canvas-preview-senpjwep.js +518 -0
- package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
- package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
- package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
- package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
- package/dist/chunks/canvas-preview-vax9ne4e.js +341 -0
- package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
- package/dist/chunks/canvas-preview-xn10d4f8.js +608 -0
- package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
- package/dist/chunks/canvas-preview-ymgz0937.js +587 -0
- package/dist/chunks/canvas-preview-yrfz2ak6.js +134 -0
- package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
- package/dist/chunks/canvas-preview-zn2d0k9d.js +133 -0
- package/dist/src/canvas-preview.d.ts +29 -0
- package/dist/src/canvas-preview.js +267 -0
- package/dist/src/credentials.d.ts +44 -0
- package/dist/src/credentials.js +10 -0
- package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
- package/dist/src/deploy/cloudflare/worker.js +196 -0
- package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
- package/dist/src/deploy/vercel/nitro.config.js +30 -0
- package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
- package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
- package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
- package/dist/src/deploy/vercel/server/utils/host.js +7 -0
- package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
- package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
- package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
- package/dist/src/deploy/vercel/server/utils/step.js +6 -0
- package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
- package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
- package/dist/src/durable-object.d.ts +14 -0
- package/dist/src/durable-object.js +231 -0
- package/dist/src/hash.d.ts +1 -0
- package/dist/src/hash.js +6 -0
- package/dist/src/identity-key.d.ts +11 -0
- package/dist/src/identity-key.js +9 -0
- package/dist/src/identity.d.ts +12 -0
- package/dist/src/identity.js +6 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.js +120 -0
- package/dist/src/key-file.d.ts +1 -0
- package/dist/src/key-file.js +7 -0
- package/dist/src/listen-fetch.d.ts +8 -0
- package/dist/src/listen-fetch.js +9 -0
- package/dist/src/listen.d.ts +4 -0
- package/dist/src/listen.js +10 -0
- package/dist/src/local/command.d.ts +15 -0
- package/dist/src/local/command.js +8 -0
- package/dist/src/local/feed.d.ts +7 -0
- package/dist/src/local/feed.js +6 -0
- package/dist/src/local/files.d.ts +18 -0
- package/dist/src/local/files.js +13 -0
- package/dist/src/local/git-watch.d.ts +15 -0
- package/dist/src/local/git-watch.js +7 -0
- package/dist/src/local/git.d.ts +5 -0
- package/dist/src/local/git.js +13 -0
- package/dist/src/local/github.d.ts +30 -0
- package/dist/src/local/github.js +9 -0
- package/dist/src/local/identity.d.ts +3 -0
- package/dist/src/local/identity.js +7 -0
- package/dist/src/local/jobs.d.ts +7 -0
- package/dist/src/local/jobs.js +6 -0
- package/dist/src/local/legacy-trust.d.ts +10 -0
- package/dist/src/local/legacy-trust.js +6 -0
- package/dist/src/local/patch.d.ts +16 -0
- package/dist/src/local/patch.js +17 -0
- package/dist/src/local/provider.d.ts +11 -0
- package/dist/src/local/provider.js +20 -0
- package/dist/src/local/snapshots.d.ts +7 -0
- package/dist/src/local/snapshots.js +11 -0
- package/dist/src/local/text.d.ts +1 -0
- package/dist/src/local/text.js +6 -0
- package/dist/src/local/tree.d.ts +50 -0
- package/dist/src/local/tree.js +16 -0
- package/dist/src/local/workspace-migration.d.ts +2 -0
- package/dist/src/local/workspace-migration.js +7 -0
- package/dist/src/local/worktrees.d.ts +46 -0
- package/dist/src/local/worktrees.js +14 -0
- package/dist/src/mcp/config.d.ts +75 -0
- package/dist/src/mcp/config.js +12 -0
- package/dist/src/mcp/connect.d.ts +28 -0
- package/dist/src/mcp/connect.js +9 -0
- package/dist/src/mcp/hub.d.ts +19 -0
- package/dist/src/mcp/hub.js +11 -0
- package/dist/src/mcp/oauth.d.ts +46 -0
- package/dist/src/mcp/oauth.js +12 -0
- package/dist/src/mcp/tools.d.ts +29 -0
- package/dist/src/mcp/tools.js +12 -0
- package/dist/src/open.d.ts +42 -0
- package/dist/src/open.js +44 -0
- package/dist/src/plugins/loader.d.ts +21 -0
- package/dist/src/plugins/loader.js +11 -0
- package/dist/src/postgres.d.ts +19 -0
- package/dist/src/postgres.js +8 -0
- package/dist/src/providers.d.ts +16 -0
- package/dist/src/providers.js +112 -0
- package/dist/src/secrets.d.ts +12 -0
- package/dist/src/secrets.js +8 -0
- package/dist/src/serve/binding.d.ts +39 -0
- package/dist/src/serve/binding.js +6 -0
- package/dist/src/serve/handler.d.ts +15 -0
- package/dist/src/serve/handler.js +8 -0
- package/dist/src/serve/stream-server.d.ts +6 -0
- package/dist/src/serve/stream-server.js +6 -0
- package/dist/src/session-index.d.ts +11 -0
- package/dist/src/session-index.js +11 -0
- package/dist/src/shell-env.d.ts +14 -0
- package/dist/src/shell-env.js +108 -0
- package/dist/src/sqlite-workers.d.ts +6 -0
- package/dist/src/sqlite-workers.js +6 -0
- package/dist/src/sqlite.d.ts +7 -0
- package/dist/src/sqlite.js +10 -0
- package/dist/src/store-owner.d.ts +15 -0
- package/dist/src/store-owner.js +9 -0
- package/dist/src/store-rows.d.ts +13 -0
- package/dist/src/store-rows.js +26 -0
- package/dist/src/store-usage.d.ts +25 -0
- package/dist/src/store-usage.js +16 -0
- package/dist/src/usage-scan.d.ts +24 -0
- package/dist/src/usage-scan.js +65 -0
- package/dist/src/usage-service.d.ts +22 -0
- package/dist/src/usage-service.js +12 -0
- package/dist/src/usage-summary.d.ts +9 -0
- package/dist/src/usage-summary.js +16 -0
- package/dist/src/usage-types.d.ts +86 -0
- package/dist/src/usage-types.js +0 -0
- package/dist/src/usage-worker.d.ts +1 -0
- package/dist/src/usage-worker.js +25 -0
- package/dist/src/usage.d.ts +72 -0
- package/dist/src/usage.js +13 -0
- package/package.json +132 -3
- package/src/canvas-preview.ts +402 -0
- package/src/credentials.ts +294 -0
- package/src/deploy/cloudflare/README.md +3 -0
- package/src/deploy/cloudflare/worker.ts +306 -0
- package/src/deploy/cloudflare/wrangler.jsonc +12 -0
- package/src/deploy/vercel/README.md +3 -0
- package/src/deploy/vercel/nitro.config.ts +29 -0
- package/src/deploy/vercel/server/routes/[...].ts +10 -0
- package/src/deploy/vercel/server/utils/host.ts +104 -0
- package/src/deploy/vercel/server/utils/repair.ts +35 -0
- package/src/deploy/vercel/server/utils/step.ts +25 -0
- package/src/deploy/vercel/server/workflows/drive.ts +46 -0
- package/src/deploy/vercel/tsconfig.json +9 -0
- package/src/deploy/vercel/types/bun.d.ts +2 -0
- package/src/durable-object.ts +386 -0
- package/src/hash.ts +5 -0
- package/src/identity-key.ts +20 -0
- package/src/identity.ts +66 -0
- package/src/index.ts +21 -0
- package/src/key-file.ts +53 -0
- package/src/listen-fetch.ts +409 -0
- package/src/listen.ts +228 -0
- package/src/local/command.ts +109 -0
- package/src/local/feed.ts +70 -0
- package/src/local/files.ts +812 -0
- package/src/local/git-watch.ts +189 -0
- package/src/local/git.ts +914 -0
- package/src/local/github.ts +692 -0
- package/src/local/identity.ts +21 -0
- package/src/local/jobs.ts +587 -0
- package/src/local/legacy-trust.ts +105 -0
- package/src/local/patch.ts +334 -0
- package/src/local/provider.ts +184 -0
- package/src/local/snapshots.ts +776 -0
- package/src/local/text.ts +9 -0
- package/src/local/tree.ts +398 -0
- package/src/local/workspace-migration.ts +95 -0
- package/src/local/worktrees.ts +239 -0
- package/src/mcp/config.ts +330 -0
- package/src/mcp/connect.ts +105 -0
- package/src/mcp/hub.ts +836 -0
- package/src/mcp/oauth.ts +436 -0
- package/src/mcp/tools.ts +333 -0
- package/src/open.ts +469 -0
- package/src/plugins/loader.ts +186 -0
- package/src/postgres.ts +492 -0
- package/src/providers.ts +135 -0
- package/src/secrets.ts +31 -0
- package/src/serve/binding.ts +628 -0
- package/src/serve/handler.ts +362 -0
- package/src/serve/stream-server.ts +176 -0
- package/src/session-index.ts +99 -0
- package/src/shell-env.ts +153 -0
- package/src/sqlite-workers.ts +283 -0
- package/src/sqlite.ts +528 -0
- package/src/store-owner.ts +98 -0
- package/src/store-rows.ts +110 -0
- package/src/store-usage.ts +344 -0
- package/src/usage-scan.ts +81 -0
- package/src/usage-service.ts +73 -0
- package/src/usage-summary.ts +79 -0
- package/src/usage-types.ts +87 -0
- package/src/usage-worker.ts +24 -0
- package/src/usage.ts +701 -0
package/src/postgres.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { asSeq } from "@nyte-ai/schema";
|
|
2
|
+
import type {
|
|
3
|
+
ObjectId,
|
|
4
|
+
Ref,
|
|
5
|
+
RefUpdate,
|
|
6
|
+
Revision,
|
|
7
|
+
Seq,
|
|
8
|
+
SessionId,
|
|
9
|
+
SetOutcome,
|
|
10
|
+
} from "@nyte-ai/schema";
|
|
11
|
+
import {
|
|
12
|
+
admits,
|
|
13
|
+
fenceNames,
|
|
14
|
+
assertStoreVersion,
|
|
15
|
+
blobId,
|
|
16
|
+
decodeObject,
|
|
17
|
+
collectSessionEvents,
|
|
18
|
+
migrateStore,
|
|
19
|
+
objectId,
|
|
20
|
+
sessionIndexRef,
|
|
21
|
+
parseSessionRef,
|
|
22
|
+
} from "@nyte-ai/core/store";
|
|
23
|
+
import type { Store, StoreConnection } from "@nyte-ai/core";
|
|
24
|
+
import type { MigrationTarget } from "@nyte-ai/core/store";
|
|
25
|
+
import { Pool } from "pg";
|
|
26
|
+
import {
|
|
27
|
+
collect,
|
|
28
|
+
decodeEvent,
|
|
29
|
+
loggedRow,
|
|
30
|
+
memoryReader,
|
|
31
|
+
mintRevision,
|
|
32
|
+
parseNumber,
|
|
33
|
+
prefixEnd,
|
|
34
|
+
parseRef,
|
|
35
|
+
parseStoredObject,
|
|
36
|
+
parseStoredRecord,
|
|
37
|
+
parseText,
|
|
38
|
+
} from "./store-rows.ts";
|
|
39
|
+
import {
|
|
40
|
+
postgresSessionIndexQuery,
|
|
41
|
+
sessionIndexChanges,
|
|
42
|
+
sqlSessionIndex,
|
|
43
|
+
} from "./session-index.ts";
|
|
44
|
+
|
|
45
|
+
/** Returns one pooled client: what `PostgresPool.connect` lends, a query at a time, released once. */
|
|
46
|
+
export type PostgresClient = {
|
|
47
|
+
query(
|
|
48
|
+
text: string,
|
|
49
|
+
values?: readonly unknown[],
|
|
50
|
+
): Promise<{ readonly rows: readonly Record<string, unknown>[] }>;
|
|
51
|
+
release(): void;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Returns the pool `postgresStore` queries through, structurally, so `pg.Pool` and a pglite adapter both fit (§41c). */
|
|
55
|
+
export type PostgresPool = {
|
|
56
|
+
connect(): Promise<PostgresClient>;
|
|
57
|
+
end(): Promise<void>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const MIGRATION_LOCK = "SELECT pg_advisory_xact_lock(hashtextextended('nyte:store', 0))";
|
|
61
|
+
|
|
62
|
+
const SHARED_MIGRATION_LOCK =
|
|
63
|
+
"SELECT pg_advisory_xact_lock_shared(hashtextextended('nyte:store', 0))";
|
|
64
|
+
|
|
65
|
+
/** Returns a `Store` over Postgres: a `pg` `Pool` of its own from `connectionString`, ended by `close()`, or the caller's `pool` (any `PostgresPool`, §41c), left open. The first `open()` brings it to core's `STORE_VERSION` with `migrateStore`, the sqlite store's tables with `bytes` as `BYTEA`, `seq`/`at` as `BIGINT` and `refs.name` collated `"C"` so `list` ranges and orders by code point as the other stores do; each commit is one transaction under an exclusive advisory lock, so instances opening at once serialize, and the open rejects with `SdkError("unavailable")` for a store a newer Nyte migrated. There is no backup. Semantics are the sqlite store's (§41f); each `refs.set` is one transaction on one pooled client that shares the migration's advisory lock, rejects with `SdkError("unavailable")` when a newer Nyte has migrated the store, then takes `pg_advisory_xact_lock` on every ref name and session it touches, in sorted order (a row lock cannot hold an absent ref or a session's next seq), then reads those refs and applies `RefStore.set`, rolling back on any error. Every `open()` returns a connection whose reads go through the pool. */
|
|
66
|
+
export function postgresStore(
|
|
67
|
+
input: { readonly connectionString: string } | { readonly pool: PostgresPool },
|
|
68
|
+
): Store {
|
|
69
|
+
const pool: PostgresPool =
|
|
70
|
+
"pool" in input ? input.pool : new Pool({ connectionString: input.connectionString });
|
|
71
|
+
|
|
72
|
+
let ready: Promise<StoreConnection> | null = null;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
open() {
|
|
76
|
+
ready ??= migrateStore(postgresTarget(pool)).then(
|
|
77
|
+
() => connect(pool),
|
|
78
|
+
(error: unknown) => {
|
|
79
|
+
ready = null;
|
|
80
|
+
|
|
81
|
+
throw error;
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return ready;
|
|
86
|
+
},
|
|
87
|
+
async close() {
|
|
88
|
+
ready = null;
|
|
89
|
+
|
|
90
|
+
if (!("pool" in input)) await pool.end();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function storedVersion(client: PostgresClient): Promise<number> {
|
|
96
|
+
const { rows } = await client.query("SELECT to_regclass('store_meta') IS NOT NULL AS stamped");
|
|
97
|
+
|
|
98
|
+
if (rows[0]?.stamped !== true) return 0;
|
|
99
|
+
const stamp = await client.query("SELECT version FROM store_meta WHERE id = 1");
|
|
100
|
+
const version = stamp.rows[0]?.version;
|
|
101
|
+
|
|
102
|
+
return version === undefined ? 0 : parseNumber(version);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function postgresTarget(pool: PostgresPool): MigrationTarget {
|
|
106
|
+
return {
|
|
107
|
+
dialect: "postgres",
|
|
108
|
+
stamp: () =>
|
|
109
|
+
withClient(pool, async (client) => {
|
|
110
|
+
const version = await storedVersion(client);
|
|
111
|
+
const { rows } = await client.query("SELECT to_regclass('refs') IS NOT NULL AS present");
|
|
112
|
+
|
|
113
|
+
const empty =
|
|
114
|
+
rows[0]?.present !== true ||
|
|
115
|
+
(await client.query("SELECT 1 FROM refs LIMIT 1")).rows.length === 0;
|
|
116
|
+
|
|
117
|
+
return { version, empty };
|
|
118
|
+
}),
|
|
119
|
+
records: async () =>
|
|
120
|
+
(
|
|
121
|
+
await withClient(pool, (client) =>
|
|
122
|
+
client.query(
|
|
123
|
+
"SELECT refs.name, refs.target, refs.revision, objects.body FROM refs JOIN objects ON objects.id = refs.target WHERE objects.body IS NOT NULL",
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
).rows.map(parseStoredRecord),
|
|
127
|
+
async *objects(kind) {
|
|
128
|
+
const { rows } = await withClient(pool, (client) =>
|
|
129
|
+
client.query(
|
|
130
|
+
"SELECT id, body FROM objects WHERE body IS NOT NULL AND strpos(body, $1) > 0",
|
|
131
|
+
[`"kind":${JSON.stringify(kind)}`],
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
yield* rows.map(parseStoredObject);
|
|
136
|
+
},
|
|
137
|
+
async *logs() {
|
|
138
|
+
const sessions = await withClient(pool, async (client) => {
|
|
139
|
+
const { rows } = await client.query("SELECT to_regclass('log') IS NOT NULL AS present");
|
|
140
|
+
|
|
141
|
+
return rows[0]?.present === true
|
|
142
|
+
? (await client.query("SELECT DISTINCT session FROM log")).rows
|
|
143
|
+
: [];
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
for (const row of sessions) {
|
|
147
|
+
const session = parseText(row.session);
|
|
148
|
+
|
|
149
|
+
if (session === null) continue;
|
|
150
|
+
|
|
151
|
+
const { rows } = await withClient(pool, (client) =>
|
|
152
|
+
client.query("SELECT seq, event FROM log WHERE session = $1 ORDER BY seq", [session]),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
yield { session, events: rows.map(loggedRow) };
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
commit: (change) =>
|
|
159
|
+
withClient(pool, async (client) => {
|
|
160
|
+
await client.query("BEGIN");
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await client.query(MIGRATION_LOCK);
|
|
164
|
+
|
|
165
|
+
if ((await storedVersion(client)) !== change.from) {
|
|
166
|
+
await client.query("ROLLBACK");
|
|
167
|
+
|
|
168
|
+
return "moved";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const statement of change.sql) await client.query(statement);
|
|
172
|
+
|
|
173
|
+
for (const moved of change.repoints) {
|
|
174
|
+
await client.query(
|
|
175
|
+
"INSERT INTO objects (id, body) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET body = excluded.body WHERE objects.body IS NULL",
|
|
176
|
+
[moved.id, moved.body],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const { rows } = await client.query(
|
|
180
|
+
"UPDATE refs SET target = $1, revision = $2 WHERE name = $3 AND revision = $4 RETURNING name",
|
|
181
|
+
[moved.id, mintRevision(), moved.name, moved.expect],
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (rows.length === 0) {
|
|
185
|
+
await client.query("ROLLBACK");
|
|
186
|
+
|
|
187
|
+
return "conflict";
|
|
188
|
+
}
|
|
189
|
+
await updateSessionIndex(client, moved.name, moved.id, mintRevision());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const created of change.creates) {
|
|
193
|
+
await client.query(
|
|
194
|
+
"INSERT INTO objects (id, body) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET body = excluded.body WHERE objects.body IS NULL",
|
|
195
|
+
[created.target, created.body],
|
|
196
|
+
);
|
|
197
|
+
const revision = mintRevision();
|
|
198
|
+
|
|
199
|
+
const { rows } = await client.query(
|
|
200
|
+
"INSERT INTO refs (name, target, revision) VALUES ($1, $2, $3) ON CONFLICT (name) DO NOTHING RETURNING name",
|
|
201
|
+
[created.name, created.target, revision],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (rows.length > 0)
|
|
205
|
+
await updateSessionIndex(client, created.name, created.target, revision);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const relog of change.relogs) {
|
|
209
|
+
const { rows } = await client.query(
|
|
210
|
+
"UPDATE log SET event = $1 WHERE session = $2 AND seq = $3 AND event = $4 RETURNING seq",
|
|
211
|
+
[relog.event, relog.session, relog.seq, relog.expect],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (rows.length === 0) {
|
|
215
|
+
await client.query("ROLLBACK");
|
|
216
|
+
|
|
217
|
+
return "conflict";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (change.reindexSessions) {
|
|
222
|
+
const { rows } = await client.query("SELECT name, target, revision FROM refs");
|
|
223
|
+
|
|
224
|
+
for (const row of rows) {
|
|
225
|
+
const ref = parseRef(row);
|
|
226
|
+
await updateSessionIndex(client, ref.name, ref.target, ref.revision);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await client.query(
|
|
231
|
+
"INSERT INTO store_meta (id, version) VALUES (1, $1) ON CONFLICT (id) DO UPDATE SET version = excluded.version",
|
|
232
|
+
[change.to],
|
|
233
|
+
);
|
|
234
|
+
await client.query("COMMIT");
|
|
235
|
+
|
|
236
|
+
return "committed";
|
|
237
|
+
} catch (error) {
|
|
238
|
+
await client.query("ROLLBACK").catch(() => undefined);
|
|
239
|
+
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function withClient<T>(
|
|
247
|
+
pool: PostgresPool,
|
|
248
|
+
work: (client: PostgresClient) => Promise<T>,
|
|
249
|
+
): Promise<T> {
|
|
250
|
+
const client = await pool.connect();
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
return await work(client);
|
|
254
|
+
} finally {
|
|
255
|
+
client.release();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function connect(pool: PostgresPool): StoreConnection {
|
|
260
|
+
const query = (text: string, values?: readonly unknown[]) =>
|
|
261
|
+
withClient(pool, (client) => client.query(text, values));
|
|
262
|
+
|
|
263
|
+
const readTail = async (client: PostgresClient, session: SessionId): Promise<Seq> => {
|
|
264
|
+
const { rows } = await client.query("SELECT MAX(seq) AS tail FROM log WHERE session = $1", [
|
|
265
|
+
session,
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
const value = rows[0]?.tail;
|
|
269
|
+
|
|
270
|
+
return asSeq(value === null || value === undefined ? 0 : parseNumber(value));
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
sessions: sqlSessionIndex(
|
|
275
|
+
async (text, values) => (await query(postgresSessionIndexQuery(text), values)).rows,
|
|
276
|
+
),
|
|
277
|
+
objects: {
|
|
278
|
+
async put(object) {
|
|
279
|
+
const id = await objectId(object);
|
|
280
|
+
await query(
|
|
281
|
+
"INSERT INTO objects (id, body) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET body = excluded.body WHERE objects.body IS NULL",
|
|
282
|
+
[id, JSON.stringify(object)],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return id;
|
|
286
|
+
},
|
|
287
|
+
async get(id) {
|
|
288
|
+
const { rows } = await query("SELECT body FROM objects WHERE id = $1", [id]);
|
|
289
|
+
const body = parseText(rows[0]?.body);
|
|
290
|
+
|
|
291
|
+
return body === null ? null : decodeObject(body);
|
|
292
|
+
},
|
|
293
|
+
async putBlob(bytes) {
|
|
294
|
+
const joined = await collect(bytes);
|
|
295
|
+
const id = await blobId(joined);
|
|
296
|
+
await query(
|
|
297
|
+
"INSERT INTO objects (id, bytes) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET bytes = excluded.bytes WHERE objects.bytes IS NULL",
|
|
298
|
+
[id, joined],
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return id;
|
|
302
|
+
},
|
|
303
|
+
async blob(id) {
|
|
304
|
+
const { rows } = await query("SELECT bytes FROM objects WHERE id = $1", [id]);
|
|
305
|
+
const bytes = rows[0]?.bytes;
|
|
306
|
+
|
|
307
|
+
return bytes instanceof Uint8Array ? memoryReader(new Uint8Array(bytes)) : null;
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
refs: {
|
|
311
|
+
async get(name) {
|
|
312
|
+
const { rows } = await query("SELECT name, target, revision FROM refs WHERE name = $1", [
|
|
313
|
+
name,
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
const row = rows[0];
|
|
317
|
+
|
|
318
|
+
return row === undefined ? null : parseRef(row);
|
|
319
|
+
},
|
|
320
|
+
async list(prefix) {
|
|
321
|
+
const end = prefixEnd(prefix);
|
|
322
|
+
|
|
323
|
+
const { rows } =
|
|
324
|
+
end === null
|
|
325
|
+
? await query(
|
|
326
|
+
"SELECT name, target, revision FROM refs WHERE name >= $1 ORDER BY name",
|
|
327
|
+
[prefix],
|
|
328
|
+
)
|
|
329
|
+
: await query(
|
|
330
|
+
"SELECT name, target, revision FROM refs WHERE name >= $1 AND name < $2 ORDER BY name",
|
|
331
|
+
[prefix, end],
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return rows.map(parseRef);
|
|
335
|
+
},
|
|
336
|
+
set: (updates) => withClient(pool, (client) => publish(client, updates, readTail)),
|
|
337
|
+
async log(session, after, limit) {
|
|
338
|
+
const { rows } = await query(
|
|
339
|
+
"SELECT seq, at, event FROM log WHERE session = $1 AND seq > $2 ORDER BY seq LIMIT $3",
|
|
340
|
+
[session, after, limit],
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
return rows.map((row) =>
|
|
344
|
+
decodeEvent(parseNumber(row.seq), parseNumber(row.at), parseText(row.event) ?? ""),
|
|
345
|
+
);
|
|
346
|
+
},
|
|
347
|
+
tail: (session) => withClient(pool, (client) => readTail(client, session)),
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function publish(
|
|
353
|
+
client: PostgresClient,
|
|
354
|
+
updates: readonly RefUpdate[],
|
|
355
|
+
readTail: (client: PostgresClient, session: SessionId) => Promise<Seq>,
|
|
356
|
+
): Promise<SetOutcome> {
|
|
357
|
+
const logged = collectSessionEvents(updates);
|
|
358
|
+
const fences = fenceNames(updates);
|
|
359
|
+
const names = [...new Set([...updates.map((update) => update.name), ...fences])];
|
|
360
|
+
const sessions = [
|
|
361
|
+
...new Set(
|
|
362
|
+
updates.flatMap(({ name }) => {
|
|
363
|
+
const session = parseSessionRef(name);
|
|
364
|
+
|
|
365
|
+
return session === null ? [] : [session];
|
|
366
|
+
}),
|
|
367
|
+
),
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
const locks = [
|
|
371
|
+
...names.map((name) => `ref:${name}`),
|
|
372
|
+
...sessions.map((id) => `log:${id}`),
|
|
373
|
+
].sort();
|
|
374
|
+
|
|
375
|
+
await client.query("BEGIN");
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
await client.query(SHARED_MIGRATION_LOCK);
|
|
379
|
+
assertStoreVersion(await storedVersion(client));
|
|
380
|
+
|
|
381
|
+
for (const key of locks)
|
|
382
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [key]);
|
|
383
|
+
|
|
384
|
+
const { rows } =
|
|
385
|
+
names.length === 0
|
|
386
|
+
? { rows: [] }
|
|
387
|
+
: await client.query(
|
|
388
|
+
`SELECT name, target, revision FROM refs WHERE name IN (${names.map((_, index) => `$${index + 1}`).join(", ")})`,
|
|
389
|
+
names,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const before = new Map<string, Ref>(
|
|
393
|
+
rows.map((row) => {
|
|
394
|
+
const ref = parseRef(row);
|
|
395
|
+
|
|
396
|
+
return [ref.name, ref];
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const fenced = fences.find((name) => !before.has(name));
|
|
401
|
+
|
|
402
|
+
if (fenced !== undefined) {
|
|
403
|
+
await client.query("ROLLBACK");
|
|
404
|
+
|
|
405
|
+
return { kind: "fenced", name: fenced };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const stale = updates.find((update) => !admits(update, before.get(update.name)));
|
|
409
|
+
|
|
410
|
+
if (stale !== undefined) {
|
|
411
|
+
await client.query("ROLLBACK");
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
kind: "stale",
|
|
415
|
+
name: stale.name,
|
|
416
|
+
revision: before.get(stale.name)?.revision ?? null,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const revisions: Revision[] = [];
|
|
421
|
+
const touched = new Map<SessionId, Revision>();
|
|
422
|
+
|
|
423
|
+
for (const update of updates) {
|
|
424
|
+
const revision = mintRevision();
|
|
425
|
+
revisions.push(revision);
|
|
426
|
+
|
|
427
|
+
if (update.target === null)
|
|
428
|
+
await client.query("DELETE FROM refs WHERE name = $1", [update.name]);
|
|
429
|
+
else
|
|
430
|
+
await client.query(
|
|
431
|
+
"INSERT INTO refs (name, target, revision) VALUES ($1, $2, $3) ON CONFLICT (name) DO UPDATE SET target = excluded.target, revision = excluded.revision",
|
|
432
|
+
[update.name, update.target, revision],
|
|
433
|
+
);
|
|
434
|
+
await updateSessionIndex(client, update.name, update.target, revision);
|
|
435
|
+
const session = parseSessionRef(update.name);
|
|
436
|
+
|
|
437
|
+
if (session !== null) touched.set(session, revision);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const [session, version] of touched)
|
|
441
|
+
await client.query("UPDATE session_index SET version = $1 WHERE session = $2", [
|
|
442
|
+
version,
|
|
443
|
+
session,
|
|
444
|
+
]);
|
|
445
|
+
|
|
446
|
+
const at = Date.now();
|
|
447
|
+
const tails = new Map<SessionId, number>();
|
|
448
|
+
const appended: Seq[] = [];
|
|
449
|
+
|
|
450
|
+
for (const { session, event } of logged) {
|
|
451
|
+
const seq = (tails.get(session) ?? (await readTail(client, session))) + 1;
|
|
452
|
+
tails.set(session, seq);
|
|
453
|
+
await client.query("INSERT INTO log (session, seq, at, event) VALUES ($1, $2, $3, $4)", [
|
|
454
|
+
session,
|
|
455
|
+
seq,
|
|
456
|
+
at,
|
|
457
|
+
JSON.stringify(event),
|
|
458
|
+
]);
|
|
459
|
+
appended.push(asSeq(seq));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
await client.query("COMMIT");
|
|
463
|
+
|
|
464
|
+
return { kind: "set", logged: appended, at, revisions };
|
|
465
|
+
} catch (error) {
|
|
466
|
+
await client.query("ROLLBACK").catch(() => undefined);
|
|
467
|
+
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function updateSessionIndex(
|
|
473
|
+
client: PostgresClient,
|
|
474
|
+
name: string,
|
|
475
|
+
target: ObjectId | null,
|
|
476
|
+
revision: Revision,
|
|
477
|
+
): Promise<void> {
|
|
478
|
+
if (sessionIndexRef(name) === null) return;
|
|
479
|
+
const body =
|
|
480
|
+
target === null
|
|
481
|
+
? null
|
|
482
|
+
: parseText(
|
|
483
|
+
(await client.query("SELECT body FROM objects WHERE id = $1", [target])).rows[0]?.body,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
for (const change of sessionIndexChanges(
|
|
487
|
+
name,
|
|
488
|
+
body === null ? null : decodeObject(body),
|
|
489
|
+
revision,
|
|
490
|
+
))
|
|
491
|
+
await client.query(postgresSessionIndexQuery(change.text), change.values);
|
|
492
|
+
}
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { asId, SdkError } from "@nyte-ai/schema";
|
|
2
|
+
import type { EnvironmentIdentity, WorkspaceTarget } from "@nyte-ai/schema";
|
|
3
|
+
import { closable } from "@nyte-ai/core";
|
|
4
|
+
import { objectId } from "@nyte-ai/core/store";
|
|
5
|
+
import type {
|
|
6
|
+
EnvironmentProvider,
|
|
7
|
+
ExecutionEnv,
|
|
8
|
+
FileBatch,
|
|
9
|
+
FilesApi,
|
|
10
|
+
GitApi,
|
|
11
|
+
SnapshotsApi,
|
|
12
|
+
} from "@nyte-ai/core";
|
|
13
|
+
|
|
14
|
+
/** Returns one booted or joined sandbox, what `sandboxProvider`'s `boot` answers: its identity (the one the provider's `inspect` answers for its target), its working directory, its files over the remote `FilesApi` contract (paths relative to `cwd`, §41j), its processes as jobs, `destroy`, which tears the sandbox down with its files and processes, and the optional `git` and `snapshots` seams of a sandbox that can serve them (§42c, §42n; paths relative to `cwd` too). */
|
|
15
|
+
export type SandboxHandle = Pick<ExecutionEnv, "identity" | "cwd" | "files" | "exec"> & {
|
|
16
|
+
readonly git?: GitApi;
|
|
17
|
+
readonly snapshots?: SnapshotsApi;
|
|
18
|
+
destroy(): Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const CHAT_ONLY: EnvironmentIdentity = asId<"environment-identity">("chat-only");
|
|
22
|
+
|
|
23
|
+
/** Returns the chat-only environment provider (`kind: "chat-only"`), node-free: a workspace with no files and no processes, for a host that only chats (a Durable Object). `inspect` answers any target with identity `"chat-only"`, `available` and no project sources; `attach` answers an `ExecutionEnv` with that identity and `cwd` `/` whose `files` see nothing — `read` answers `missing`, `stat` null, `bytes` throws `SdkError("unknown")`, `scan` and `search` answer `complete` alone, `find` no entries, `watch` yields nothing until its caller returns, `format` answers `unavailable { reason: "unsupported" }` — and refuse every change (`write`, `remove`, `upload` and a batch's `apply` throw `SdkError("unavailable")`), whose `exec` throws `SdkError("unavailable")`, and whose `close` resolves. No `git`, `snapshots`, `browser` or `destroy`. */
|
|
24
|
+
export function chatOnlyProvider(): EnvironmentProvider {
|
|
25
|
+
const env: ExecutionEnv = {
|
|
26
|
+
identity: CHAT_ONLY,
|
|
27
|
+
cwd: "/",
|
|
28
|
+
files: chatOnlyFiles(),
|
|
29
|
+
async exec() {
|
|
30
|
+
throw new SdkError("unavailable", "A chat-only workspace runs no processes.");
|
|
31
|
+
},
|
|
32
|
+
close: async () => undefined,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
kind: "chat-only",
|
|
37
|
+
inspect: async () => ({
|
|
38
|
+
identity: CHAT_ONLY,
|
|
39
|
+
availability: { kind: "available" },
|
|
40
|
+
projectSources: [],
|
|
41
|
+
}),
|
|
42
|
+
attach: async () => env,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns an environment provider over remote sandboxes (`kind` default `"sandbox"`), node-free. `inspect` is `options.inspect`, else it answers the content hash of the target (core's `objectId`) as its identity, `available` and no project sources, booting nothing. `attach(target)` joins the sandbox this provider already booted for the target or boots it with `boot(target)` (one boot in flight per target, keyed by that hash; a failed boot is forgotten so the next attach boots again) and answers an `ExecutionEnv` with the handle's `identity`, `cwd`, `files` and `exec`, whose `close` detaches and leaves the sandbox running. `git` and `snapshots` are on the provider, `(env) => handle.git` and `(env) => handle.snapshots` for the handle behind an environment it attached, found by the environment's identity, which core's wraps pass through (§42c, §42o): a handle that has a seam serves it (`git` `available` to the workspace, snapshots taken around each run), and one without answers `undefined`, which the workspace sees as `git` `unsupported` and no snapshots — the baseline's "required for local and sandbox" unmet for that sandbox. `destroy(target)` calls the handle's `destroy` (joining the sandbox through `boot` first when this provider holds none for the target) and forgets it. No `browser`. */
|
|
47
|
+
export function sandboxProvider(options: {
|
|
48
|
+
readonly kind?: string;
|
|
49
|
+
boot(target: WorkspaceTarget): Promise<SandboxHandle>;
|
|
50
|
+
readonly inspect?: EnvironmentProvider["inspect"];
|
|
51
|
+
}): EnvironmentProvider {
|
|
52
|
+
const booted = new Map<string, Promise<SandboxHandle>>();
|
|
53
|
+
const attached = new Map<EnvironmentIdentity, SandboxHandle>();
|
|
54
|
+
|
|
55
|
+
const boot = async (target: WorkspaceTarget): Promise<SandboxHandle> => {
|
|
56
|
+
const key = await objectId(target);
|
|
57
|
+
const known = booted.get(key);
|
|
58
|
+
|
|
59
|
+
if (known !== undefined) return known;
|
|
60
|
+
const booting = options.boot(target);
|
|
61
|
+
booted.set(key, booting);
|
|
62
|
+
|
|
63
|
+
booting.catch(() => {
|
|
64
|
+
if (booted.get(key) === booting) booted.delete(key);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return booting;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
kind: options.kind ?? "sandbox",
|
|
72
|
+
inspect:
|
|
73
|
+
options.inspect ??
|
|
74
|
+
(async (target) => ({
|
|
75
|
+
identity: asId<"environment-identity">(await objectId(target)),
|
|
76
|
+
availability: { kind: "available" },
|
|
77
|
+
projectSources: [],
|
|
78
|
+
})),
|
|
79
|
+
async attach(target) {
|
|
80
|
+
const handle = await boot(target);
|
|
81
|
+
attached.set(handle.identity, handle);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
identity: handle.identity,
|
|
85
|
+
cwd: handle.cwd,
|
|
86
|
+
files: handle.files,
|
|
87
|
+
exec: (command, run) => handle.exec(command, run),
|
|
88
|
+
close: async () => undefined,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
git: (env) => attached.get(env.identity)?.git,
|
|
92
|
+
snapshots: (env) => attached.get(env.identity)?.snapshots,
|
|
93
|
+
async destroy(target) {
|
|
94
|
+
const key = await objectId(target);
|
|
95
|
+
const handle = await (booted.get(key) ?? options.boot(target));
|
|
96
|
+
booted.delete(key);
|
|
97
|
+
|
|
98
|
+
if (attached.get(handle.identity) === handle) attached.delete(handle.identity);
|
|
99
|
+
await handle.destroy();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function chatOnlyFiles(): FilesApi {
|
|
105
|
+
const refuse = async (): Promise<never> => {
|
|
106
|
+
throw new SdkError("unavailable", "A chat-only workspace has no files to change.");
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const plan: FileBatch = { put: () => plan, remove: () => plan, apply: refuse };
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
read: async (path) => ({ kind: "missing", path }),
|
|
113
|
+
async bytes(path) {
|
|
114
|
+
throw new SdkError("unknown", `No file is at ${path}.`);
|
|
115
|
+
},
|
|
116
|
+
stat: async () => null,
|
|
117
|
+
write: refuse,
|
|
118
|
+
remove: refuse,
|
|
119
|
+
batch: () => plan,
|
|
120
|
+
format: async () => ({ kind: "unavailable", reason: "unsupported" }),
|
|
121
|
+
async *scan() {
|
|
122
|
+
yield { kind: "complete" };
|
|
123
|
+
},
|
|
124
|
+
find: async () => [],
|
|
125
|
+
async *search() {
|
|
126
|
+
yield { kind: "complete" };
|
|
127
|
+
},
|
|
128
|
+
watch: () =>
|
|
129
|
+
closable(() => ({
|
|
130
|
+
pull: () => new Promise<never>(() => undefined),
|
|
131
|
+
close: () => undefined,
|
|
132
|
+
})),
|
|
133
|
+
upload: refuse,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/src/secrets.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { OpenOptions } from "@nyte-ai/core";
|
|
3
|
+
import { readKeyFile } from "./key-file.ts";
|
|
4
|
+
|
|
5
|
+
/** Returns where a host gets the 32-byte key core encrypts secrets with: `OpenOptions.secrets` (decision 10). */
|
|
6
|
+
export type SecretsSource = NonNullable<OpenOptions["secrets"]>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the Node host's default secrets key: `<home>/.nyte/secret.key`, 32
|
|
10
|
+
* raw bytes read or minted by `readKeyFile` (decision 10; §43d as amended by
|
|
11
|
+
* §43i). The source memoises a resolved answer itself; a rejected one is not
|
|
12
|
+
* kept, so the next call reads again (§43k). The key is for secrets only: the
|
|
13
|
+
* host's identity has its own file (§50a). There is no keychain here, and
|
|
14
|
+
* the desktop uses this file as the tui and cli do (§55).
|
|
15
|
+
*/
|
|
16
|
+
export function defaultSecrets(home: string): SecretsSource {
|
|
17
|
+
const path = join(home, ".nyte", "secret.key");
|
|
18
|
+
let key: Promise<Uint8Array> | null = null;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
key: () => {
|
|
22
|
+
const known = key ?? readKeyFile(path, "secrets key");
|
|
23
|
+
key = known;
|
|
24
|
+
known.catch(() => {
|
|
25
|
+
if (key === known) key = null;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return known;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|