omnigateway 0.3.0 → 0.4.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 +172 -1
- package/bin/omni.js +1117 -40
- package/gateway.js +947 -89
- package/package.json +1 -1
- package/public/assets/{Chip-DbKk1ExR.js → Chip-BltJSw8A.js} +5 -5
- package/public/assets/Confirm-gd8Y6snu.js +4 -0
- package/public/assets/CopyValue-Dds-Ow9a.js +27 -0
- package/public/assets/{Field-B7GOsJf8.js → Field-D-14HF0x.js} +11 -11
- package/public/assets/Lamp-BDf7-dMa.js +25 -0
- package/public/assets/Match-CgNxphvS.js +1 -0
- package/public/assets/Meter-stpt3vQu.js +14 -0
- package/public/assets/Modal-Dj3LvMS9.js +82 -0
- package/public/assets/Rack-B4c0dbIb.js +180 -0
- package/public/assets/{Readout--CQnPjoO.js → Readout-Du15WYx0.js} +5 -5
- package/public/assets/{States-CQFkw1UZ.js → States-CxLDlCaw.js} +5 -5
- package/public/assets/{Table-Ba58Wg4-.js → Table-CVDZ69Zx.js} +1 -1
- package/public/assets/Toggle-DMk46RX2.js +39 -0
- package/public/assets/TokenBreakdown-BySwFK6Q.js +31 -0
- package/public/assets/_app-Dx_xnlud.js +1 -0
- package/public/assets/_app.accounts-DvBYnbfS.js +64 -0
- package/public/assets/_app.console-CoaRdkB1.js +44 -0
- package/public/assets/_app.database-CES5ec5g.js +39 -0
- package/public/assets/_app.index-DwOZe1A-.js +62 -0
- package/public/assets/_app.keys-B5HrAu2U.js +76 -0
- package/public/assets/_app.logs-NbfIexz0.js +68 -0
- package/public/assets/_app.models-B1MdFRtQ.js +148 -0
- package/public/assets/_app.plugins._pluginId-ByMziW8p.js +20 -0
- package/public/assets/_app.settings-BkDkGkKF.js +46 -0
- package/public/assets/_app.usage-DNpM8Nub.js +83 -0
- package/public/assets/dist-9ps0Qd15.js +1 -0
- package/public/assets/index-gMv8Qs7m.js +178 -0
- package/public/assets/login-cCXnl34r.js +36 -0
- package/public/assets/matchContext-BOenO3J2.js +1 -0
- package/public/assets/plus-fXfUTt6_.js +1 -0
- package/public/assets/queries-CIOPbRcE.js +142 -0
- package/public/assets/shared-B4o-2quR.js +84 -0
- package/public/assets/{trash-2-DTWWI3vA.js → trash-2-ZPh3619D.js} +1 -1
- package/public/index.html +7 -2
- package/public/shared/chunks/jsx-runtime-CznXPbDH.js +1 -0
- package/public/shared/chunks/react-GitiQ0dt.js +1 -0
- package/public/shared/chunks/react-dom-BTlfKTOo.js +1 -0
- package/public/shared/chunks/rolldown-runtime-CbXtAM7H.js +1 -0
- package/public/shared/jsx-runtime.js +1 -0
- package/public/shared/react-dom-client.js +9 -0
- package/public/shared/react-dom.js +1 -0
- package/public/shared/react-query.js +1 -0
- package/public/shared/react.js +1 -0
- package/public/shared/styled-components.js +7 -0
- package/public/assets/Confirm-C5DYoI93.js +0 -4
- package/public/assets/CopyValue-CXjietiZ.js +0 -27
- package/public/assets/Lamp-QRSv-SqM.js +0 -25
- package/public/assets/Meter-Dngwbpjo.js +0 -14
- package/public/assets/Modal-DfltaSCz.js +0 -82
- package/public/assets/Rack-D1zBw9SW.js +0 -151
- package/public/assets/Toggle-DWrWc9-9.js +0 -39
- package/public/assets/TokenBreakdown-CyHJv0O5.js +0 -31
- package/public/assets/_app-CTDgfstQ.js +0 -1
- package/public/assets/_app.accounts-DP6T8z7I.js +0 -64
- package/public/assets/_app.console-DYr7TCAl.js +0 -44
- package/public/assets/_app.database-BzO1S_VR.js +0 -39
- package/public/assets/_app.index-CBt6b-lA.js +0 -62
- package/public/assets/_app.keys-B8HGsPPS.js +0 -76
- package/public/assets/_app.logs-DKsxa2Su.js +0 -68
- package/public/assets/_app.models-CZD2UpM-.js +0 -148
- package/public/assets/_app.settings-B2C1LQIF.js +0 -46
- package/public/assets/_app.usage-CDWUXtZy.js +0 -83
- package/public/assets/dist-IkJ0qg-4.js +0 -1
- package/public/assets/index-ClU3Xqui.js +0 -186
- package/public/assets/login-E3gh2_Cl.js +0 -36
- package/public/assets/plus-DkO4vG-T.js +0 -1
- package/public/assets/queries-BJHk_wso.js +0 -144
- package/public/assets/shared-Bp35cgFf.js +0 -84
package/bin/omni.js
CHANGED
|
@@ -114,7 +114,7 @@ function formatBytes(bytes) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
// apps/cli/src/run.ts
|
|
117
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
117
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
118
118
|
import { homedir as homedir3 } from "os";
|
|
119
119
|
|
|
120
120
|
// packages/control/src/adminAuth.ts
|
|
@@ -19551,6 +19551,39 @@ CREATE TABLE usage_rollup (
|
|
|
19551
19551
|
-- cannot drift into disagreeing about which rows count.
|
|
19552
19552
|
`;
|
|
19553
19553
|
|
|
19554
|
+
// packages/store/src/sqlite/migrations/011_plugin_migrations.sql
|
|
19555
|
+
var _011_plugin_migrations_default = `-- The plugin migration ledger, kept deliberately apart from core's \`migrations\`.
|
|
19556
|
+
--
|
|
19557
|
+
-- Core's track is a single ascending integer owned by this repository. A plugin
|
|
19558
|
+
-- ships its own numbered migrations and knows nothing about ours, so the two
|
|
19559
|
+
-- cannot share a counter: if they did, installing a plugin would consume numbers
|
|
19560
|
+
-- that core's next migration then collides with, and the collision would surface
|
|
19561
|
+
-- as "migration already applied" on an upgrade rather than as anything legible.
|
|
19562
|
+
-- Core's next migration is 012 regardless of how many migrations any plugin
|
|
19563
|
+
-- ships, and a plugin's version 1 is its own version 1 forever.
|
|
19564
|
+
--
|
|
19565
|
+
-- The primary key is (plugin_id, version) rather than a surrogate id because
|
|
19566
|
+
-- that pair *is* the identity: applying the same version twice is the thing this
|
|
19567
|
+
-- table exists to prevent, and a uniqueness constraint states it once instead of
|
|
19568
|
+
-- leaving it to whichever code path happens to check first.
|
|
19569
|
+
--
|
|
19570
|
+
-- \`applied_at\` is milliseconds, matching every other timestamp in this schema.
|
|
19571
|
+
-- It is recorded for an operator reading the table during an incident, not read
|
|
19572
|
+
-- by the host: the host asks only whether a row exists.
|
|
19573
|
+
--
|
|
19574
|
+
-- Note what is *not* here: no row for the plugin's tables, and no cascade. A
|
|
19575
|
+
-- plugin's tables outlive its rows here on purpose \u2014 see \`PluginRepo.dropAll\`
|
|
19576
|
+
-- and \`orphanTables\`. Dropping a table because a plugin is currently absent
|
|
19577
|
+
-- destroys data irreversibly, and a restore is precisely the moment a plugin is
|
|
19578
|
+
-- most likely to be absent.
|
|
19579
|
+
CREATE TABLE plugin_migrations (
|
|
19580
|
+
plugin_id TEXT NOT NULL,
|
|
19581
|
+
version INTEGER NOT NULL,
|
|
19582
|
+
applied_at INTEGER NOT NULL,
|
|
19583
|
+
PRIMARY KEY (plugin_id, version)
|
|
19584
|
+
);
|
|
19585
|
+
`;
|
|
19586
|
+
|
|
19554
19587
|
// packages/store/src/sqlite/rollup.ts
|
|
19555
19588
|
var HOUR_MS = 3600000;
|
|
19556
19589
|
function hourOf(at) {
|
|
@@ -19785,7 +19818,8 @@ var MIGRATIONS = [
|
|
|
19785
19818
|
{ id: 7, sql: _007_quota_samples_default },
|
|
19786
19819
|
{ id: 8, sql: _008_body_logging_default },
|
|
19787
19820
|
{ id: 9, sql: _009_key_limits_default },
|
|
19788
|
-
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup }
|
|
19821
|
+
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
|
|
19822
|
+
{ id: 11, sql: _011_plugin_migrations_default }
|
|
19789
19823
|
];
|
|
19790
19824
|
function openDb(path) {
|
|
19791
19825
|
const db = new Database(path, { create: true });
|
|
@@ -19938,6 +19972,168 @@ function createMaintenanceRepo(db) {
|
|
|
19938
19972
|
}
|
|
19939
19973
|
};
|
|
19940
19974
|
}
|
|
19975
|
+
// packages/store/src/sqlite/plugins.ts
|
|
19976
|
+
var PLUGIN_ID = /^[a-z][a-z0-9-]{0,31}$/;
|
|
19977
|
+
var TABLE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
|
|
19978
|
+
var PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
|
|
19979
|
+
var CORE_TABLES = [
|
|
19980
|
+
"api_keys",
|
|
19981
|
+
"credential_health",
|
|
19982
|
+
"credentials",
|
|
19983
|
+
"migrations",
|
|
19984
|
+
"plugin_migrations",
|
|
19985
|
+
"quota_samples",
|
|
19986
|
+
"quota_windows",
|
|
19987
|
+
"request_bodies",
|
|
19988
|
+
"request_logs",
|
|
19989
|
+
"settings",
|
|
19990
|
+
"usage_daily",
|
|
19991
|
+
"usage_rollup",
|
|
19992
|
+
"virtual_models"
|
|
19993
|
+
];
|
|
19994
|
+
var CORE_TABLE_REFERENCE = new RegExp(`\\b(?:${CORE_TABLES.join("|")})\\b`);
|
|
19995
|
+
var PREFIX3 = "plugin_";
|
|
19996
|
+
function stripNoise(sql) {
|
|
19997
|
+
let out = "";
|
|
19998
|
+
let i = 0;
|
|
19999
|
+
while (i < sql.length) {
|
|
20000
|
+
const c = sql[i];
|
|
20001
|
+
const next = sql[i + 1];
|
|
20002
|
+
if (c === "'") {
|
|
20003
|
+
i += 1;
|
|
20004
|
+
while (i < sql.length) {
|
|
20005
|
+
if (sql[i] === "'") {
|
|
20006
|
+
if (sql[i + 1] === "'")
|
|
20007
|
+
i += 2;
|
|
20008
|
+
else
|
|
20009
|
+
break;
|
|
20010
|
+
} else
|
|
20011
|
+
i += 1;
|
|
20012
|
+
}
|
|
20013
|
+
i += 1;
|
|
20014
|
+
out += " ";
|
|
20015
|
+
} else if (c === "-" && next === "-") {
|
|
20016
|
+
while (i < sql.length && sql[i] !== `
|
|
20017
|
+
`)
|
|
20018
|
+
i += 1;
|
|
20019
|
+
out += " ";
|
|
20020
|
+
} else if (c === "/" && next === "*") {
|
|
20021
|
+
i += 2;
|
|
20022
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
|
|
20023
|
+
i += 1;
|
|
20024
|
+
i += 2;
|
|
20025
|
+
out += " ";
|
|
20026
|
+
} else {
|
|
20027
|
+
out += c;
|
|
20028
|
+
i += 1;
|
|
20029
|
+
}
|
|
20030
|
+
}
|
|
20031
|
+
return out;
|
|
20032
|
+
}
|
|
20033
|
+
function tableFor(pluginId, name) {
|
|
20034
|
+
return `"${PREFIX3}${pluginId}_${name}"`;
|
|
20035
|
+
}
|
|
20036
|
+
function prefixFor(pluginId) {
|
|
20037
|
+
return `${PREFIX3}${pluginId}_`;
|
|
20038
|
+
}
|
|
20039
|
+
function assertPluginId(pluginId) {
|
|
20040
|
+
if (!PLUGIN_ID.test(pluginId)) {
|
|
20041
|
+
throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
|
|
20042
|
+
}
|
|
20043
|
+
}
|
|
20044
|
+
function prepare(pluginId, sql) {
|
|
20045
|
+
assertPluginId(pluginId);
|
|
20046
|
+
const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
|
|
20047
|
+
if (!TABLE_NAME.test(name)) {
|
|
20048
|
+
throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
|
|
20049
|
+
}
|
|
20050
|
+
return tableFor(pluginId, name);
|
|
20051
|
+
});
|
|
20052
|
+
const found = CORE_TABLE_REFERENCE.exec(stripNoise(expanded));
|
|
20053
|
+
if (found !== null) {
|
|
20054
|
+
throw new Error(`plugin sql may not reference the core table ${found[0]}`);
|
|
20055
|
+
}
|
|
20056
|
+
return expanded;
|
|
20057
|
+
}
|
|
20058
|
+
function toBinding(value, index) {
|
|
20059
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
|
|
20060
|
+
return value;
|
|
20061
|
+
}
|
|
20062
|
+
throw new Error(`plugin sql parameter ${index} is not a bindable value`);
|
|
20063
|
+
}
|
|
20064
|
+
var toBindings = (params) => (params ?? []).map(toBinding);
|
|
20065
|
+
function createPluginRepo(db) {
|
|
20066
|
+
const pluginTables = () => db.query("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all().map((r) => r.name).filter((name) => name.startsWith(PREFIX3) && name !== "plugin_migrations");
|
|
20067
|
+
const tablesOf = (pluginId) => {
|
|
20068
|
+
const prefix = prefixFor(pluginId);
|
|
20069
|
+
return pluginTables().filter((name) => name.startsWith(prefix));
|
|
20070
|
+
};
|
|
20071
|
+
const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
|
|
20072
|
+
return {
|
|
20073
|
+
migrate(pluginId, migrations) {
|
|
20074
|
+
assertPluginId(pluginId);
|
|
20075
|
+
const done = appliedVersions(pluginId);
|
|
20076
|
+
const applied = [];
|
|
20077
|
+
const ordered = [...migrations].sort((a, b) => a.version - b.version);
|
|
20078
|
+
for (const migration of ordered) {
|
|
20079
|
+
if (done.has(migration.version))
|
|
20080
|
+
continue;
|
|
20081
|
+
try {
|
|
20082
|
+
if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
|
|
20083
|
+
throw new Error("migration version must be a positive integer");
|
|
20084
|
+
}
|
|
20085
|
+
const sql = prepare(pluginId, migration.sql);
|
|
20086
|
+
db.transaction(() => {
|
|
20087
|
+
db.run(sql);
|
|
20088
|
+
db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
|
|
20089
|
+
})();
|
|
20090
|
+
applied.push(migration.version);
|
|
20091
|
+
} catch (error51) {
|
|
20092
|
+
return {
|
|
20093
|
+
applied,
|
|
20094
|
+
failed: {
|
|
20095
|
+
version: migration.version,
|
|
20096
|
+
reason: error51 instanceof Error ? error51.message : String(error51)
|
|
20097
|
+
}
|
|
20098
|
+
};
|
|
20099
|
+
}
|
|
20100
|
+
}
|
|
20101
|
+
return { applied };
|
|
20102
|
+
},
|
|
20103
|
+
run(pluginId, sql, params) {
|
|
20104
|
+
db.run(prepare(pluginId, sql), toBindings(params));
|
|
20105
|
+
},
|
|
20106
|
+
all(pluginId, sql, params) {
|
|
20107
|
+
return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
|
|
20108
|
+
},
|
|
20109
|
+
get(pluginId, sql, params) {
|
|
20110
|
+
const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
|
|
20111
|
+
return row === null || row === undefined ? null : row;
|
|
20112
|
+
},
|
|
20113
|
+
transaction(pluginId, fn) {
|
|
20114
|
+
assertPluginId(pluginId);
|
|
20115
|
+
return db.transaction(fn)();
|
|
20116
|
+
},
|
|
20117
|
+
listTables(pluginId) {
|
|
20118
|
+
assertPluginId(pluginId);
|
|
20119
|
+
return tablesOf(pluginId);
|
|
20120
|
+
},
|
|
20121
|
+
dropAll(pluginId) {
|
|
20122
|
+
assertPluginId(pluginId);
|
|
20123
|
+
const tables = tablesOf(pluginId);
|
|
20124
|
+
db.transaction(() => {
|
|
20125
|
+
for (const table2 of tables)
|
|
20126
|
+
db.run(`DROP TABLE IF EXISTS "${table2}"`);
|
|
20127
|
+
db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
|
|
20128
|
+
})();
|
|
20129
|
+
return tables.length;
|
|
20130
|
+
},
|
|
20131
|
+
orphanTables(installedIds) {
|
|
20132
|
+
const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
|
|
20133
|
+
return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
|
|
20134
|
+
}
|
|
20135
|
+
};
|
|
20136
|
+
}
|
|
19941
20137
|
// packages/rtk/src/catalog.ts
|
|
19942
20138
|
var RTK_FILTER_IDS = [
|
|
19943
20139
|
"git-diff",
|
|
@@ -20250,7 +20446,8 @@ async function createStore(opts) {
|
|
|
20250
20446
|
keys: createKeyRepo(db, logger2),
|
|
20251
20447
|
usage: createUsageRepo(db),
|
|
20252
20448
|
bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
20253
|
-
maintenance: createMaintenanceRepo(db)
|
|
20449
|
+
maintenance: createMaintenanceRepo(db),
|
|
20450
|
+
plugins: createPluginRepo(db)
|
|
20254
20451
|
};
|
|
20255
20452
|
};
|
|
20256
20453
|
let handle = open();
|
|
@@ -20323,6 +20520,22 @@ async function createStore(opts) {
|
|
|
20323
20520
|
snapshotTo: (path) => handle.maintenance.snapshotTo(path),
|
|
20324
20521
|
inspect: (path) => handle.maintenance.inspect(path)
|
|
20325
20522
|
},
|
|
20523
|
+
plugins: {
|
|
20524
|
+
migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
|
|
20525
|
+
run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
|
|
20526
|
+
all(pluginId, sql, params) {
|
|
20527
|
+
return handle.plugins.all(pluginId, sql, params);
|
|
20528
|
+
},
|
|
20529
|
+
get(pluginId, sql, params) {
|
|
20530
|
+
return handle.plugins.get(pluginId, sql, params);
|
|
20531
|
+
},
|
|
20532
|
+
transaction(pluginId, fn) {
|
|
20533
|
+
return handle.plugins.transaction(pluginId, fn);
|
|
20534
|
+
},
|
|
20535
|
+
listTables: (pluginId) => handle.plugins.listTables(pluginId),
|
|
20536
|
+
dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
|
|
20537
|
+
orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
|
|
20538
|
+
},
|
|
20326
20539
|
routing: {
|
|
20327
20540
|
version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
20328
20541
|
subscribe(listener) {
|
|
@@ -20727,6 +20940,13 @@ async function swapIn(deps, candidate) {
|
|
|
20727
20940
|
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
20728
20941
|
});
|
|
20729
20942
|
}
|
|
20943
|
+
try {
|
|
20944
|
+
await deps.reapplyPluginSchema?.();
|
|
20945
|
+
} catch (error51) {
|
|
20946
|
+
(deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
|
|
20947
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
20948
|
+
});
|
|
20949
|
+
}
|
|
20730
20950
|
return {
|
|
20731
20951
|
ok: true,
|
|
20732
20952
|
counts: inspection.counts,
|
|
@@ -21028,13 +21248,594 @@ import {
|
|
|
21028
21248
|
copyFileSync,
|
|
21029
21249
|
mkdirSync,
|
|
21030
21250
|
readdirSync,
|
|
21251
|
+
readFileSync,
|
|
21031
21252
|
realpathSync,
|
|
21032
21253
|
renameSync,
|
|
21033
21254
|
rmSync,
|
|
21034
21255
|
statfsSync,
|
|
21035
|
-
statSync
|
|
21256
|
+
statSync,
|
|
21257
|
+
writeFileSync
|
|
21036
21258
|
} from "fs";
|
|
21037
|
-
import { join as
|
|
21259
|
+
import { join as join4 } from "path";
|
|
21260
|
+
|
|
21261
|
+
// packages/control/src/plugins.ts
|
|
21262
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
21263
|
+
import { basename as basename2, join as join3, resolve as resolve2, sep as sep2 } from "path";
|
|
21264
|
+
|
|
21265
|
+
// packages/plugin-api/src/version.ts
|
|
21266
|
+
var PLUGIN_API_VERSION = 1;
|
|
21267
|
+
var DASHBOARD_SDK_VERSION = "0.1.0";
|
|
21268
|
+
// packages/plugin-api/src/manifest.ts
|
|
21269
|
+
var CAPABILITIES = [
|
|
21270
|
+
"storage",
|
|
21271
|
+
"files",
|
|
21272
|
+
"net:outbound",
|
|
21273
|
+
"events:request",
|
|
21274
|
+
"events:limit"
|
|
21275
|
+
];
|
|
21276
|
+
var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
21277
|
+
var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
|
|
21278
|
+
var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
|
|
21279
|
+
message: "entry must be a relative path with no '..' segment"
|
|
21280
|
+
});
|
|
21281
|
+
var originSchema = exports_external.string().refine((value) => {
|
|
21282
|
+
let url2;
|
|
21283
|
+
try {
|
|
21284
|
+
url2 = new URL(value);
|
|
21285
|
+
} catch {
|
|
21286
|
+
return false;
|
|
21287
|
+
}
|
|
21288
|
+
if (url2.protocol !== "https:" && url2.protocol !== "http:")
|
|
21289
|
+
return false;
|
|
21290
|
+
if (url2.hostname === "")
|
|
21291
|
+
return false;
|
|
21292
|
+
if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
|
|
21293
|
+
return false;
|
|
21294
|
+
return value === url2.origin || value === `${url2.origin}/`;
|
|
21295
|
+
}, { message: "origin must be scheme://host with no path, query or fragment" });
|
|
21296
|
+
var navSchema = exports_external.object({
|
|
21297
|
+
label: exports_external.string().min(1).max(32)
|
|
21298
|
+
}).strict();
|
|
21299
|
+
var manifestSchema = exports_external.object({
|
|
21300
|
+
id: idSchema,
|
|
21301
|
+
name: exports_external.string().min(1).max(64),
|
|
21302
|
+
version: exports_external.string().min(1).max(32),
|
|
21303
|
+
api: exports_external.number().int().nonnegative(),
|
|
21304
|
+
sdk: exports_external.string().min(1).max(32).optional(),
|
|
21305
|
+
server: entrySchema.optional(),
|
|
21306
|
+
ui: entrySchema.optional(),
|
|
21307
|
+
nav: navSchema.optional(),
|
|
21308
|
+
capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
|
|
21309
|
+
origins: exports_external.array(originSchema).min(1).optional(),
|
|
21310
|
+
defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21311
|
+
}).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
|
|
21312
|
+
message: "manifest must declare a server entry, a ui entry, or both"
|
|
21313
|
+
}).refine((m) => m.ui === undefined || m.sdk !== undefined, {
|
|
21314
|
+
message: "a ui entry requires an sdk range"
|
|
21315
|
+
}).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
|
|
21316
|
+
message: "net:outbound requires origins"
|
|
21317
|
+
}).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
|
|
21318
|
+
message: "origins requires the net:outbound capability"
|
|
21319
|
+
});
|
|
21320
|
+
function safeParseManifest(input) {
|
|
21321
|
+
const parsed = manifestSchema.safeParse(input);
|
|
21322
|
+
if (parsed.success)
|
|
21323
|
+
return { ok: true, manifest: parsed.data };
|
|
21324
|
+
const first = parsed.error.issues[0];
|
|
21325
|
+
const path = first?.path.join(".") ?? "";
|
|
21326
|
+
const message = first?.message ?? "invalid manifest";
|
|
21327
|
+
return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
|
|
21328
|
+
}
|
|
21329
|
+
function isApiCompatible(manifest) {
|
|
21330
|
+
return manifest.api === PLUGIN_API_VERSION;
|
|
21331
|
+
}
|
|
21332
|
+
|
|
21333
|
+
// packages/control/src/plugins.ts
|
|
21334
|
+
var PLUGINS_DIRNAME = "plugins";
|
|
21335
|
+
function pluginsDir(root) {
|
|
21336
|
+
return join3(root, PLUGINS_DIRNAME);
|
|
21337
|
+
}
|
|
21338
|
+
var MANIFEST_FILENAME = "omni-plugin.json";
|
|
21339
|
+
var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
|
|
21340
|
+
var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
21341
|
+
var NPM_TARBALL_ROOT = "package";
|
|
21342
|
+
function readManifest(fs, home) {
|
|
21343
|
+
const raw = fs.readText(join3(home, MANIFEST_FILENAME));
|
|
21344
|
+
if (raw === null) {
|
|
21345
|
+
return { check: "manifest", reason: `no ${MANIFEST_FILENAME}`, fatal: true };
|
|
21346
|
+
}
|
|
21347
|
+
let document;
|
|
21348
|
+
try {
|
|
21349
|
+
document = JSON.parse(raw);
|
|
21350
|
+
} catch (error51) {
|
|
21351
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21352
|
+
return {
|
|
21353
|
+
check: "manifest",
|
|
21354
|
+
reason: `${MANIFEST_FILENAME} is not valid JSON: ${detail}`,
|
|
21355
|
+
fatal: true
|
|
21356
|
+
};
|
|
21357
|
+
}
|
|
21358
|
+
const parsed = safeParseManifest(document);
|
|
21359
|
+
if (!parsed.ok)
|
|
21360
|
+
return { check: "manifest", reason: parsed.reason, fatal: true };
|
|
21361
|
+
return parsed.manifest;
|
|
21362
|
+
}
|
|
21363
|
+
function checkEntry(fs, home, entry, label2, fatal) {
|
|
21364
|
+
const target = resolve2(home, entry);
|
|
21365
|
+
if (target !== home && !target.startsWith(`${home}${sep2}`)) {
|
|
21366
|
+
return {
|
|
21367
|
+
check: "entry",
|
|
21368
|
+
reason: `${label2} entry resolves outside the plugin directory`,
|
|
21369
|
+
fatal
|
|
21370
|
+
};
|
|
21371
|
+
}
|
|
21372
|
+
if (!fs.isFile(target)) {
|
|
21373
|
+
return { check: "entry", reason: `${label2} entry ${entry} does not exist`, fatal };
|
|
21374
|
+
}
|
|
21375
|
+
return null;
|
|
21376
|
+
}
|
|
21377
|
+
function verifyPlugin(deps, root, id) {
|
|
21378
|
+
const home = join3(pluginsDir(root), id);
|
|
21379
|
+
if (!ID_PATTERN2.test(id) || !deps.fs.isDirectory(home)) {
|
|
21380
|
+
throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
|
|
21381
|
+
}
|
|
21382
|
+
return reportFor(deps, home, id);
|
|
21383
|
+
}
|
|
21384
|
+
function reportFor(deps, home, id) {
|
|
21385
|
+
const problems = [];
|
|
21386
|
+
const read = readManifest(deps.fs, home);
|
|
21387
|
+
if (!("id" in read)) {
|
|
21388
|
+
problems.push(read);
|
|
21389
|
+
return { id, path: home, manifest: null, problems, loadable: false };
|
|
21390
|
+
}
|
|
21391
|
+
const manifest = read;
|
|
21392
|
+
if (manifest.id !== id) {
|
|
21393
|
+
problems.push({
|
|
21394
|
+
check: "id",
|
|
21395
|
+
reason: `manifest id ${manifest.id} does not match its directory ${id}`,
|
|
21396
|
+
fatal: true
|
|
21397
|
+
});
|
|
21398
|
+
}
|
|
21399
|
+
if (!isApiCompatible(manifest)) {
|
|
21400
|
+
problems.push({
|
|
21401
|
+
check: "api",
|
|
21402
|
+
reason: `plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`,
|
|
21403
|
+
fatal: true
|
|
21404
|
+
});
|
|
21405
|
+
}
|
|
21406
|
+
if (manifest.server !== undefined) {
|
|
21407
|
+
const problem = checkEntry(deps.fs, home, manifest.server, "server", true);
|
|
21408
|
+
if (problem !== null)
|
|
21409
|
+
problems.push(problem);
|
|
21410
|
+
}
|
|
21411
|
+
if (manifest.ui !== undefined) {
|
|
21412
|
+
const problem = checkEntry(deps.fs, home, manifest.ui, "ui", false);
|
|
21413
|
+
if (problem !== null)
|
|
21414
|
+
problems.push(problem);
|
|
21415
|
+
}
|
|
21416
|
+
const sdk = sdkProblem(deps, manifest);
|
|
21417
|
+
if (sdk !== null)
|
|
21418
|
+
problems.push(sdk);
|
|
21419
|
+
return {
|
|
21420
|
+
id,
|
|
21421
|
+
path: home,
|
|
21422
|
+
manifest,
|
|
21423
|
+
problems,
|
|
21424
|
+
loadable: !problems.some((problem) => problem.fatal)
|
|
21425
|
+
};
|
|
21426
|
+
}
|
|
21427
|
+
function sdkProblem(deps, manifest) {
|
|
21428
|
+
if (manifest.ui === undefined || manifest.sdk === undefined)
|
|
21429
|
+
return null;
|
|
21430
|
+
if (deps.sdkVersion === undefined)
|
|
21431
|
+
return null;
|
|
21432
|
+
if (Bun.semver.satisfies(deps.sdkVersion, manifest.sdk))
|
|
21433
|
+
return null;
|
|
21434
|
+
return {
|
|
21435
|
+
check: "sdk",
|
|
21436
|
+
reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}`,
|
|
21437
|
+
fatal: false
|
|
21438
|
+
};
|
|
21439
|
+
}
|
|
21440
|
+
function listPlugins(deps, root) {
|
|
21441
|
+
const dir = pluginsDir(root);
|
|
21442
|
+
const summaries = [];
|
|
21443
|
+
for (const entry of [...deps.fs.readdir(dir)].sort()) {
|
|
21444
|
+
const home = join3(dir, entry);
|
|
21445
|
+
if (!deps.fs.isDirectory(home))
|
|
21446
|
+
continue;
|
|
21447
|
+
if (!ID_PATTERN2.test(entry)) {
|
|
21448
|
+
summaries.push({
|
|
21449
|
+
id: entry,
|
|
21450
|
+
path: home,
|
|
21451
|
+
manifest: null,
|
|
21452
|
+
problems: [
|
|
21453
|
+
{
|
|
21454
|
+
check: "id",
|
|
21455
|
+
reason: `directory name ${entry} is not a valid plugin id`,
|
|
21456
|
+
fatal: true
|
|
21457
|
+
}
|
|
21458
|
+
],
|
|
21459
|
+
loadable: false,
|
|
21460
|
+
name: null,
|
|
21461
|
+
version: null,
|
|
21462
|
+
api: null,
|
|
21463
|
+
sdk: null,
|
|
21464
|
+
capabilities: [],
|
|
21465
|
+
origins: []
|
|
21466
|
+
});
|
|
21467
|
+
continue;
|
|
21468
|
+
}
|
|
21469
|
+
const report = reportFor(deps, home, entry);
|
|
21470
|
+
const manifest = report.manifest;
|
|
21471
|
+
summaries.push({
|
|
21472
|
+
...report,
|
|
21473
|
+
name: manifest?.name ?? null,
|
|
21474
|
+
version: manifest?.version ?? null,
|
|
21475
|
+
api: manifest?.api ?? null,
|
|
21476
|
+
sdk: manifest?.sdk ?? null,
|
|
21477
|
+
capabilities: manifest?.capabilities ?? [],
|
|
21478
|
+
origins: manifest?.origins ?? []
|
|
21479
|
+
});
|
|
21480
|
+
}
|
|
21481
|
+
return summaries;
|
|
21482
|
+
}
|
|
21483
|
+
var BLOCK = 512;
|
|
21484
|
+
var decoder2 = new TextDecoder;
|
|
21485
|
+
function fieldText(header, offset, length) {
|
|
21486
|
+
const slice = header.subarray(offset, offset + length);
|
|
21487
|
+
const end = slice.indexOf(0);
|
|
21488
|
+
return decoder2.decode(end === -1 ? slice : slice.subarray(0, end));
|
|
21489
|
+
}
|
|
21490
|
+
function fieldOctal(header, offset, length) {
|
|
21491
|
+
const text = fieldText(header, offset, length).trim();
|
|
21492
|
+
if (text.length === 0)
|
|
21493
|
+
return 0;
|
|
21494
|
+
const value = Number.parseInt(text, 8);
|
|
21495
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : -1;
|
|
21496
|
+
}
|
|
21497
|
+
function isSafeArchivePath(path) {
|
|
21498
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("\x00"))
|
|
21499
|
+
return false;
|
|
21500
|
+
if (path.includes("\\"))
|
|
21501
|
+
return false;
|
|
21502
|
+
return !path.split("/").includes("..");
|
|
21503
|
+
}
|
|
21504
|
+
function readTar(bytes) {
|
|
21505
|
+
const files = new Map;
|
|
21506
|
+
let total = 0;
|
|
21507
|
+
let longName = null;
|
|
21508
|
+
let offset = 0;
|
|
21509
|
+
while (offset + BLOCK <= bytes.length) {
|
|
21510
|
+
const header = bytes.subarray(offset, offset + BLOCK);
|
|
21511
|
+
if (header.every((byte) => byte === 0))
|
|
21512
|
+
break;
|
|
21513
|
+
const size = fieldOctal(header, 124, 12);
|
|
21514
|
+
if (size < 0)
|
|
21515
|
+
throw new GatewayError("BAD_REQUEST", "archive has an unreadable entry size");
|
|
21516
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
21517
|
+
const name = fieldText(header, 0, 100);
|
|
21518
|
+
const prefix = fieldText(header, 345, 155);
|
|
21519
|
+
offset += BLOCK;
|
|
21520
|
+
total += size;
|
|
21521
|
+
if (total > MAX_PLUGIN_BYTES) {
|
|
21522
|
+
throw new GatewayError("BAD_REQUEST", `archive unpacks to more than ${MAX_PLUGIN_BYTES} bytes`);
|
|
21523
|
+
}
|
|
21524
|
+
if (offset + size > bytes.length) {
|
|
21525
|
+
throw new GatewayError("BAD_REQUEST", `archive is truncated: entry ${name || "(unnamed)"} declares ${size} bytes and the archive ends before them`);
|
|
21526
|
+
}
|
|
21527
|
+
const data = bytes.subarray(offset, offset + size);
|
|
21528
|
+
offset += Math.ceil(size / BLOCK) * BLOCK;
|
|
21529
|
+
if (type === "1" || type === "2") {
|
|
21530
|
+
throw new GatewayError("BAD_REQUEST", `archive contains a link (${name}); plugin packages must be plain files`);
|
|
21531
|
+
}
|
|
21532
|
+
if (type === "L") {
|
|
21533
|
+
longName = decoder2.decode(data).replace(/\0+$/, "");
|
|
21534
|
+
continue;
|
|
21535
|
+
}
|
|
21536
|
+
if (type === "K" || type === "x" || type === "g")
|
|
21537
|
+
continue;
|
|
21538
|
+
if (type !== "0" && type !== "\x00") {
|
|
21539
|
+
longName = null;
|
|
21540
|
+
continue;
|
|
21541
|
+
}
|
|
21542
|
+
const path = longName ?? (prefix.length > 0 ? `${prefix}/${name}` : name);
|
|
21543
|
+
longName = null;
|
|
21544
|
+
if (!isSafeArchivePath(path)) {
|
|
21545
|
+
throw new GatewayError("BAD_REQUEST", `archive contains an unsafe path: ${path}`);
|
|
21546
|
+
}
|
|
21547
|
+
files.set(path, new Uint8Array(data));
|
|
21548
|
+
}
|
|
21549
|
+
return files;
|
|
21550
|
+
}
|
|
21551
|
+
function gunzipIfNeeded(bytes) {
|
|
21552
|
+
if (bytes[0] !== 31 || bytes[1] !== 139)
|
|
21553
|
+
return bytes;
|
|
21554
|
+
try {
|
|
21555
|
+
return Bun.gunzipSync(new Uint8Array(bytes));
|
|
21556
|
+
} catch (error51) {
|
|
21557
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21558
|
+
throw new GatewayError("BAD_REQUEST", `could not decompress the archive: ${detail}`);
|
|
21559
|
+
}
|
|
21560
|
+
}
|
|
21561
|
+
function stripRoot(files) {
|
|
21562
|
+
const roots = new Set;
|
|
21563
|
+
for (const path of files.keys()) {
|
|
21564
|
+
const slash = path.indexOf("/");
|
|
21565
|
+
if (slash <= 0)
|
|
21566
|
+
return { files, claimedId: null };
|
|
21567
|
+
roots.add(path.slice(0, slash));
|
|
21568
|
+
}
|
|
21569
|
+
const [only] = [...roots];
|
|
21570
|
+
if (roots.size !== 1 || only === undefined)
|
|
21571
|
+
return { files, claimedId: null };
|
|
21572
|
+
const stripped = new Map;
|
|
21573
|
+
for (const [path, bytes] of files)
|
|
21574
|
+
stripped.set(path.slice(only.length + 1), bytes);
|
|
21575
|
+
return { files: stripped, claimedId: only === NPM_TARBALL_ROOT ? null : only };
|
|
21576
|
+
}
|
|
21577
|
+
function readTree(fs, dir, prefix = "") {
|
|
21578
|
+
const files = new Map;
|
|
21579
|
+
let total = 0;
|
|
21580
|
+
for (const entry of fs.readdir(dir)) {
|
|
21581
|
+
const path = join3(dir, entry);
|
|
21582
|
+
const relative = prefix.length === 0 ? entry : `${prefix}/${entry}`;
|
|
21583
|
+
if (fs.isDirectory(path)) {
|
|
21584
|
+
for (const [nested, bytes2] of readTree(fs, path, relative))
|
|
21585
|
+
files.set(nested, bytes2);
|
|
21586
|
+
continue;
|
|
21587
|
+
}
|
|
21588
|
+
const bytes = fs.readBytes(path);
|
|
21589
|
+
if (bytes === null)
|
|
21590
|
+
continue;
|
|
21591
|
+
total += bytes.byteLength;
|
|
21592
|
+
if (total > MAX_PLUGIN_BYTES) {
|
|
21593
|
+
throw new GatewayError("BAD_REQUEST", `${dir} holds more than ${MAX_PLUGIN_BYTES} bytes`);
|
|
21594
|
+
}
|
|
21595
|
+
files.set(relative, bytes);
|
|
21596
|
+
}
|
|
21597
|
+
return files;
|
|
21598
|
+
}
|
|
21599
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
21600
|
+
var NPM_PACKUMENT_ACCEPT = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
|
|
21601
|
+
var NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
21602
|
+
var EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
21603
|
+
var SRI_ALGORITHMS = ["sha512", "sha384", "sha256"];
|
|
21604
|
+
function asRecord(value) {
|
|
21605
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
21606
|
+
return null;
|
|
21607
|
+
return value;
|
|
21608
|
+
}
|
|
21609
|
+
function asString(value) {
|
|
21610
|
+
return typeof value === "string" ? value : null;
|
|
21611
|
+
}
|
|
21612
|
+
function parseNpmSpec(spec) {
|
|
21613
|
+
const at = spec.lastIndexOf("@");
|
|
21614
|
+
const name = at <= 0 ? spec : spec.slice(0, at);
|
|
21615
|
+
const version2 = at <= 0 ? null : spec.slice(at + 1);
|
|
21616
|
+
if (name.length > 214 || !NPM_NAME_PATTERN.test(name))
|
|
21617
|
+
return null;
|
|
21618
|
+
return { name, version: version2 };
|
|
21619
|
+
}
|
|
21620
|
+
function registryUrl(deps) {
|
|
21621
|
+
const raw = deps.registry ?? DEFAULT_NPM_REGISTRY;
|
|
21622
|
+
let url2;
|
|
21623
|
+
try {
|
|
21624
|
+
url2 = new URL(raw);
|
|
21625
|
+
} catch {
|
|
21626
|
+
throw new GatewayError("BAD_REQUEST", `registry ${raw} is not a URL`);
|
|
21627
|
+
}
|
|
21628
|
+
if (url2.protocol !== "https:") {
|
|
21629
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; ${raw} is not`);
|
|
21630
|
+
}
|
|
21631
|
+
return url2;
|
|
21632
|
+
}
|
|
21633
|
+
function packumentUrl(registry2, name) {
|
|
21634
|
+
return `${registry2.href.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
|
|
21635
|
+
}
|
|
21636
|
+
function resolveDist(document, spec) {
|
|
21637
|
+
const root = asRecord(document);
|
|
21638
|
+
if (root === null) {
|
|
21639
|
+
throw new GatewayError("BAD_REQUEST", `the registry's answer for ${spec.name} is not a package`);
|
|
21640
|
+
}
|
|
21641
|
+
const versions2 = asRecord(root.versions);
|
|
21642
|
+
if (versions2 === null) {
|
|
21643
|
+
throw new GatewayError("BAD_REQUEST", `the registry lists no versions of ${spec.name}`);
|
|
21644
|
+
}
|
|
21645
|
+
let wanted = spec.version;
|
|
21646
|
+
if (wanted === null) {
|
|
21647
|
+
const tags = asRecord(root["dist-tags"]);
|
|
21648
|
+
wanted = tags === null ? null : asString(tags.latest);
|
|
21649
|
+
if (wanted === null) {
|
|
21650
|
+
throw new GatewayError("BAD_REQUEST", `the registry names no dist-tags.latest for ${spec.name}; install ${spec.name}@<exact version>`);
|
|
21651
|
+
}
|
|
21652
|
+
}
|
|
21653
|
+
const entry = asRecord(versions2[wanted]);
|
|
21654
|
+
if (entry === null) {
|
|
21655
|
+
throw new GatewayError("BAD_REQUEST", `the registry has no version ${wanted} of ${spec.name}`);
|
|
21656
|
+
}
|
|
21657
|
+
const dist = asRecord(entry.dist);
|
|
21658
|
+
const tarball = dist === null ? null : asString(dist.tarball);
|
|
21659
|
+
if (dist === null || tarball === null) {
|
|
21660
|
+
throw new GatewayError("BAD_REQUEST", `the registry's entry for ${spec.name}@${wanted} names no tarball`);
|
|
21661
|
+
}
|
|
21662
|
+
return {
|
|
21663
|
+
version: wanted,
|
|
21664
|
+
dist: { tarball, integrity: asString(dist.integrity), shasum: asString(dist.shasum) }
|
|
21665
|
+
};
|
|
21666
|
+
}
|
|
21667
|
+
function checkTarballUrl(tarball, registry2, what) {
|
|
21668
|
+
let url2;
|
|
21669
|
+
try {
|
|
21670
|
+
url2 = new URL(tarball);
|
|
21671
|
+
} catch {
|
|
21672
|
+
throw new GatewayError("BAD_REQUEST", `the registry gave ${what} a tarball that is not a URL`);
|
|
21673
|
+
}
|
|
21674
|
+
if (url2.protocol !== "https:") {
|
|
21675
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; the registry points ${what} at ${tarball}`);
|
|
21676
|
+
}
|
|
21677
|
+
if (url2.host !== registry2.host) {
|
|
21678
|
+
throw new GatewayError("BAD_REQUEST", `registry ${registry2.host} points ${what} at ${url2.host}; ` + "install that URL directly if it is the one you meant");
|
|
21679
|
+
}
|
|
21680
|
+
}
|
|
21681
|
+
function digestsFor(dist, what) {
|
|
21682
|
+
const digests = [];
|
|
21683
|
+
if (dist.integrity !== null) {
|
|
21684
|
+
for (const entry of dist.integrity.trim().split(/\s+/)) {
|
|
21685
|
+
const dash = entry.indexOf("-");
|
|
21686
|
+
const algorithm = dash === -1 ? "" : entry.slice(0, dash);
|
|
21687
|
+
const value = dash === -1 ? "" : entry.slice(dash + 1);
|
|
21688
|
+
if (value.length === 0)
|
|
21689
|
+
continue;
|
|
21690
|
+
const known = SRI_ALGORITHMS.find((candidate) => candidate === algorithm);
|
|
21691
|
+
if (known === undefined)
|
|
21692
|
+
continue;
|
|
21693
|
+
digests.push({
|
|
21694
|
+
algorithm: known,
|
|
21695
|
+
expected: Buffer2.from(value, "base64").toString("hex"),
|
|
21696
|
+
source: `${known} integrity`
|
|
21697
|
+
});
|
|
21698
|
+
}
|
|
21699
|
+
}
|
|
21700
|
+
if (digests.length === 0 && dist.shasum !== null && /^[0-9a-f]{40}$/i.test(dist.shasum)) {
|
|
21701
|
+
digests.push({ algorithm: "sha1", expected: dist.shasum.toLowerCase(), source: "sha1 shasum" });
|
|
21702
|
+
}
|
|
21703
|
+
if (digests.length === 0) {
|
|
21704
|
+
throw new GatewayError("BAD_REQUEST", `the registry advertises no usable integrity or shasum for ${what}; ` + "refusing to install bytes nothing vouches for");
|
|
21705
|
+
}
|
|
21706
|
+
return digests;
|
|
21707
|
+
}
|
|
21708
|
+
function verifyDigests(bytes, digests, what) {
|
|
21709
|
+
for (const digest of digests) {
|
|
21710
|
+
const actual = new Bun.CryptoHasher(digest.algorithm).update(bytes).digest("hex");
|
|
21711
|
+
if (actual === digest.expected)
|
|
21712
|
+
return;
|
|
21713
|
+
}
|
|
21714
|
+
const first = digests[0];
|
|
21715
|
+
throw new GatewayError("BAD_REQUEST", `${what} does not match the ${first?.source ?? "digest"} the registry advertised; ` + "the download was corrupted or the registry served something else");
|
|
21716
|
+
}
|
|
21717
|
+
async function npmPayload(deps, spec) {
|
|
21718
|
+
if (spec.version !== null && !EXACT_VERSION_PATTERN.test(spec.version)) {
|
|
21719
|
+
throw new GatewayError("BAD_REQUEST", `${spec.name}@${spec.version} is not an exact version; this installer resolves no ranges ` + `or tags \u2014 install ${spec.name}@1.2.3, or ${spec.name} for the registry's latest`);
|
|
21720
|
+
}
|
|
21721
|
+
const fetchBytes = deps.fetchBytes;
|
|
21722
|
+
if (fetchBytes === undefined) {
|
|
21723
|
+
throw new GatewayError("BAD_REQUEST", "this caller cannot install from a registry");
|
|
21724
|
+
}
|
|
21725
|
+
const registry2 = registryUrl(deps);
|
|
21726
|
+
const document = parseJson(await fetchBytes(packumentUrl(registry2, spec.name), NPM_PACKUMENT_ACCEPT), `the registry's answer for ${spec.name}`);
|
|
21727
|
+
const { version: version2, dist } = resolveDist(document, spec);
|
|
21728
|
+
const what = `${spec.name}@${version2}`;
|
|
21729
|
+
checkTarballUrl(dist.tarball, registry2, what);
|
|
21730
|
+
const digests = digestsFor(dist, what);
|
|
21731
|
+
const bytes = await fetchBytes(dist.tarball);
|
|
21732
|
+
verifyDigests(bytes, digests, what);
|
|
21733
|
+
return stripRoot(readTar(gunzipIfNeeded(bytes)));
|
|
21734
|
+
}
|
|
21735
|
+
function parseJson(bytes, what) {
|
|
21736
|
+
try {
|
|
21737
|
+
return JSON.parse(decoder2.decode(bytes));
|
|
21738
|
+
} catch (error51) {
|
|
21739
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21740
|
+
throw new GatewayError("BAD_REQUEST", `${what} is not valid JSON: ${detail}`);
|
|
21741
|
+
}
|
|
21742
|
+
}
|
|
21743
|
+
async function loadPayload(deps, spec) {
|
|
21744
|
+
if (spec.startsWith("http://")) {
|
|
21745
|
+
throw new GatewayError("BAD_REQUEST", "plugins may only be installed over https");
|
|
21746
|
+
}
|
|
21747
|
+
if (spec.startsWith("https://")) {
|
|
21748
|
+
if (deps.fetchBytes === undefined) {
|
|
21749
|
+
throw new GatewayError("BAD_REQUEST", "this caller cannot install from a URL");
|
|
21750
|
+
}
|
|
21751
|
+
return stripRoot(readTar(gunzipIfNeeded(await deps.fetchBytes(spec))));
|
|
21752
|
+
}
|
|
21753
|
+
const source = resolve2(spec);
|
|
21754
|
+
if (deps.fs.isDirectory(source)) {
|
|
21755
|
+
const files = readTree(deps.fs, source);
|
|
21756
|
+
const claimed = basename2(source);
|
|
21757
|
+
return { files, claimedId: ID_PATTERN2.test(claimed) ? claimed : null };
|
|
21758
|
+
}
|
|
21759
|
+
const bytes = deps.fs.readBytes(source);
|
|
21760
|
+
if (bytes !== null)
|
|
21761
|
+
return stripRoot(readTar(gunzipIfNeeded(bytes)));
|
|
21762
|
+
const npm = parseNpmSpec(spec);
|
|
21763
|
+
if (npm !== null)
|
|
21764
|
+
return npmPayload(deps, npm);
|
|
21765
|
+
throw new GatewayError("BAD_REQUEST", `no directory or archive at ${source}, and "${spec}" is not a package name`);
|
|
21766
|
+
}
|
|
21767
|
+
async function installPlugin(deps, root, spec) {
|
|
21768
|
+
const payload = await loadPayload(deps, spec);
|
|
21769
|
+
const raw = payload.files.get(MANIFEST_FILENAME);
|
|
21770
|
+
if (raw === undefined) {
|
|
21771
|
+
throw new GatewayError("BAD_REQUEST", `${spec} has no ${MANIFEST_FILENAME} at its root`);
|
|
21772
|
+
}
|
|
21773
|
+
let document;
|
|
21774
|
+
try {
|
|
21775
|
+
document = JSON.parse(decoder2.decode(raw));
|
|
21776
|
+
} catch (error51) {
|
|
21777
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21778
|
+
throw new GatewayError("BAD_REQUEST", `${MANIFEST_FILENAME} is not valid JSON: ${detail}`);
|
|
21779
|
+
}
|
|
21780
|
+
const parsed = safeParseManifest(document);
|
|
21781
|
+
if (!parsed.ok)
|
|
21782
|
+
throw new GatewayError("BAD_REQUEST", `invalid ${MANIFEST_FILENAME}: ${parsed.reason}`);
|
|
21783
|
+
const manifest = parsed.manifest;
|
|
21784
|
+
const targetId = payload.claimedId ?? manifest.id;
|
|
21785
|
+
if (manifest.id !== targetId) {
|
|
21786
|
+
throw new GatewayError("BAD_REQUEST", `manifest id ${manifest.id} does not match its directory ${targetId}; ` + "rename one of them so the gateway loads what you installed");
|
|
21787
|
+
}
|
|
21788
|
+
const dir = pluginsDir(root);
|
|
21789
|
+
const target = join3(dir, targetId);
|
|
21790
|
+
const replaced = deps.fs.isDirectory(target);
|
|
21791
|
+
const staging = join3(dir, `.staging-${targetId}`);
|
|
21792
|
+
deps.fs.rm(staging);
|
|
21793
|
+
try {
|
|
21794
|
+
for (const [path, bytes] of payload.files) {
|
|
21795
|
+
const destination = join3(staging, path);
|
|
21796
|
+
deps.fs.mkdir(join3(destination, ".."));
|
|
21797
|
+
deps.fs.writeBytes(destination, bytes);
|
|
21798
|
+
}
|
|
21799
|
+
deps.fs.rm(target);
|
|
21800
|
+
deps.fs.rename(staging, target);
|
|
21801
|
+
} finally {
|
|
21802
|
+
deps.fs.rm(staging);
|
|
21803
|
+
}
|
|
21804
|
+
return {
|
|
21805
|
+
id: targetId,
|
|
21806
|
+
name: manifest.name,
|
|
21807
|
+
version: manifest.version,
|
|
21808
|
+
path: target,
|
|
21809
|
+
replaced,
|
|
21810
|
+
restartRequired: true
|
|
21811
|
+
};
|
|
21812
|
+
}
|
|
21813
|
+
function removePlugin(deps, root, id, options = {}) {
|
|
21814
|
+
if (!ID_PATTERN2.test(id)) {
|
|
21815
|
+
throw new GatewayError("BAD_REQUEST", `"${id}" is not a valid plugin id`);
|
|
21816
|
+
}
|
|
21817
|
+
const target = join3(pluginsDir(root), id);
|
|
21818
|
+
const present = deps.fs.isDirectory(target);
|
|
21819
|
+
const purge = options.purge === true;
|
|
21820
|
+
if (purge && deps.store === undefined) {
|
|
21821
|
+
throw new GatewayError("INTERNAL", "purging a plugin's data needs a database");
|
|
21822
|
+
}
|
|
21823
|
+
const tables = purge && deps.store !== undefined ? deps.store.plugins.listTables(id) : [];
|
|
21824
|
+
if (!present && tables.length === 0) {
|
|
21825
|
+
throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
|
|
21826
|
+
}
|
|
21827
|
+
if (present)
|
|
21828
|
+
deps.fs.rm(target);
|
|
21829
|
+
if (purge && deps.store !== undefined)
|
|
21830
|
+
deps.store.plugins.dropAll(id);
|
|
21831
|
+
return { id, removed: present, droppedTables: tables };
|
|
21832
|
+
}
|
|
21833
|
+
function orphanPluginTables(deps, root, store) {
|
|
21834
|
+
const installed = listPlugins(deps, root).map((plugin) => plugin.id);
|
|
21835
|
+
return store.plugins.orphanTables(installed);
|
|
21836
|
+
}
|
|
21837
|
+
|
|
21838
|
+
// packages/control/src/nodeFs.ts
|
|
21038
21839
|
function nodeDatabaseFs() {
|
|
21039
21840
|
const dirBytes = (dir) => {
|
|
21040
21841
|
let entries;
|
|
@@ -21045,7 +21846,7 @@ function nodeDatabaseFs() {
|
|
|
21045
21846
|
}
|
|
21046
21847
|
let total = 0;
|
|
21047
21848
|
for (const entry of entries) {
|
|
21048
|
-
const path =
|
|
21849
|
+
const path = join4(dir, entry.name);
|
|
21049
21850
|
if (entry.isDirectory()) {
|
|
21050
21851
|
total += dirBytes(path);
|
|
21051
21852
|
continue;
|
|
@@ -21098,6 +21899,96 @@ function nodeDatabaseFs() {
|
|
|
21098
21899
|
dirBytes
|
|
21099
21900
|
};
|
|
21100
21901
|
}
|
|
21902
|
+
function nodePluginFs() {
|
|
21903
|
+
return {
|
|
21904
|
+
readdir: (dir) => {
|
|
21905
|
+
try {
|
|
21906
|
+
return readdirSync(dir);
|
|
21907
|
+
} catch {
|
|
21908
|
+
return [];
|
|
21909
|
+
}
|
|
21910
|
+
},
|
|
21911
|
+
readText: (path) => {
|
|
21912
|
+
try {
|
|
21913
|
+
return readFileSync(path, "utf8");
|
|
21914
|
+
} catch {
|
|
21915
|
+
return null;
|
|
21916
|
+
}
|
|
21917
|
+
},
|
|
21918
|
+
readBytes: (path) => {
|
|
21919
|
+
try {
|
|
21920
|
+
return new Uint8Array(readFileSync(path));
|
|
21921
|
+
} catch {
|
|
21922
|
+
return null;
|
|
21923
|
+
}
|
|
21924
|
+
},
|
|
21925
|
+
writeBytes: (path, bytes) => {
|
|
21926
|
+
writeFileSync(path, bytes);
|
|
21927
|
+
},
|
|
21928
|
+
isDirectory: (path) => {
|
|
21929
|
+
try {
|
|
21930
|
+
return statSync(path).isDirectory();
|
|
21931
|
+
} catch {
|
|
21932
|
+
return false;
|
|
21933
|
+
}
|
|
21934
|
+
},
|
|
21935
|
+
isFile: (path) => {
|
|
21936
|
+
try {
|
|
21937
|
+
return statSync(path).isFile();
|
|
21938
|
+
} catch {
|
|
21939
|
+
return false;
|
|
21940
|
+
}
|
|
21941
|
+
},
|
|
21942
|
+
mkdir: (dir) => {
|
|
21943
|
+
mkdirSync(dir, { recursive: true });
|
|
21944
|
+
},
|
|
21945
|
+
rm: (path) => {
|
|
21946
|
+
rmSync(path, { force: true, recursive: true });
|
|
21947
|
+
},
|
|
21948
|
+
rename: (from, to) => renameSync(from, to)
|
|
21949
|
+
};
|
|
21950
|
+
}
|
|
21951
|
+
function nodeFetchBytes(options = {}) {
|
|
21952
|
+
const impl = options.fetchImpl ?? fetch;
|
|
21953
|
+
const maxBytes = options.maxBytes ?? MAX_PLUGIN_BYTES;
|
|
21954
|
+
return async (url2, accept) => {
|
|
21955
|
+
if (!url2.startsWith("https://")) {
|
|
21956
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be fetched over https; ${url2} is not`);
|
|
21957
|
+
}
|
|
21958
|
+
const response = await impl(url2, {
|
|
21959
|
+
headers: accept === undefined ? {} : { accept },
|
|
21960
|
+
redirect: "follow"
|
|
21961
|
+
});
|
|
21962
|
+
if (!response.ok) {
|
|
21963
|
+
throw new GatewayError("BAD_REQUEST", `${url2} answered ${response.status}`);
|
|
21964
|
+
}
|
|
21965
|
+
const reader = response.body?.getReader();
|
|
21966
|
+
if (reader === undefined)
|
|
21967
|
+
return new Uint8Array(0);
|
|
21968
|
+
const chunks = [];
|
|
21969
|
+
let total = 0;
|
|
21970
|
+
for (;; ) {
|
|
21971
|
+
const { done, value } = await reader.read();
|
|
21972
|
+
if (done)
|
|
21973
|
+
break;
|
|
21974
|
+
if (value === undefined)
|
|
21975
|
+
continue;
|
|
21976
|
+
total += value.byteLength;
|
|
21977
|
+
if (total > maxBytes) {
|
|
21978
|
+
await reader.cancel();
|
|
21979
|
+
throw new GatewayError("BAD_REQUEST", `${url2} sent more than ${maxBytes} bytes`);
|
|
21980
|
+
}
|
|
21981
|
+
chunks.push(value);
|
|
21982
|
+
}
|
|
21983
|
+
const bytes = new Uint8Array(total);
|
|
21984
|
+
let offset = 0;
|
|
21985
|
+
for (const chunk of chunks) {
|
|
21986
|
+
bytes.set(chunk, offset);
|
|
21987
|
+
offset += chunk.byteLength;
|
|
21988
|
+
}
|
|
21989
|
+
return bytes;
|
|
21990
|
+
};
|
|
21991
|
+
}
|
|
21101
21992
|
// packages/control/src/oauth/pkce.ts
|
|
21102
21993
|
function randomToken() {
|
|
21103
21994
|
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url");
|
|
@@ -22305,9 +23196,9 @@ function requirePositional(parsed, index, name) {
|
|
|
22305
23196
|
}
|
|
22306
23197
|
|
|
22307
23198
|
// apps/cli/src/context.ts
|
|
22308
|
-
import { existsSync, readFileSync } from "fs";
|
|
23199
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
22309
23200
|
import { homedir } from "os";
|
|
22310
|
-
import { isAbsolute, join as
|
|
23201
|
+
import { isAbsolute, join as join5, resolve as resolve3 } from "path";
|
|
22311
23202
|
class CliError extends Error {
|
|
22312
23203
|
exitCode;
|
|
22313
23204
|
constructor(message, exitCode = 1) {
|
|
@@ -22316,21 +23207,21 @@ class CliError extends Error {
|
|
|
22316
23207
|
this.exitCode = exitCode;
|
|
22317
23208
|
}
|
|
22318
23209
|
}
|
|
22319
|
-
var DEFAULT_ROOT =
|
|
23210
|
+
var DEFAULT_ROOT = join5(homedir(), ".config", "omnigateway");
|
|
22320
23211
|
function looksLikeRoot(dir) {
|
|
22321
|
-
return existsSync(
|
|
23212
|
+
return existsSync(join5(dir, ".env")) || existsSync(join5(dir, "omnigateway.db")) || existsSync(join5(dir, "apps", "gateway", "src", "index.ts"));
|
|
22322
23213
|
}
|
|
22323
23214
|
function resolveRoot(flags, env2, cwd) {
|
|
22324
23215
|
const withEnvFile = (root, source) => {
|
|
22325
|
-
const envFile =
|
|
23216
|
+
const envFile = join5(root, ".env");
|
|
22326
23217
|
return { root, source, envFile: existsSync(envFile) ? envFile : null };
|
|
22327
23218
|
};
|
|
22328
23219
|
if (flags.root !== undefined && flags.root.length > 0) {
|
|
22329
|
-
return withEnvFile(
|
|
23220
|
+
return withEnvFile(resolve3(cwd, flags.root), "flag");
|
|
22330
23221
|
}
|
|
22331
23222
|
const fromEnv = env2.OMNI_ROOT;
|
|
22332
23223
|
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
22333
|
-
return withEnvFile(
|
|
23224
|
+
return withEnvFile(resolve3(cwd, fromEnv), "env");
|
|
22334
23225
|
}
|
|
22335
23226
|
if (looksLikeRoot(cwd))
|
|
22336
23227
|
return withEnvFile(cwd, "cwd");
|
|
@@ -22361,7 +23252,7 @@ function createContext(parsed, options = {}) {
|
|
|
22361
23252
|
const processEnv = options.env ?? process.env;
|
|
22362
23253
|
const cwd = options.cwd ?? process.cwd();
|
|
22363
23254
|
const root = resolveRoot({ root: stringFlag(parsed.values, "root") }, processEnv, cwd);
|
|
22364
|
-
const fileEnv = root.envFile === null ? {} : parseEnvFile(
|
|
23255
|
+
const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync2(root.envFile, "utf8"));
|
|
22365
23256
|
const env2 = { ...processEnv, ...fileEnv };
|
|
22366
23257
|
const rawDbFlag = stringFlag(parsed.values, "db");
|
|
22367
23258
|
const dbFlag = rawDbFlag === undefined || rawDbFlag.trim().length === 0 ? undefined : rawDbFlag;
|
|
@@ -22377,7 +23268,7 @@ function createContext(parsed, options = {}) {
|
|
|
22377
23268
|
configError = error51 instanceof Error ? error51.message : "invalid configuration";
|
|
22378
23269
|
}
|
|
22379
23270
|
const configuredPath = dbFlag ?? config2?.databasePath ?? "omnigateway.db";
|
|
22380
|
-
const databasePath = isAbsolute(configuredPath) ? configuredPath :
|
|
23271
|
+
const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve3(root.root, configuredPath);
|
|
22381
23272
|
const warnings = suppressedDbPath === null ? [] : [
|
|
22382
23273
|
`ignoring OMNI_DB_PATH=${suppressedDbPath} from the environment because --root was given; using ${databasePath}`
|
|
22383
23274
|
];
|
|
@@ -22603,21 +23494,21 @@ async function pollUntilAuthorized(flows, start, deps) {
|
|
|
22603
23494
|
}
|
|
22604
23495
|
|
|
22605
23496
|
// apps/cli/src/service.ts
|
|
22606
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as
|
|
23497
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22607
23498
|
import { homedir as homedir2 } from "os";
|
|
22608
|
-
import { dirname as dirname3, join as
|
|
23499
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
22609
23500
|
var UNIT_NAME2 = "omnigateway.service";
|
|
22610
23501
|
function unitPath(scope) {
|
|
22611
|
-
return scope === "system" ?
|
|
23502
|
+
return scope === "system" ? join6("/etc/systemd/system", UNIT_NAME2) : join6(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
|
|
22612
23503
|
}
|
|
22613
23504
|
function systemctlArgs(scope, args) {
|
|
22614
23505
|
return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
|
|
22615
23506
|
}
|
|
22616
23507
|
function pidFile(stateDir) {
|
|
22617
|
-
return
|
|
23508
|
+
return join6(stateDir, "gateway.pid");
|
|
22618
23509
|
}
|
|
22619
23510
|
function logFile(stateDir) {
|
|
22620
|
-
return
|
|
23511
|
+
return join6(stateDir, "gateway.log");
|
|
22621
23512
|
}
|
|
22622
23513
|
function supervisedLogFile(deps) {
|
|
22623
23514
|
const configured = deps.logFile?.trim();
|
|
@@ -22625,8 +23516,8 @@ function supervisedLogFile(deps) {
|
|
|
22625
23516
|
}
|
|
22626
23517
|
function defaultStateDir(env2) {
|
|
22627
23518
|
const xdg = env2.XDG_STATE_HOME;
|
|
22628
|
-
const base = typeof xdg === "string" && xdg.length > 0 ? xdg :
|
|
22629
|
-
return
|
|
23519
|
+
const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join6(homedir2(), ".local", "state");
|
|
23520
|
+
return join6(base, "omnigateway");
|
|
22630
23521
|
}
|
|
22631
23522
|
function unitFile(input) {
|
|
22632
23523
|
return `[Unit]
|
|
@@ -22637,7 +23528,7 @@ Wants=network-online.target
|
|
|
22637
23528
|
[Service]
|
|
22638
23529
|
Type=simple
|
|
22639
23530
|
WorkingDirectory=${input.root}
|
|
22640
|
-
EnvironmentFile=${
|
|
23531
|
+
EnvironmentFile=${join6(input.root, ".env")}
|
|
22641
23532
|
ExecStart=${input.bun} ${input.entrypoint}
|
|
22642
23533
|
Restart=on-failure
|
|
22643
23534
|
RestartSec=2
|
|
@@ -22653,7 +23544,7 @@ function readPid(stateDir) {
|
|
|
22653
23544
|
const file2 = pidFile(stateDir);
|
|
22654
23545
|
if (!existsSync2(file2))
|
|
22655
23546
|
return null;
|
|
22656
|
-
const pid = Number(
|
|
23547
|
+
const pid = Number(readFileSync3(file2, "utf8").trim());
|
|
22657
23548
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
22658
23549
|
}
|
|
22659
23550
|
function livePid(deps) {
|
|
@@ -22719,7 +23610,7 @@ async function start(deps, input) {
|
|
|
22719
23610
|
env: { ...input.env, OMNI_LOG_FILE: file2 },
|
|
22720
23611
|
logFile: file2
|
|
22721
23612
|
});
|
|
22722
|
-
|
|
23613
|
+
writeFileSync2(pidFile(deps.stateDir), `${pid}
|
|
22723
23614
|
`);
|
|
22724
23615
|
return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
|
|
22725
23616
|
}
|
|
@@ -22765,7 +23656,7 @@ async function install(deps, input) {
|
|
|
22765
23656
|
throw new Error(`${path} already exists; pass --force to replace it`);
|
|
22766
23657
|
}
|
|
22767
23658
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
22768
|
-
|
|
23659
|
+
writeFileSync2(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
|
|
22769
23660
|
const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
|
|
22770
23661
|
let enabled = false;
|
|
22771
23662
|
if (input.enable) {
|
|
@@ -23716,6 +24607,168 @@ ${candidates}${excluded}`;
|
|
|
23716
24607
|
});
|
|
23717
24608
|
}
|
|
23718
24609
|
};
|
|
24610
|
+
// packages/plugin-api/src/events.ts
|
|
24611
|
+
var WINDOW_MS2 = {
|
|
24612
|
+
"1m": 60000,
|
|
24613
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
24614
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
24615
|
+
};
|
|
24616
|
+
// apps/cli/src/commands/plugins.ts
|
|
24617
|
+
function pluginDeps() {
|
|
24618
|
+
return { fs: nodePluginFs(), sdkVersion: DASHBOARD_SDK_VERSION };
|
|
24619
|
+
}
|
|
24620
|
+
function installDeps(ctx, args) {
|
|
24621
|
+
const flag = stringFlag(args.values, "registry");
|
|
24622
|
+
const raw = flag ?? ctx.env.OMNI_PLUGIN_REGISTRY;
|
|
24623
|
+
const registry2 = raw === undefined || raw.trim().length === 0 ? undefined : raw.trim();
|
|
24624
|
+
return {
|
|
24625
|
+
...pluginDeps(),
|
|
24626
|
+
fetchBytes: nodeFetchBytes(),
|
|
24627
|
+
...registry2 === undefined ? {} : { registry: registry2 }
|
|
24628
|
+
};
|
|
24629
|
+
}
|
|
24630
|
+
function pluginState(ctx, plugin) {
|
|
24631
|
+
if (!plugin.loadable)
|
|
24632
|
+
return state(ctx, false, "will not load");
|
|
24633
|
+
if (plugin.problems.length > 0)
|
|
24634
|
+
return paint(ctx, "yellow", "loads, with warnings");
|
|
24635
|
+
return state(ctx, true, "ok");
|
|
24636
|
+
}
|
|
24637
|
+
var pluginList = {
|
|
24638
|
+
usage: "plugin list",
|
|
24639
|
+
summary: "List installed plugins and whether the gateway would load each one",
|
|
24640
|
+
async run(_args, { ctx, writer }) {
|
|
24641
|
+
const root = ctx.root.root;
|
|
24642
|
+
const plugins = listPlugins(pluginDeps(), root);
|
|
24643
|
+
emit(ctx, writer, { pluginsDir: pluginsDir(root), plugins }, () => {
|
|
24644
|
+
if (plugins.length === 0) {
|
|
24645
|
+
return `no plugins installed; unpack one into ${pluginsDir(root)} with: omni plugin install <path-or-package>`;
|
|
24646
|
+
}
|
|
24647
|
+
const rows = plugins.map((plugin) => [
|
|
24648
|
+
plugin.id,
|
|
24649
|
+
plugin.name ?? "\u2014",
|
|
24650
|
+
plugin.version ?? "\u2014",
|
|
24651
|
+
plugin.api === null ? "\u2014" : String(plugin.api),
|
|
24652
|
+
plugin.sdk ?? "\u2014",
|
|
24653
|
+
plugin.capabilities.length === 0 ? "\u2014" : plugin.capabilities.join(","),
|
|
24654
|
+
pluginState(ctx, plugin)
|
|
24655
|
+
]);
|
|
24656
|
+
const body2 = table([
|
|
24657
|
+
{ header: "ID" },
|
|
24658
|
+
{ header: "NAME" },
|
|
24659
|
+
{ header: "VERSION" },
|
|
24660
|
+
{ header: "API" },
|
|
24661
|
+
{ header: "SDK" },
|
|
24662
|
+
{ header: "CAPABILITIES" },
|
|
24663
|
+
{ header: "STATE" }
|
|
24664
|
+
], rows);
|
|
24665
|
+
const notes = plugins.flatMap((plugin) => plugin.problems.map((problem) => `${plugin.id}: ${paint(ctx, problem.fatal ? "red" : "yellow", problem.reason)}`));
|
|
24666
|
+
return notes.length === 0 ? body2 : `${body2}
|
|
24667
|
+
|
|
24668
|
+
${notes.join(`
|
|
24669
|
+
`)}`;
|
|
24670
|
+
});
|
|
24671
|
+
}
|
|
24672
|
+
};
|
|
24673
|
+
var pluginVerify = {
|
|
24674
|
+
usage: "plugin verify <id>",
|
|
24675
|
+
summary: "Run every load-time check on one plugin without loading it",
|
|
24676
|
+
async run(args, { ctx, writer }) {
|
|
24677
|
+
const id = requirePositional(args, 0, "plugin id");
|
|
24678
|
+
const report = verifyPlugin(pluginDeps(), ctx.root.root, id);
|
|
24679
|
+
emit(ctx, writer, report, () => {
|
|
24680
|
+
const manifest = report.manifest;
|
|
24681
|
+
const head = fields([
|
|
24682
|
+
["id", report.id],
|
|
24683
|
+
["path", report.path],
|
|
24684
|
+
["name", manifest?.name ?? "\u2014"],
|
|
24685
|
+
["version", manifest?.version ?? "\u2014"],
|
|
24686
|
+
["api", manifest === null ? "\u2014" : String(manifest.api)],
|
|
24687
|
+
["sdk", manifest?.sdk ?? "\u2014"],
|
|
24688
|
+
["server", manifest?.server ?? "\u2014"],
|
|
24689
|
+
["ui", manifest?.ui ?? "\u2014"],
|
|
24690
|
+
[
|
|
24691
|
+
"capabilities",
|
|
24692
|
+
manifest === null || manifest.capabilities.length === 0 ? "\u2014" : manifest.capabilities.join(",")
|
|
24693
|
+
],
|
|
24694
|
+
["origins", manifest?.origins?.join(",") ?? "\u2014"]
|
|
24695
|
+
]);
|
|
24696
|
+
if (report.problems.length === 0) {
|
|
24697
|
+
return `${head}
|
|
24698
|
+
|
|
24699
|
+
${state(ctx, true, "ok")}: this plugin would load`;
|
|
24700
|
+
}
|
|
24701
|
+
const lines = report.problems.map((problem) => ` ${paint(ctx, problem.fatal ? "red" : "yellow", problem.check)} ${problem.reason}`);
|
|
24702
|
+
const verdict = report.loadable ? paint(ctx, "yellow", "this plugin would load, with the warnings above") : state(ctx, false, "this plugin would be skipped at boot");
|
|
24703
|
+
return `${head}
|
|
24704
|
+
|
|
24705
|
+
${lines.join(`
|
|
24706
|
+
`)}
|
|
24707
|
+
|
|
24708
|
+
${verdict}`;
|
|
24709
|
+
});
|
|
24710
|
+
if (!report.loadable)
|
|
24711
|
+
throw new CliError(`plugin "${id}" would not load`);
|
|
24712
|
+
}
|
|
24713
|
+
};
|
|
24714
|
+
var pluginInstall = {
|
|
24715
|
+
usage: "plugin install <path|tarball|https url|package[@version]> [--registry <url>]",
|
|
24716
|
+
summary: "Unpack a plugin into this installation, running nothing from it",
|
|
24717
|
+
options: { registry: { type: "string" } },
|
|
24718
|
+
async run(args, { ctx, writer }) {
|
|
24719
|
+
const spec = requirePositional(args, 0, "plugin directory, tarball, url, or package name");
|
|
24720
|
+
const result = await installPlugin(installDeps(ctx, args), ctx.root.root, spec);
|
|
24721
|
+
emit(ctx, writer, result, () => {
|
|
24722
|
+
note(ctx, writer, paint(ctx, "dim", "no code from the package was executed; verify it before restarting"));
|
|
24723
|
+
const verb = result.replaced ? "replaced" : "installed";
|
|
24724
|
+
return [
|
|
24725
|
+
`${verb} ${result.name} ${result.version} at ${result.path}`,
|
|
24726
|
+
paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart")
|
|
24727
|
+
].join(`
|
|
24728
|
+
`);
|
|
24729
|
+
});
|
|
24730
|
+
}
|
|
24731
|
+
};
|
|
24732
|
+
var pluginRemove = {
|
|
24733
|
+
usage: "plugin remove <id> [--purge]",
|
|
24734
|
+
summary: "Remove a plugin, optionally dropping its stored data too",
|
|
24735
|
+
options: { purge: { type: "boolean" } },
|
|
24736
|
+
async run(args, { ctx, writer, prompt }) {
|
|
24737
|
+
const id = requirePositional(args, 0, "plugin id");
|
|
24738
|
+
const purge = boolFlag(args.values, "purge");
|
|
24739
|
+
const question = purge ? `remove plugin "${id}" AND permanently drop its stored data?` : `remove plugin "${id}"? its stored data is kept`;
|
|
24740
|
+
if (!await prompt.confirm(question))
|
|
24741
|
+
throw new CliError("cancelled");
|
|
24742
|
+
const deps = purge ? { fs: nodePluginFs(), store: await ctx.store() } : { fs: nodePluginFs() };
|
|
24743
|
+
const result = removePlugin(deps, ctx.root.root, id, { purge });
|
|
24744
|
+
emit(ctx, writer, result, () => {
|
|
24745
|
+
const lines = [];
|
|
24746
|
+
lines.push(result.removed ? `removed ${id}` : `no directory for ${id} to remove`);
|
|
24747
|
+
if (purge) {
|
|
24748
|
+
lines.push(result.droppedTables.length === 0 ? "no stored tables to drop" : `dropped ${result.droppedTables.length} table(s): ${result.droppedTables.join(", ")}`);
|
|
24749
|
+
} else {
|
|
24750
|
+
lines.push(paint(ctx, "dim", "stored tables and migration history kept; --purge drops them"));
|
|
24751
|
+
}
|
|
24752
|
+
lines.push(paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart"));
|
|
24753
|
+
return lines.join(`
|
|
24754
|
+
`);
|
|
24755
|
+
});
|
|
24756
|
+
}
|
|
24757
|
+
};
|
|
24758
|
+
function doctorPluginDeps() {
|
|
24759
|
+
return pluginDeps();
|
|
24760
|
+
}
|
|
24761
|
+
function pluginDoctorLines(ctx, plugins) {
|
|
24762
|
+
return plugins.map((plugin) => {
|
|
24763
|
+
const version2 = plugin.version === null ? "" : ` ${plugin.version}`;
|
|
24764
|
+
const api2 = plugin.api === null ? "api ?" : `api ${plugin.api}`;
|
|
24765
|
+
const sdk = plugin.sdk === null ? "no ui" : `sdk ${plugin.sdk}`;
|
|
24766
|
+
const reasons = plugin.problems.filter((problem) => problem.fatal).map((p) => p.reason);
|
|
24767
|
+
const warnings = plugin.problems.length - reasons.length;
|
|
24768
|
+
const verdict = reasons.length > 0 ? state(ctx, false, `will not load: ${reasons.join("; ")}`) : warnings > 0 ? paint(ctx, "yellow", `loads, ${warnings} warning(s); see omni plugin verify ${plugin.id}`) : state(ctx, true, "ok");
|
|
24769
|
+
return `${plugin.id}${version2} (${api2}, ${sdk}) ${verdict}`;
|
|
24770
|
+
});
|
|
24771
|
+
}
|
|
23719
24772
|
|
|
23720
24773
|
// apps/cli/src/commands/quota.ts
|
|
23721
24774
|
var WINDOW_ORDER = {
|
|
@@ -23820,7 +24873,7 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
23820
24873
|
|
|
23821
24874
|
// apps/cli/src/runtime.ts
|
|
23822
24875
|
import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
|
|
23823
|
-
import { join as
|
|
24876
|
+
import { join as join7 } from "path";
|
|
23824
24877
|
var runCommand = async (argv) => {
|
|
23825
24878
|
const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
|
|
23826
24879
|
const [stdout, stderr, code] = await Promise.all([
|
|
@@ -23877,9 +24930,9 @@ function processAlive(pid) {
|
|
|
23877
24930
|
}
|
|
23878
24931
|
function gatewayEntrypoint(root, cliDir = import.meta.dir) {
|
|
23879
24932
|
const candidates = [
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
24933
|
+
join7(root, "apps", "gateway", "src", "index.ts"),
|
|
24934
|
+
join7(cliDir, "..", "gateway.js"),
|
|
24935
|
+
join7(cliDir, "gateway.js")
|
|
23883
24936
|
];
|
|
23884
24937
|
return candidates.find((path) => existsSync4(path)) ?? null;
|
|
23885
24938
|
}
|
|
@@ -24016,6 +25069,15 @@ async function rollupState(ctx) {
|
|
|
24016
25069
|
return null;
|
|
24017
25070
|
}
|
|
24018
25071
|
}
|
|
25072
|
+
async function orphanTables(ctx) {
|
|
25073
|
+
if (ctx.configError !== null || !existsSync5(ctx.databasePath))
|
|
25074
|
+
return null;
|
|
25075
|
+
try {
|
|
25076
|
+
return orphanPluginTables(doctorPluginDeps(), ctx.root.root, await ctx.store());
|
|
25077
|
+
} catch {
|
|
25078
|
+
return null;
|
|
25079
|
+
}
|
|
25080
|
+
}
|
|
24019
25081
|
var doctor = {
|
|
24020
25082
|
usage: "doctor",
|
|
24021
25083
|
summary: "Check what this CLI resolved, and whether it can do anything with it",
|
|
@@ -24026,6 +25088,8 @@ var doctor = {
|
|
|
24026
25088
|
const unit = unitInstalled(deps);
|
|
24027
25089
|
const status2 = await status(deps);
|
|
24028
25090
|
const usageRollup = await rollupState(ctx);
|
|
25091
|
+
const plugins = listPlugins(doctorPluginDeps(), deps.root);
|
|
25092
|
+
const orphans = await orphanTables(ctx);
|
|
24029
25093
|
const checks3 = {
|
|
24030
25094
|
root: deps.root,
|
|
24031
25095
|
rootSource: ctx.root.source,
|
|
@@ -24040,6 +25104,9 @@ var doctor = {
|
|
|
24040
25104
|
supervisor: status2.supervisor,
|
|
24041
25105
|
running: status2.running,
|
|
24042
25106
|
consoleSource: consoleSource(deps).source,
|
|
25107
|
+
pluginsDir: pluginsDir(deps.root),
|
|
25108
|
+
plugins,
|
|
25109
|
+
orphanPluginTables: orphans,
|
|
24043
25110
|
warnings: ctx.warnings
|
|
24044
25111
|
};
|
|
24045
25112
|
emit(ctx, writer, checks3, () => {
|
|
@@ -24061,6 +25128,11 @@ var doctor = {
|
|
|
24061
25128
|
["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
|
|
24062
25129
|
["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
|
|
24063
25130
|
["console log", sourceHint(checks3.consoleSource)],
|
|
25131
|
+
...plugins.length === 0 ? [["plugins", paint(ctx, "dim", "none")]] : pluginDoctorLines(ctx, plugins).map((line, index) => [index === 0 ? "plugins" : "", line]),
|
|
25132
|
+
[
|
|
25133
|
+
"orphan plugin tables",
|
|
25134
|
+
orphans === null ? paint(ctx, "dim", "not checked") : orphans.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${orphans.length}: ${orphans.join(", ")}`)
|
|
25135
|
+
],
|
|
24064
25136
|
...checks3.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
|
|
24065
25137
|
]);
|
|
24066
25138
|
});
|
|
@@ -24141,7 +25213,7 @@ var settingsSet = {
|
|
|
24141
25213
|
};
|
|
24142
25214
|
|
|
24143
25215
|
// apps/cli/src/commands/setup.ts
|
|
24144
|
-
import { join as
|
|
25216
|
+
import { join as join8 } from "path";
|
|
24145
25217
|
var OPTIONS = {
|
|
24146
25218
|
dir: { type: "string" },
|
|
24147
25219
|
key: { type: "string" },
|
|
@@ -24173,7 +25245,7 @@ ${f.contents}`).join(`
|
|
|
24173
25245
|
}
|
|
24174
25246
|
}
|
|
24175
25247
|
function at(dir, file2) {
|
|
24176
|
-
return { path:
|
|
25248
|
+
return { path: join8(dir, file2.path), contents: file2.contents };
|
|
24177
25249
|
}
|
|
24178
25250
|
async function promptMapping(models, prompt) {
|
|
24179
25251
|
const choices = models.map(({ model }) => model.id).join(", ");
|
|
@@ -24207,11 +25279,11 @@ var setupClaude = {
|
|
|
24207
25279
|
options: OPTIONS,
|
|
24208
25280
|
async run(args, { ctx, writer, prompt, setupFs }) {
|
|
24209
25281
|
const models = await described(ctx);
|
|
24210
|
-
const dir = stringFlag(args.values, "dir") ??
|
|
25282
|
+
const dir = stringFlag(args.values, "dir") ?? join8(setupFs.homeDir, ".claude");
|
|
24211
25283
|
const key = stringFlag(args.values, "key");
|
|
24212
25284
|
const dryRun2 = boolFlag(args.values, "dry-run");
|
|
24213
25285
|
const mapping = await promptMapping(models, prompt);
|
|
24214
|
-
const path =
|
|
25286
|
+
const path = join8(dir, "settings.json");
|
|
24215
25287
|
const file2 = claudeSettings(models, {
|
|
24216
25288
|
baseUrl: baseUrl(ctx),
|
|
24217
25289
|
discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
|
|
@@ -24466,6 +25538,10 @@ var COMMANDS = {
|
|
|
24466
25538
|
"keys create": keysCreate,
|
|
24467
25539
|
"keys limits": keysLimits,
|
|
24468
25540
|
"keys revoke": keysRevoke,
|
|
25541
|
+
"plugin list": pluginList,
|
|
25542
|
+
"plugin verify": pluginVerify,
|
|
25543
|
+
"plugin install": pluginInstall,
|
|
25544
|
+
"plugin remove": pluginRemove,
|
|
24469
25545
|
"settings get": settingsGet,
|
|
24470
25546
|
"settings set": settingsSet,
|
|
24471
25547
|
"setup claude": setupClaude,
|
|
@@ -24521,6 +25597,7 @@ var GROUPS = [
|
|
|
24521
25597
|
{ title: "Accounts", prefixes: ["connect", "credentials "] },
|
|
24522
25598
|
{ title: "Models", prefixes: ["models "] },
|
|
24523
25599
|
{ title: "Keys", prefixes: ["keys "] },
|
|
25600
|
+
{ title: "Plugins", prefixes: ["plugin "] },
|
|
24524
25601
|
{ title: "Configuration", prefixes: ["settings ", "admin ", "db "] },
|
|
24525
25602
|
{ title: "Reports", prefixes: ["usage", "quota"] }
|
|
24526
25603
|
];
|
|
@@ -24577,10 +25654,10 @@ async function readHidden() {
|
|
|
24577
25654
|
stdin.setRawMode?.(true);
|
|
24578
25655
|
stdin.resume();
|
|
24579
25656
|
try {
|
|
24580
|
-
const
|
|
25657
|
+
const decoder3 = new TextDecoder;
|
|
24581
25658
|
let line = "";
|
|
24582
25659
|
for await (const chunk of stdin) {
|
|
24583
|
-
const text =
|
|
25660
|
+
const text = decoder3.decode(chunk, { stream: true });
|
|
24584
25661
|
for (const char of text) {
|
|
24585
25662
|
if (char === "\r" || char === `
|
|
24586
25663
|
`)
|
|
@@ -24601,10 +25678,10 @@ async function readHidden() {
|
|
|
24601
25678
|
}
|
|
24602
25679
|
}
|
|
24603
25680
|
async function readLine() {
|
|
24604
|
-
const
|
|
25681
|
+
const decoder3 = new TextDecoder;
|
|
24605
25682
|
let line = "";
|
|
24606
25683
|
for await (const chunk of process.stdin) {
|
|
24607
|
-
const text =
|
|
25684
|
+
const text = decoder3.decode(chunk, { stream: true });
|
|
24608
25685
|
const newline = text.indexOf(`
|
|
24609
25686
|
`);
|
|
24610
25687
|
if (newline >= 0)
|
|
@@ -24700,10 +25777,10 @@ async function run(argv, writer, options = {}) {
|
|
|
24700
25777
|
setupFs: options.setupFs ?? {
|
|
24701
25778
|
homeDir: homedir3(),
|
|
24702
25779
|
cwd: process.cwd(),
|
|
24703
|
-
read: (path) => existsSync6(path) ?
|
|
25780
|
+
read: (path) => existsSync6(path) ? readFileSync4(path, "utf8") : null,
|
|
24704
25781
|
write: (path, contents) => atomicWriteFile(path, contents, {
|
|
24705
25782
|
mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
|
|
24706
|
-
write: (temporary, data) =>
|
|
25783
|
+
write: (temporary, data) => writeFileSync3(temporary, data),
|
|
24707
25784
|
rename: renameSync2,
|
|
24708
25785
|
remove: (temporary) => rmSync3(temporary, { force: true })
|
|
24709
25786
|
})
|