omnigateway 0.3.0 → 0.4.1
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 +175 -1
- package/bin/omni.js +1155 -40
- package/gateway.js +985 -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,206 @@ 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`, "i");
|
|
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 === '"' || c === "`" || c === "[") {
|
|
20003
|
+
const close = c === "[" ? "]" : c;
|
|
20004
|
+
out += c;
|
|
20005
|
+
i += 1;
|
|
20006
|
+
while (i < sql.length) {
|
|
20007
|
+
if (sql[i] === close) {
|
|
20008
|
+
if (close !== "]" && sql[i + 1] === close) {
|
|
20009
|
+
out += close + close;
|
|
20010
|
+
i += 2;
|
|
20011
|
+
continue;
|
|
20012
|
+
}
|
|
20013
|
+
break;
|
|
20014
|
+
}
|
|
20015
|
+
out += sql[i];
|
|
20016
|
+
i += 1;
|
|
20017
|
+
}
|
|
20018
|
+
out += close;
|
|
20019
|
+
i += 1;
|
|
20020
|
+
} else if (c === "'") {
|
|
20021
|
+
i += 1;
|
|
20022
|
+
while (i < sql.length) {
|
|
20023
|
+
if (sql[i] === "'") {
|
|
20024
|
+
if (sql[i + 1] === "'")
|
|
20025
|
+
i += 2;
|
|
20026
|
+
else
|
|
20027
|
+
break;
|
|
20028
|
+
} else
|
|
20029
|
+
i += 1;
|
|
20030
|
+
}
|
|
20031
|
+
i += 1;
|
|
20032
|
+
out += " ";
|
|
20033
|
+
} else if (c === "-" && next === "-") {
|
|
20034
|
+
while (i < sql.length && sql[i] !== `
|
|
20035
|
+
`)
|
|
20036
|
+
i += 1;
|
|
20037
|
+
out += " ";
|
|
20038
|
+
} else if (c === "/" && next === "*") {
|
|
20039
|
+
i += 2;
|
|
20040
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
|
|
20041
|
+
i += 1;
|
|
20042
|
+
i += 2;
|
|
20043
|
+
out += " ";
|
|
20044
|
+
} else {
|
|
20045
|
+
out += c;
|
|
20046
|
+
i += 1;
|
|
20047
|
+
}
|
|
20048
|
+
}
|
|
20049
|
+
return out;
|
|
20050
|
+
}
|
|
20051
|
+
function tableFor(pluginId, name) {
|
|
20052
|
+
return `"${PREFIX3}${pluginId}_${name}"`;
|
|
20053
|
+
}
|
|
20054
|
+
function prefixFor(pluginId) {
|
|
20055
|
+
return `${PREFIX3}${pluginId}_`;
|
|
20056
|
+
}
|
|
20057
|
+
function assertPluginId(pluginId) {
|
|
20058
|
+
if (!PLUGIN_ID.test(pluginId)) {
|
|
20059
|
+
throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
|
|
20060
|
+
}
|
|
20061
|
+
}
|
|
20062
|
+
var CONNECTION_STATEMENT = /^\s*(pragma|attach|detach|vacuum)\b/i;
|
|
20063
|
+
function prepare(pluginId, sql) {
|
|
20064
|
+
assertPluginId(pluginId);
|
|
20065
|
+
const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
|
|
20066
|
+
if (!TABLE_NAME.test(name)) {
|
|
20067
|
+
throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
|
|
20068
|
+
}
|
|
20069
|
+
return tableFor(pluginId, name);
|
|
20070
|
+
});
|
|
20071
|
+
const stripped = stripNoise(expanded);
|
|
20072
|
+
const connection = CONNECTION_STATEMENT.exec(stripped);
|
|
20073
|
+
if (connection !== null) {
|
|
20074
|
+
throw new Error(`plugin sql may not ${connection[1]?.toUpperCase()}: the database handle is the gateway's, ` + "and this would reconfigure it for everything sharing it");
|
|
20075
|
+
}
|
|
20076
|
+
const found = CORE_TABLE_REFERENCE.exec(stripped);
|
|
20077
|
+
if (found !== null) {
|
|
20078
|
+
throw new Error(`plugin sql may not reference the core table ${found[0]}`);
|
|
20079
|
+
}
|
|
20080
|
+
return expanded;
|
|
20081
|
+
}
|
|
20082
|
+
function schemaObjects(db) {
|
|
20083
|
+
const rows = db.query("SELECT name FROM sqlite_master").all();
|
|
20084
|
+
return new Set(rows.map((row) => row.name));
|
|
20085
|
+
}
|
|
20086
|
+
function assertOwnNamespace(db, pluginId, before) {
|
|
20087
|
+
const prefix = `${PREFIX3}${pluginId}_`;
|
|
20088
|
+
for (const name of schemaObjects(db)) {
|
|
20089
|
+
if (before.has(name) || name.startsWith(prefix) || name.startsWith("sqlite_"))
|
|
20090
|
+
continue;
|
|
20091
|
+
throw new Error(`plugin migration created ${name}, which is outside this plugin's namespace \u2014 ` + `name it {{\u2026}} so it becomes ${prefix}\u2026`);
|
|
20092
|
+
}
|
|
20093
|
+
}
|
|
20094
|
+
function toBinding(value, index) {
|
|
20095
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
|
|
20096
|
+
return value;
|
|
20097
|
+
}
|
|
20098
|
+
throw new Error(`plugin sql parameter ${index} is not a bindable value`);
|
|
20099
|
+
}
|
|
20100
|
+
var toBindings = (params) => (params ?? []).map(toBinding);
|
|
20101
|
+
function createPluginRepo(db) {
|
|
20102
|
+
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");
|
|
20103
|
+
const tablesOf = (pluginId) => {
|
|
20104
|
+
const prefix = prefixFor(pluginId);
|
|
20105
|
+
return pluginTables().filter((name) => name.startsWith(prefix));
|
|
20106
|
+
};
|
|
20107
|
+
const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
|
|
20108
|
+
return {
|
|
20109
|
+
migrate(pluginId, migrations) {
|
|
20110
|
+
assertPluginId(pluginId);
|
|
20111
|
+
const done = appliedVersions(pluginId);
|
|
20112
|
+
const applied = [];
|
|
20113
|
+
const ordered = [...migrations].sort((a, b) => a.version - b.version);
|
|
20114
|
+
for (const migration of ordered) {
|
|
20115
|
+
if (done.has(migration.version))
|
|
20116
|
+
continue;
|
|
20117
|
+
try {
|
|
20118
|
+
if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
|
|
20119
|
+
throw new Error("migration version must be a positive integer");
|
|
20120
|
+
}
|
|
20121
|
+
const sql = prepare(pluginId, migration.sql);
|
|
20122
|
+
const before = schemaObjects(db);
|
|
20123
|
+
db.transaction(() => {
|
|
20124
|
+
db.run(sql);
|
|
20125
|
+
assertOwnNamespace(db, pluginId, before);
|
|
20126
|
+
db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
|
|
20127
|
+
})();
|
|
20128
|
+
applied.push(migration.version);
|
|
20129
|
+
} catch (error51) {
|
|
20130
|
+
return {
|
|
20131
|
+
applied,
|
|
20132
|
+
failed: {
|
|
20133
|
+
version: migration.version,
|
|
20134
|
+
reason: error51 instanceof Error ? error51.message : String(error51)
|
|
20135
|
+
}
|
|
20136
|
+
};
|
|
20137
|
+
}
|
|
20138
|
+
}
|
|
20139
|
+
return { applied };
|
|
20140
|
+
},
|
|
20141
|
+
run(pluginId, sql, params) {
|
|
20142
|
+
db.run(prepare(pluginId, sql), toBindings(params));
|
|
20143
|
+
},
|
|
20144
|
+
all(pluginId, sql, params) {
|
|
20145
|
+
return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
|
|
20146
|
+
},
|
|
20147
|
+
get(pluginId, sql, params) {
|
|
20148
|
+
const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
|
|
20149
|
+
return row === null || row === undefined ? null : row;
|
|
20150
|
+
},
|
|
20151
|
+
transaction(pluginId, fn) {
|
|
20152
|
+
assertPluginId(pluginId);
|
|
20153
|
+
return db.transaction(fn)();
|
|
20154
|
+
},
|
|
20155
|
+
listTables(pluginId) {
|
|
20156
|
+
assertPluginId(pluginId);
|
|
20157
|
+
return tablesOf(pluginId);
|
|
20158
|
+
},
|
|
20159
|
+
dropAll(pluginId) {
|
|
20160
|
+
assertPluginId(pluginId);
|
|
20161
|
+
const tables = tablesOf(pluginId);
|
|
20162
|
+
db.transaction(() => {
|
|
20163
|
+
for (const table2 of tables)
|
|
20164
|
+
db.run(`DROP TABLE IF EXISTS "${table2}"`);
|
|
20165
|
+
db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
|
|
20166
|
+
})();
|
|
20167
|
+
return tables.length;
|
|
20168
|
+
},
|
|
20169
|
+
orphanTables(installedIds) {
|
|
20170
|
+
const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
|
|
20171
|
+
return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
|
|
20172
|
+
}
|
|
20173
|
+
};
|
|
20174
|
+
}
|
|
19941
20175
|
// packages/rtk/src/catalog.ts
|
|
19942
20176
|
var RTK_FILTER_IDS = [
|
|
19943
20177
|
"git-diff",
|
|
@@ -20250,7 +20484,8 @@ async function createStore(opts) {
|
|
|
20250
20484
|
keys: createKeyRepo(db, logger2),
|
|
20251
20485
|
usage: createUsageRepo(db),
|
|
20252
20486
|
bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
20253
|
-
maintenance: createMaintenanceRepo(db)
|
|
20487
|
+
maintenance: createMaintenanceRepo(db),
|
|
20488
|
+
plugins: createPluginRepo(db)
|
|
20254
20489
|
};
|
|
20255
20490
|
};
|
|
20256
20491
|
let handle = open();
|
|
@@ -20323,6 +20558,22 @@ async function createStore(opts) {
|
|
|
20323
20558
|
snapshotTo: (path) => handle.maintenance.snapshotTo(path),
|
|
20324
20559
|
inspect: (path) => handle.maintenance.inspect(path)
|
|
20325
20560
|
},
|
|
20561
|
+
plugins: {
|
|
20562
|
+
migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
|
|
20563
|
+
run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
|
|
20564
|
+
all(pluginId, sql, params) {
|
|
20565
|
+
return handle.plugins.all(pluginId, sql, params);
|
|
20566
|
+
},
|
|
20567
|
+
get(pluginId, sql, params) {
|
|
20568
|
+
return handle.plugins.get(pluginId, sql, params);
|
|
20569
|
+
},
|
|
20570
|
+
transaction(pluginId, fn) {
|
|
20571
|
+
return handle.plugins.transaction(pluginId, fn);
|
|
20572
|
+
},
|
|
20573
|
+
listTables: (pluginId) => handle.plugins.listTables(pluginId),
|
|
20574
|
+
dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
|
|
20575
|
+
orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
|
|
20576
|
+
},
|
|
20326
20577
|
routing: {
|
|
20327
20578
|
version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
20328
20579
|
subscribe(listener) {
|
|
@@ -20727,6 +20978,13 @@ async function swapIn(deps, candidate) {
|
|
|
20727
20978
|
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
20728
20979
|
});
|
|
20729
20980
|
}
|
|
20981
|
+
try {
|
|
20982
|
+
await deps.reapplyPluginSchema?.();
|
|
20983
|
+
} catch (error51) {
|
|
20984
|
+
(deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
|
|
20985
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
20986
|
+
});
|
|
20987
|
+
}
|
|
20730
20988
|
return {
|
|
20731
20989
|
ok: true,
|
|
20732
20990
|
counts: inspection.counts,
|
|
@@ -21028,13 +21286,594 @@ import {
|
|
|
21028
21286
|
copyFileSync,
|
|
21029
21287
|
mkdirSync,
|
|
21030
21288
|
readdirSync,
|
|
21289
|
+
readFileSync,
|
|
21031
21290
|
realpathSync,
|
|
21032
21291
|
renameSync,
|
|
21033
21292
|
rmSync,
|
|
21034
21293
|
statfsSync,
|
|
21035
|
-
statSync
|
|
21294
|
+
statSync,
|
|
21295
|
+
writeFileSync
|
|
21036
21296
|
} from "fs";
|
|
21037
|
-
import { join as
|
|
21297
|
+
import { join as join4 } from "path";
|
|
21298
|
+
|
|
21299
|
+
// packages/control/src/plugins.ts
|
|
21300
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
21301
|
+
import { basename as basename2, join as join3, resolve as resolve2, sep as sep2 } from "path";
|
|
21302
|
+
|
|
21303
|
+
// packages/plugin-api/src/version.ts
|
|
21304
|
+
var PLUGIN_API_VERSION = 1;
|
|
21305
|
+
var DASHBOARD_SDK_VERSION = "0.1.0";
|
|
21306
|
+
// packages/plugin-api/src/manifest.ts
|
|
21307
|
+
var CAPABILITIES = [
|
|
21308
|
+
"storage",
|
|
21309
|
+
"files",
|
|
21310
|
+
"net:outbound",
|
|
21311
|
+
"events:request",
|
|
21312
|
+
"events:limit"
|
|
21313
|
+
];
|
|
21314
|
+
var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
21315
|
+
var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
|
|
21316
|
+
var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
|
|
21317
|
+
message: "entry must be a relative path with no '..' segment"
|
|
21318
|
+
});
|
|
21319
|
+
var originSchema = exports_external.string().refine((value) => {
|
|
21320
|
+
let url2;
|
|
21321
|
+
try {
|
|
21322
|
+
url2 = new URL(value);
|
|
21323
|
+
} catch {
|
|
21324
|
+
return false;
|
|
21325
|
+
}
|
|
21326
|
+
if (url2.protocol !== "https:" && url2.protocol !== "http:")
|
|
21327
|
+
return false;
|
|
21328
|
+
if (url2.hostname === "")
|
|
21329
|
+
return false;
|
|
21330
|
+
if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
|
|
21331
|
+
return false;
|
|
21332
|
+
return value === url2.origin || value === `${url2.origin}/`;
|
|
21333
|
+
}, { message: "origin must be scheme://host with no path, query or fragment" });
|
|
21334
|
+
var navSchema = exports_external.object({
|
|
21335
|
+
label: exports_external.string().min(1).max(32)
|
|
21336
|
+
}).strict();
|
|
21337
|
+
var manifestSchema = exports_external.object({
|
|
21338
|
+
id: idSchema,
|
|
21339
|
+
name: exports_external.string().min(1).max(64),
|
|
21340
|
+
version: exports_external.string().min(1).max(32),
|
|
21341
|
+
api: exports_external.number().int().nonnegative(),
|
|
21342
|
+
sdk: exports_external.string().min(1).max(32).optional(),
|
|
21343
|
+
server: entrySchema.optional(),
|
|
21344
|
+
ui: entrySchema.optional(),
|
|
21345
|
+
nav: navSchema.optional(),
|
|
21346
|
+
capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
|
|
21347
|
+
origins: exports_external.array(originSchema).min(1).optional(),
|
|
21348
|
+
defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21349
|
+
}).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
|
|
21350
|
+
message: "manifest must declare a server entry, a ui entry, or both"
|
|
21351
|
+
}).refine((m) => m.ui === undefined || m.sdk !== undefined, {
|
|
21352
|
+
message: "a ui entry requires an sdk range"
|
|
21353
|
+
}).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
|
|
21354
|
+
message: "net:outbound requires origins"
|
|
21355
|
+
}).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
|
|
21356
|
+
message: "origins requires the net:outbound capability"
|
|
21357
|
+
});
|
|
21358
|
+
function safeParseManifest(input) {
|
|
21359
|
+
const parsed = manifestSchema.safeParse(input);
|
|
21360
|
+
if (parsed.success)
|
|
21361
|
+
return { ok: true, manifest: parsed.data };
|
|
21362
|
+
const first = parsed.error.issues[0];
|
|
21363
|
+
const path = first?.path.join(".") ?? "";
|
|
21364
|
+
const message = first?.message ?? "invalid manifest";
|
|
21365
|
+
return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
|
|
21366
|
+
}
|
|
21367
|
+
function isApiCompatible(manifest) {
|
|
21368
|
+
return manifest.api === PLUGIN_API_VERSION;
|
|
21369
|
+
}
|
|
21370
|
+
|
|
21371
|
+
// packages/control/src/plugins.ts
|
|
21372
|
+
var PLUGINS_DIRNAME = "plugins";
|
|
21373
|
+
function pluginsDir(root) {
|
|
21374
|
+
return join3(root, PLUGINS_DIRNAME);
|
|
21375
|
+
}
|
|
21376
|
+
var MANIFEST_FILENAME = "omni-plugin.json";
|
|
21377
|
+
var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
|
|
21378
|
+
var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
21379
|
+
var NPM_TARBALL_ROOT = "package";
|
|
21380
|
+
function readManifest(fs, home) {
|
|
21381
|
+
const raw = fs.readText(join3(home, MANIFEST_FILENAME));
|
|
21382
|
+
if (raw === null) {
|
|
21383
|
+
return { check: "manifest", reason: `no ${MANIFEST_FILENAME}`, fatal: true };
|
|
21384
|
+
}
|
|
21385
|
+
let document;
|
|
21386
|
+
try {
|
|
21387
|
+
document = JSON.parse(raw);
|
|
21388
|
+
} catch (error51) {
|
|
21389
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21390
|
+
return {
|
|
21391
|
+
check: "manifest",
|
|
21392
|
+
reason: `${MANIFEST_FILENAME} is not valid JSON: ${detail}`,
|
|
21393
|
+
fatal: true
|
|
21394
|
+
};
|
|
21395
|
+
}
|
|
21396
|
+
const parsed = safeParseManifest(document);
|
|
21397
|
+
if (!parsed.ok)
|
|
21398
|
+
return { check: "manifest", reason: parsed.reason, fatal: true };
|
|
21399
|
+
return parsed.manifest;
|
|
21400
|
+
}
|
|
21401
|
+
function checkEntry(fs, home, entry, label2, fatal) {
|
|
21402
|
+
const target = resolve2(home, entry);
|
|
21403
|
+
if (target !== home && !target.startsWith(`${home}${sep2}`)) {
|
|
21404
|
+
return {
|
|
21405
|
+
check: "entry",
|
|
21406
|
+
reason: `${label2} entry resolves outside the plugin directory`,
|
|
21407
|
+
fatal
|
|
21408
|
+
};
|
|
21409
|
+
}
|
|
21410
|
+
if (!fs.isFile(target)) {
|
|
21411
|
+
return { check: "entry", reason: `${label2} entry ${entry} does not exist`, fatal };
|
|
21412
|
+
}
|
|
21413
|
+
return null;
|
|
21414
|
+
}
|
|
21415
|
+
function verifyPlugin(deps, root, id) {
|
|
21416
|
+
const home = join3(pluginsDir(root), id);
|
|
21417
|
+
if (!ID_PATTERN2.test(id) || !deps.fs.isDirectory(home)) {
|
|
21418
|
+
throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
|
|
21419
|
+
}
|
|
21420
|
+
return reportFor(deps, home, id);
|
|
21421
|
+
}
|
|
21422
|
+
function reportFor(deps, home, id) {
|
|
21423
|
+
const problems = [];
|
|
21424
|
+
const read = readManifest(deps.fs, home);
|
|
21425
|
+
if (!("id" in read)) {
|
|
21426
|
+
problems.push(read);
|
|
21427
|
+
return { id, path: home, manifest: null, problems, loadable: false };
|
|
21428
|
+
}
|
|
21429
|
+
const manifest = read;
|
|
21430
|
+
if (manifest.id !== id) {
|
|
21431
|
+
problems.push({
|
|
21432
|
+
check: "id",
|
|
21433
|
+
reason: `manifest id ${manifest.id} does not match its directory ${id}`,
|
|
21434
|
+
fatal: true
|
|
21435
|
+
});
|
|
21436
|
+
}
|
|
21437
|
+
if (!isApiCompatible(manifest)) {
|
|
21438
|
+
problems.push({
|
|
21439
|
+
check: "api",
|
|
21440
|
+
reason: `plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`,
|
|
21441
|
+
fatal: true
|
|
21442
|
+
});
|
|
21443
|
+
}
|
|
21444
|
+
if (manifest.server !== undefined) {
|
|
21445
|
+
const problem = checkEntry(deps.fs, home, manifest.server, "server", true);
|
|
21446
|
+
if (problem !== null)
|
|
21447
|
+
problems.push(problem);
|
|
21448
|
+
}
|
|
21449
|
+
if (manifest.ui !== undefined) {
|
|
21450
|
+
const problem = checkEntry(deps.fs, home, manifest.ui, "ui", false);
|
|
21451
|
+
if (problem !== null)
|
|
21452
|
+
problems.push(problem);
|
|
21453
|
+
}
|
|
21454
|
+
const sdk = sdkProblem(deps, manifest);
|
|
21455
|
+
if (sdk !== null)
|
|
21456
|
+
problems.push(sdk);
|
|
21457
|
+
return {
|
|
21458
|
+
id,
|
|
21459
|
+
path: home,
|
|
21460
|
+
manifest,
|
|
21461
|
+
problems,
|
|
21462
|
+
loadable: !problems.some((problem) => problem.fatal)
|
|
21463
|
+
};
|
|
21464
|
+
}
|
|
21465
|
+
function sdkProblem(deps, manifest) {
|
|
21466
|
+
if (manifest.ui === undefined || manifest.sdk === undefined)
|
|
21467
|
+
return null;
|
|
21468
|
+
if (deps.sdkVersion === undefined)
|
|
21469
|
+
return null;
|
|
21470
|
+
if (Bun.semver.satisfies(deps.sdkVersion, manifest.sdk))
|
|
21471
|
+
return null;
|
|
21472
|
+
return {
|
|
21473
|
+
check: "sdk",
|
|
21474
|
+
reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}`,
|
|
21475
|
+
fatal: false
|
|
21476
|
+
};
|
|
21477
|
+
}
|
|
21478
|
+
function listPlugins(deps, root) {
|
|
21479
|
+
const dir = pluginsDir(root);
|
|
21480
|
+
const summaries = [];
|
|
21481
|
+
for (const entry of [...deps.fs.readdir(dir)].sort()) {
|
|
21482
|
+
const home = join3(dir, entry);
|
|
21483
|
+
if (!deps.fs.isDirectory(home))
|
|
21484
|
+
continue;
|
|
21485
|
+
if (!ID_PATTERN2.test(entry)) {
|
|
21486
|
+
summaries.push({
|
|
21487
|
+
id: entry,
|
|
21488
|
+
path: home,
|
|
21489
|
+
manifest: null,
|
|
21490
|
+
problems: [
|
|
21491
|
+
{
|
|
21492
|
+
check: "id",
|
|
21493
|
+
reason: `directory name ${entry} is not a valid plugin id`,
|
|
21494
|
+
fatal: true
|
|
21495
|
+
}
|
|
21496
|
+
],
|
|
21497
|
+
loadable: false,
|
|
21498
|
+
name: null,
|
|
21499
|
+
version: null,
|
|
21500
|
+
api: null,
|
|
21501
|
+
sdk: null,
|
|
21502
|
+
capabilities: [],
|
|
21503
|
+
origins: []
|
|
21504
|
+
});
|
|
21505
|
+
continue;
|
|
21506
|
+
}
|
|
21507
|
+
const report = reportFor(deps, home, entry);
|
|
21508
|
+
const manifest = report.manifest;
|
|
21509
|
+
summaries.push({
|
|
21510
|
+
...report,
|
|
21511
|
+
name: manifest?.name ?? null,
|
|
21512
|
+
version: manifest?.version ?? null,
|
|
21513
|
+
api: manifest?.api ?? null,
|
|
21514
|
+
sdk: manifest?.sdk ?? null,
|
|
21515
|
+
capabilities: manifest?.capabilities ?? [],
|
|
21516
|
+
origins: manifest?.origins ?? []
|
|
21517
|
+
});
|
|
21518
|
+
}
|
|
21519
|
+
return summaries;
|
|
21520
|
+
}
|
|
21521
|
+
var BLOCK = 512;
|
|
21522
|
+
var decoder2 = new TextDecoder;
|
|
21523
|
+
function fieldText(header, offset, length) {
|
|
21524
|
+
const slice = header.subarray(offset, offset + length);
|
|
21525
|
+
const end = slice.indexOf(0);
|
|
21526
|
+
return decoder2.decode(end === -1 ? slice : slice.subarray(0, end));
|
|
21527
|
+
}
|
|
21528
|
+
function fieldOctal(header, offset, length) {
|
|
21529
|
+
const text = fieldText(header, offset, length).trim();
|
|
21530
|
+
if (text.length === 0)
|
|
21531
|
+
return 0;
|
|
21532
|
+
const value = Number.parseInt(text, 8);
|
|
21533
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : -1;
|
|
21534
|
+
}
|
|
21535
|
+
function isSafeArchivePath(path) {
|
|
21536
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("\x00"))
|
|
21537
|
+
return false;
|
|
21538
|
+
if (path.includes("\\"))
|
|
21539
|
+
return false;
|
|
21540
|
+
return !path.split("/").includes("..");
|
|
21541
|
+
}
|
|
21542
|
+
function readTar(bytes) {
|
|
21543
|
+
const files = new Map;
|
|
21544
|
+
let total = 0;
|
|
21545
|
+
let longName = null;
|
|
21546
|
+
let offset = 0;
|
|
21547
|
+
while (offset + BLOCK <= bytes.length) {
|
|
21548
|
+
const header = bytes.subarray(offset, offset + BLOCK);
|
|
21549
|
+
if (header.every((byte) => byte === 0))
|
|
21550
|
+
break;
|
|
21551
|
+
const size = fieldOctal(header, 124, 12);
|
|
21552
|
+
if (size < 0)
|
|
21553
|
+
throw new GatewayError("BAD_REQUEST", "archive has an unreadable entry size");
|
|
21554
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
21555
|
+
const name = fieldText(header, 0, 100);
|
|
21556
|
+
const prefix = fieldText(header, 345, 155);
|
|
21557
|
+
offset += BLOCK;
|
|
21558
|
+
total += size;
|
|
21559
|
+
if (total > MAX_PLUGIN_BYTES) {
|
|
21560
|
+
throw new GatewayError("BAD_REQUEST", `archive unpacks to more than ${MAX_PLUGIN_BYTES} bytes`);
|
|
21561
|
+
}
|
|
21562
|
+
if (offset + size > bytes.length) {
|
|
21563
|
+
throw new GatewayError("BAD_REQUEST", `archive is truncated: entry ${name || "(unnamed)"} declares ${size} bytes and the archive ends before them`);
|
|
21564
|
+
}
|
|
21565
|
+
const data = bytes.subarray(offset, offset + size);
|
|
21566
|
+
offset += Math.ceil(size / BLOCK) * BLOCK;
|
|
21567
|
+
if (type === "1" || type === "2") {
|
|
21568
|
+
throw new GatewayError("BAD_REQUEST", `archive contains a link (${name}); plugin packages must be plain files`);
|
|
21569
|
+
}
|
|
21570
|
+
if (type === "L") {
|
|
21571
|
+
longName = decoder2.decode(data).replace(/\0+$/, "");
|
|
21572
|
+
continue;
|
|
21573
|
+
}
|
|
21574
|
+
if (type === "K" || type === "x" || type === "g")
|
|
21575
|
+
continue;
|
|
21576
|
+
if (type !== "0" && type !== "\x00") {
|
|
21577
|
+
longName = null;
|
|
21578
|
+
continue;
|
|
21579
|
+
}
|
|
21580
|
+
const path = longName ?? (prefix.length > 0 ? `${prefix}/${name}` : name);
|
|
21581
|
+
longName = null;
|
|
21582
|
+
if (!isSafeArchivePath(path)) {
|
|
21583
|
+
throw new GatewayError("BAD_REQUEST", `archive contains an unsafe path: ${path}`);
|
|
21584
|
+
}
|
|
21585
|
+
files.set(path, new Uint8Array(data));
|
|
21586
|
+
}
|
|
21587
|
+
return files;
|
|
21588
|
+
}
|
|
21589
|
+
function gunzipIfNeeded(bytes) {
|
|
21590
|
+
if (bytes[0] !== 31 || bytes[1] !== 139)
|
|
21591
|
+
return bytes;
|
|
21592
|
+
try {
|
|
21593
|
+
return Bun.gunzipSync(new Uint8Array(bytes));
|
|
21594
|
+
} catch (error51) {
|
|
21595
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21596
|
+
throw new GatewayError("BAD_REQUEST", `could not decompress the archive: ${detail}`);
|
|
21597
|
+
}
|
|
21598
|
+
}
|
|
21599
|
+
function stripRoot(files) {
|
|
21600
|
+
const roots = new Set;
|
|
21601
|
+
for (const path of files.keys()) {
|
|
21602
|
+
const slash = path.indexOf("/");
|
|
21603
|
+
if (slash <= 0)
|
|
21604
|
+
return { files, claimedId: null };
|
|
21605
|
+
roots.add(path.slice(0, slash));
|
|
21606
|
+
}
|
|
21607
|
+
const [only] = [...roots];
|
|
21608
|
+
if (roots.size !== 1 || only === undefined)
|
|
21609
|
+
return { files, claimedId: null };
|
|
21610
|
+
const stripped = new Map;
|
|
21611
|
+
for (const [path, bytes] of files)
|
|
21612
|
+
stripped.set(path.slice(only.length + 1), bytes);
|
|
21613
|
+
return { files: stripped, claimedId: only === NPM_TARBALL_ROOT ? null : only };
|
|
21614
|
+
}
|
|
21615
|
+
function readTree(fs, dir, prefix = "") {
|
|
21616
|
+
const files = new Map;
|
|
21617
|
+
let total = 0;
|
|
21618
|
+
for (const entry of fs.readdir(dir)) {
|
|
21619
|
+
const path = join3(dir, entry);
|
|
21620
|
+
const relative = prefix.length === 0 ? entry : `${prefix}/${entry}`;
|
|
21621
|
+
if (fs.isDirectory(path)) {
|
|
21622
|
+
for (const [nested, bytes2] of readTree(fs, path, relative))
|
|
21623
|
+
files.set(nested, bytes2);
|
|
21624
|
+
continue;
|
|
21625
|
+
}
|
|
21626
|
+
const bytes = fs.readBytes(path);
|
|
21627
|
+
if (bytes === null)
|
|
21628
|
+
continue;
|
|
21629
|
+
total += bytes.byteLength;
|
|
21630
|
+
if (total > MAX_PLUGIN_BYTES) {
|
|
21631
|
+
throw new GatewayError("BAD_REQUEST", `${dir} holds more than ${MAX_PLUGIN_BYTES} bytes`);
|
|
21632
|
+
}
|
|
21633
|
+
files.set(relative, bytes);
|
|
21634
|
+
}
|
|
21635
|
+
return files;
|
|
21636
|
+
}
|
|
21637
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
21638
|
+
var NPM_PACKUMENT_ACCEPT = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
|
|
21639
|
+
var NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
21640
|
+
var EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
21641
|
+
var SRI_ALGORITHMS = ["sha512", "sha384", "sha256"];
|
|
21642
|
+
function asRecord(value) {
|
|
21643
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
21644
|
+
return null;
|
|
21645
|
+
return value;
|
|
21646
|
+
}
|
|
21647
|
+
function asString(value) {
|
|
21648
|
+
return typeof value === "string" ? value : null;
|
|
21649
|
+
}
|
|
21650
|
+
function parseNpmSpec(spec) {
|
|
21651
|
+
const at = spec.lastIndexOf("@");
|
|
21652
|
+
const name = at <= 0 ? spec : spec.slice(0, at);
|
|
21653
|
+
const version2 = at <= 0 ? null : spec.slice(at + 1);
|
|
21654
|
+
if (name.length > 214 || !NPM_NAME_PATTERN.test(name))
|
|
21655
|
+
return null;
|
|
21656
|
+
return { name, version: version2 };
|
|
21657
|
+
}
|
|
21658
|
+
function registryUrl(deps) {
|
|
21659
|
+
const raw = deps.registry ?? DEFAULT_NPM_REGISTRY;
|
|
21660
|
+
let url2;
|
|
21661
|
+
try {
|
|
21662
|
+
url2 = new URL(raw);
|
|
21663
|
+
} catch {
|
|
21664
|
+
throw new GatewayError("BAD_REQUEST", `registry ${raw} is not a URL`);
|
|
21665
|
+
}
|
|
21666
|
+
if (url2.protocol !== "https:") {
|
|
21667
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; ${raw} is not`);
|
|
21668
|
+
}
|
|
21669
|
+
return url2;
|
|
21670
|
+
}
|
|
21671
|
+
function packumentUrl(registry2, name) {
|
|
21672
|
+
return `${registry2.href.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
|
|
21673
|
+
}
|
|
21674
|
+
function resolveDist(document, spec) {
|
|
21675
|
+
const root = asRecord(document);
|
|
21676
|
+
if (root === null) {
|
|
21677
|
+
throw new GatewayError("BAD_REQUEST", `the registry's answer for ${spec.name} is not a package`);
|
|
21678
|
+
}
|
|
21679
|
+
const versions2 = asRecord(root.versions);
|
|
21680
|
+
if (versions2 === null) {
|
|
21681
|
+
throw new GatewayError("BAD_REQUEST", `the registry lists no versions of ${spec.name}`);
|
|
21682
|
+
}
|
|
21683
|
+
let wanted = spec.version;
|
|
21684
|
+
if (wanted === null) {
|
|
21685
|
+
const tags = asRecord(root["dist-tags"]);
|
|
21686
|
+
wanted = tags === null ? null : asString(tags.latest);
|
|
21687
|
+
if (wanted === null) {
|
|
21688
|
+
throw new GatewayError("BAD_REQUEST", `the registry names no dist-tags.latest for ${spec.name}; install ${spec.name}@<exact version>`);
|
|
21689
|
+
}
|
|
21690
|
+
}
|
|
21691
|
+
const entry = asRecord(versions2[wanted]);
|
|
21692
|
+
if (entry === null) {
|
|
21693
|
+
throw new GatewayError("BAD_REQUEST", `the registry has no version ${wanted} of ${spec.name}`);
|
|
21694
|
+
}
|
|
21695
|
+
const dist = asRecord(entry.dist);
|
|
21696
|
+
const tarball = dist === null ? null : asString(dist.tarball);
|
|
21697
|
+
if (dist === null || tarball === null) {
|
|
21698
|
+
throw new GatewayError("BAD_REQUEST", `the registry's entry for ${spec.name}@${wanted} names no tarball`);
|
|
21699
|
+
}
|
|
21700
|
+
return {
|
|
21701
|
+
version: wanted,
|
|
21702
|
+
dist: { tarball, integrity: asString(dist.integrity), shasum: asString(dist.shasum) }
|
|
21703
|
+
};
|
|
21704
|
+
}
|
|
21705
|
+
function checkTarballUrl(tarball, registry2, what) {
|
|
21706
|
+
let url2;
|
|
21707
|
+
try {
|
|
21708
|
+
url2 = new URL(tarball);
|
|
21709
|
+
} catch {
|
|
21710
|
+
throw new GatewayError("BAD_REQUEST", `the registry gave ${what} a tarball that is not a URL`);
|
|
21711
|
+
}
|
|
21712
|
+
if (url2.protocol !== "https:") {
|
|
21713
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; the registry points ${what} at ${tarball}`);
|
|
21714
|
+
}
|
|
21715
|
+
if (url2.host !== registry2.host) {
|
|
21716
|
+
throw new GatewayError("BAD_REQUEST", `registry ${registry2.host} points ${what} at ${url2.host}; ` + "install that URL directly if it is the one you meant");
|
|
21717
|
+
}
|
|
21718
|
+
}
|
|
21719
|
+
function digestsFor(dist, what) {
|
|
21720
|
+
const digests = [];
|
|
21721
|
+
if (dist.integrity !== null) {
|
|
21722
|
+
for (const entry of dist.integrity.trim().split(/\s+/)) {
|
|
21723
|
+
const dash = entry.indexOf("-");
|
|
21724
|
+
const algorithm = dash === -1 ? "" : entry.slice(0, dash);
|
|
21725
|
+
const value = dash === -1 ? "" : entry.slice(dash + 1);
|
|
21726
|
+
if (value.length === 0)
|
|
21727
|
+
continue;
|
|
21728
|
+
const known = SRI_ALGORITHMS.find((candidate) => candidate === algorithm);
|
|
21729
|
+
if (known === undefined)
|
|
21730
|
+
continue;
|
|
21731
|
+
digests.push({
|
|
21732
|
+
algorithm: known,
|
|
21733
|
+
expected: Buffer2.from(value, "base64").toString("hex"),
|
|
21734
|
+
source: `${known} integrity`
|
|
21735
|
+
});
|
|
21736
|
+
}
|
|
21737
|
+
}
|
|
21738
|
+
if (digests.length === 0 && dist.shasum !== null && /^[0-9a-f]{40}$/i.test(dist.shasum)) {
|
|
21739
|
+
digests.push({ algorithm: "sha1", expected: dist.shasum.toLowerCase(), source: "sha1 shasum" });
|
|
21740
|
+
}
|
|
21741
|
+
if (digests.length === 0) {
|
|
21742
|
+
throw new GatewayError("BAD_REQUEST", `the registry advertises no usable integrity or shasum for ${what}; ` + "refusing to install bytes nothing vouches for");
|
|
21743
|
+
}
|
|
21744
|
+
return digests;
|
|
21745
|
+
}
|
|
21746
|
+
function verifyDigests(bytes, digests, what) {
|
|
21747
|
+
for (const digest of digests) {
|
|
21748
|
+
const actual = new Bun.CryptoHasher(digest.algorithm).update(bytes).digest("hex");
|
|
21749
|
+
if (actual === digest.expected)
|
|
21750
|
+
return;
|
|
21751
|
+
}
|
|
21752
|
+
const first = digests[0];
|
|
21753
|
+
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");
|
|
21754
|
+
}
|
|
21755
|
+
async function npmPayload(deps, spec) {
|
|
21756
|
+
if (spec.version !== null && !EXACT_VERSION_PATTERN.test(spec.version)) {
|
|
21757
|
+
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`);
|
|
21758
|
+
}
|
|
21759
|
+
const fetchBytes = deps.fetchBytes;
|
|
21760
|
+
if (fetchBytes === undefined) {
|
|
21761
|
+
throw new GatewayError("BAD_REQUEST", "this caller cannot install from a registry");
|
|
21762
|
+
}
|
|
21763
|
+
const registry2 = registryUrl(deps);
|
|
21764
|
+
const document = parseJson(await fetchBytes(packumentUrl(registry2, spec.name), NPM_PACKUMENT_ACCEPT), `the registry's answer for ${spec.name}`);
|
|
21765
|
+
const { version: version2, dist } = resolveDist(document, spec);
|
|
21766
|
+
const what = `${spec.name}@${version2}`;
|
|
21767
|
+
checkTarballUrl(dist.tarball, registry2, what);
|
|
21768
|
+
const digests = digestsFor(dist, what);
|
|
21769
|
+
const bytes = await fetchBytes(dist.tarball);
|
|
21770
|
+
verifyDigests(bytes, digests, what);
|
|
21771
|
+
return stripRoot(readTar(gunzipIfNeeded(bytes)));
|
|
21772
|
+
}
|
|
21773
|
+
function parseJson(bytes, what) {
|
|
21774
|
+
try {
|
|
21775
|
+
return JSON.parse(decoder2.decode(bytes));
|
|
21776
|
+
} catch (error51) {
|
|
21777
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21778
|
+
throw new GatewayError("BAD_REQUEST", `${what} is not valid JSON: ${detail}`);
|
|
21779
|
+
}
|
|
21780
|
+
}
|
|
21781
|
+
async function loadPayload(deps, spec) {
|
|
21782
|
+
if (spec.startsWith("http://")) {
|
|
21783
|
+
throw new GatewayError("BAD_REQUEST", "plugins may only be installed over https");
|
|
21784
|
+
}
|
|
21785
|
+
if (spec.startsWith("https://")) {
|
|
21786
|
+
if (deps.fetchBytes === undefined) {
|
|
21787
|
+
throw new GatewayError("BAD_REQUEST", "this caller cannot install from a URL");
|
|
21788
|
+
}
|
|
21789
|
+
return stripRoot(readTar(gunzipIfNeeded(await deps.fetchBytes(spec))));
|
|
21790
|
+
}
|
|
21791
|
+
const source = resolve2(spec);
|
|
21792
|
+
if (deps.fs.isDirectory(source)) {
|
|
21793
|
+
const files = readTree(deps.fs, source);
|
|
21794
|
+
const claimed = basename2(source);
|
|
21795
|
+
return { files, claimedId: ID_PATTERN2.test(claimed) ? claimed : null };
|
|
21796
|
+
}
|
|
21797
|
+
const bytes = deps.fs.readBytes(source);
|
|
21798
|
+
if (bytes !== null)
|
|
21799
|
+
return stripRoot(readTar(gunzipIfNeeded(bytes)));
|
|
21800
|
+
const npm = parseNpmSpec(spec);
|
|
21801
|
+
if (npm !== null)
|
|
21802
|
+
return npmPayload(deps, npm);
|
|
21803
|
+
throw new GatewayError("BAD_REQUEST", `no directory or archive at ${source}, and "${spec}" is not a package name`);
|
|
21804
|
+
}
|
|
21805
|
+
async function installPlugin(deps, root, spec) {
|
|
21806
|
+
const payload = await loadPayload(deps, spec);
|
|
21807
|
+
const raw = payload.files.get(MANIFEST_FILENAME);
|
|
21808
|
+
if (raw === undefined) {
|
|
21809
|
+
throw new GatewayError("BAD_REQUEST", `${spec} has no ${MANIFEST_FILENAME} at its root`);
|
|
21810
|
+
}
|
|
21811
|
+
let document;
|
|
21812
|
+
try {
|
|
21813
|
+
document = JSON.parse(decoder2.decode(raw));
|
|
21814
|
+
} catch (error51) {
|
|
21815
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
21816
|
+
throw new GatewayError("BAD_REQUEST", `${MANIFEST_FILENAME} is not valid JSON: ${detail}`);
|
|
21817
|
+
}
|
|
21818
|
+
const parsed = safeParseManifest(document);
|
|
21819
|
+
if (!parsed.ok)
|
|
21820
|
+
throw new GatewayError("BAD_REQUEST", `invalid ${MANIFEST_FILENAME}: ${parsed.reason}`);
|
|
21821
|
+
const manifest = parsed.manifest;
|
|
21822
|
+
const targetId = payload.claimedId ?? manifest.id;
|
|
21823
|
+
if (manifest.id !== targetId) {
|
|
21824
|
+
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");
|
|
21825
|
+
}
|
|
21826
|
+
const dir = pluginsDir(root);
|
|
21827
|
+
const target = join3(dir, targetId);
|
|
21828
|
+
const replaced = deps.fs.isDirectory(target);
|
|
21829
|
+
const staging = join3(dir, `.staging-${targetId}`);
|
|
21830
|
+
deps.fs.rm(staging);
|
|
21831
|
+
try {
|
|
21832
|
+
for (const [path, bytes] of payload.files) {
|
|
21833
|
+
const destination = join3(staging, path);
|
|
21834
|
+
deps.fs.mkdir(join3(destination, ".."));
|
|
21835
|
+
deps.fs.writeBytes(destination, bytes);
|
|
21836
|
+
}
|
|
21837
|
+
deps.fs.rm(target);
|
|
21838
|
+
deps.fs.rename(staging, target);
|
|
21839
|
+
} finally {
|
|
21840
|
+
deps.fs.rm(staging);
|
|
21841
|
+
}
|
|
21842
|
+
return {
|
|
21843
|
+
id: targetId,
|
|
21844
|
+
name: manifest.name,
|
|
21845
|
+
version: manifest.version,
|
|
21846
|
+
path: target,
|
|
21847
|
+
replaced,
|
|
21848
|
+
restartRequired: true
|
|
21849
|
+
};
|
|
21850
|
+
}
|
|
21851
|
+
function removePlugin(deps, root, id, options = {}) {
|
|
21852
|
+
if (!ID_PATTERN2.test(id)) {
|
|
21853
|
+
throw new GatewayError("BAD_REQUEST", `"${id}" is not a valid plugin id`);
|
|
21854
|
+
}
|
|
21855
|
+
const target = join3(pluginsDir(root), id);
|
|
21856
|
+
const present = deps.fs.isDirectory(target);
|
|
21857
|
+
const purge = options.purge === true;
|
|
21858
|
+
if (purge && deps.store === undefined) {
|
|
21859
|
+
throw new GatewayError("INTERNAL", "purging a plugin's data needs a database");
|
|
21860
|
+
}
|
|
21861
|
+
const tables = purge && deps.store !== undefined ? deps.store.plugins.listTables(id) : [];
|
|
21862
|
+
if (!present && tables.length === 0) {
|
|
21863
|
+
throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
|
|
21864
|
+
}
|
|
21865
|
+
if (present)
|
|
21866
|
+
deps.fs.rm(target);
|
|
21867
|
+
if (purge && deps.store !== undefined)
|
|
21868
|
+
deps.store.plugins.dropAll(id);
|
|
21869
|
+
return { id, removed: present, droppedTables: tables };
|
|
21870
|
+
}
|
|
21871
|
+
function orphanPluginTables(deps, root, store) {
|
|
21872
|
+
const installed = listPlugins(deps, root).map((plugin) => plugin.id);
|
|
21873
|
+
return store.plugins.orphanTables(installed);
|
|
21874
|
+
}
|
|
21875
|
+
|
|
21876
|
+
// packages/control/src/nodeFs.ts
|
|
21038
21877
|
function nodeDatabaseFs() {
|
|
21039
21878
|
const dirBytes = (dir) => {
|
|
21040
21879
|
let entries;
|
|
@@ -21045,7 +21884,7 @@ function nodeDatabaseFs() {
|
|
|
21045
21884
|
}
|
|
21046
21885
|
let total = 0;
|
|
21047
21886
|
for (const entry of entries) {
|
|
21048
|
-
const path =
|
|
21887
|
+
const path = join4(dir, entry.name);
|
|
21049
21888
|
if (entry.isDirectory()) {
|
|
21050
21889
|
total += dirBytes(path);
|
|
21051
21890
|
continue;
|
|
@@ -21098,6 +21937,96 @@ function nodeDatabaseFs() {
|
|
|
21098
21937
|
dirBytes
|
|
21099
21938
|
};
|
|
21100
21939
|
}
|
|
21940
|
+
function nodePluginFs() {
|
|
21941
|
+
return {
|
|
21942
|
+
readdir: (dir) => {
|
|
21943
|
+
try {
|
|
21944
|
+
return readdirSync(dir);
|
|
21945
|
+
} catch {
|
|
21946
|
+
return [];
|
|
21947
|
+
}
|
|
21948
|
+
},
|
|
21949
|
+
readText: (path) => {
|
|
21950
|
+
try {
|
|
21951
|
+
return readFileSync(path, "utf8");
|
|
21952
|
+
} catch {
|
|
21953
|
+
return null;
|
|
21954
|
+
}
|
|
21955
|
+
},
|
|
21956
|
+
readBytes: (path) => {
|
|
21957
|
+
try {
|
|
21958
|
+
return new Uint8Array(readFileSync(path));
|
|
21959
|
+
} catch {
|
|
21960
|
+
return null;
|
|
21961
|
+
}
|
|
21962
|
+
},
|
|
21963
|
+
writeBytes: (path, bytes) => {
|
|
21964
|
+
writeFileSync(path, bytes);
|
|
21965
|
+
},
|
|
21966
|
+
isDirectory: (path) => {
|
|
21967
|
+
try {
|
|
21968
|
+
return statSync(path).isDirectory();
|
|
21969
|
+
} catch {
|
|
21970
|
+
return false;
|
|
21971
|
+
}
|
|
21972
|
+
},
|
|
21973
|
+
isFile: (path) => {
|
|
21974
|
+
try {
|
|
21975
|
+
return statSync(path).isFile();
|
|
21976
|
+
} catch {
|
|
21977
|
+
return false;
|
|
21978
|
+
}
|
|
21979
|
+
},
|
|
21980
|
+
mkdir: (dir) => {
|
|
21981
|
+
mkdirSync(dir, { recursive: true });
|
|
21982
|
+
},
|
|
21983
|
+
rm: (path) => {
|
|
21984
|
+
rmSync(path, { force: true, recursive: true });
|
|
21985
|
+
},
|
|
21986
|
+
rename: (from, to) => renameSync(from, to)
|
|
21987
|
+
};
|
|
21988
|
+
}
|
|
21989
|
+
function nodeFetchBytes(options = {}) {
|
|
21990
|
+
const impl = options.fetchImpl ?? fetch;
|
|
21991
|
+
const maxBytes = options.maxBytes ?? MAX_PLUGIN_BYTES;
|
|
21992
|
+
return async (url2, accept) => {
|
|
21993
|
+
if (!url2.startsWith("https://")) {
|
|
21994
|
+
throw new GatewayError("BAD_REQUEST", `plugins may only be fetched over https; ${url2} is not`);
|
|
21995
|
+
}
|
|
21996
|
+
const response = await impl(url2, {
|
|
21997
|
+
headers: accept === undefined ? {} : { accept },
|
|
21998
|
+
redirect: "follow"
|
|
21999
|
+
});
|
|
22000
|
+
if (!response.ok) {
|
|
22001
|
+
throw new GatewayError("BAD_REQUEST", `${url2} answered ${response.status}`);
|
|
22002
|
+
}
|
|
22003
|
+
const reader = response.body?.getReader();
|
|
22004
|
+
if (reader === undefined)
|
|
22005
|
+
return new Uint8Array(0);
|
|
22006
|
+
const chunks = [];
|
|
22007
|
+
let total = 0;
|
|
22008
|
+
for (;; ) {
|
|
22009
|
+
const { done, value } = await reader.read();
|
|
22010
|
+
if (done)
|
|
22011
|
+
break;
|
|
22012
|
+
if (value === undefined)
|
|
22013
|
+
continue;
|
|
22014
|
+
total += value.byteLength;
|
|
22015
|
+
if (total > maxBytes) {
|
|
22016
|
+
await reader.cancel();
|
|
22017
|
+
throw new GatewayError("BAD_REQUEST", `${url2} sent more than ${maxBytes} bytes`);
|
|
22018
|
+
}
|
|
22019
|
+
chunks.push(value);
|
|
22020
|
+
}
|
|
22021
|
+
const bytes = new Uint8Array(total);
|
|
22022
|
+
let offset = 0;
|
|
22023
|
+
for (const chunk of chunks) {
|
|
22024
|
+
bytes.set(chunk, offset);
|
|
22025
|
+
offset += chunk.byteLength;
|
|
22026
|
+
}
|
|
22027
|
+
return bytes;
|
|
22028
|
+
};
|
|
22029
|
+
}
|
|
21101
22030
|
// packages/control/src/oauth/pkce.ts
|
|
21102
22031
|
function randomToken() {
|
|
21103
22032
|
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url");
|
|
@@ -22305,9 +23234,9 @@ function requirePositional(parsed, index, name) {
|
|
|
22305
23234
|
}
|
|
22306
23235
|
|
|
22307
23236
|
// apps/cli/src/context.ts
|
|
22308
|
-
import { existsSync, readFileSync } from "fs";
|
|
23237
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
22309
23238
|
import { homedir } from "os";
|
|
22310
|
-
import { isAbsolute, join as
|
|
23239
|
+
import { isAbsolute, join as join5, resolve as resolve3 } from "path";
|
|
22311
23240
|
class CliError extends Error {
|
|
22312
23241
|
exitCode;
|
|
22313
23242
|
constructor(message, exitCode = 1) {
|
|
@@ -22316,21 +23245,21 @@ class CliError extends Error {
|
|
|
22316
23245
|
this.exitCode = exitCode;
|
|
22317
23246
|
}
|
|
22318
23247
|
}
|
|
22319
|
-
var DEFAULT_ROOT =
|
|
23248
|
+
var DEFAULT_ROOT = join5(homedir(), ".config", "omnigateway");
|
|
22320
23249
|
function looksLikeRoot(dir) {
|
|
22321
|
-
return existsSync(
|
|
23250
|
+
return existsSync(join5(dir, ".env")) || existsSync(join5(dir, "omnigateway.db")) || existsSync(join5(dir, "apps", "gateway", "src", "index.ts"));
|
|
22322
23251
|
}
|
|
22323
23252
|
function resolveRoot(flags, env2, cwd) {
|
|
22324
23253
|
const withEnvFile = (root, source) => {
|
|
22325
|
-
const envFile =
|
|
23254
|
+
const envFile = join5(root, ".env");
|
|
22326
23255
|
return { root, source, envFile: existsSync(envFile) ? envFile : null };
|
|
22327
23256
|
};
|
|
22328
23257
|
if (flags.root !== undefined && flags.root.length > 0) {
|
|
22329
|
-
return withEnvFile(
|
|
23258
|
+
return withEnvFile(resolve3(cwd, flags.root), "flag");
|
|
22330
23259
|
}
|
|
22331
23260
|
const fromEnv = env2.OMNI_ROOT;
|
|
22332
23261
|
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
22333
|
-
return withEnvFile(
|
|
23262
|
+
return withEnvFile(resolve3(cwd, fromEnv), "env");
|
|
22334
23263
|
}
|
|
22335
23264
|
if (looksLikeRoot(cwd))
|
|
22336
23265
|
return withEnvFile(cwd, "cwd");
|
|
@@ -22361,7 +23290,7 @@ function createContext(parsed, options = {}) {
|
|
|
22361
23290
|
const processEnv = options.env ?? process.env;
|
|
22362
23291
|
const cwd = options.cwd ?? process.cwd();
|
|
22363
23292
|
const root = resolveRoot({ root: stringFlag(parsed.values, "root") }, processEnv, cwd);
|
|
22364
|
-
const fileEnv = root.envFile === null ? {} : parseEnvFile(
|
|
23293
|
+
const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync2(root.envFile, "utf8"));
|
|
22365
23294
|
const env2 = { ...processEnv, ...fileEnv };
|
|
22366
23295
|
const rawDbFlag = stringFlag(parsed.values, "db");
|
|
22367
23296
|
const dbFlag = rawDbFlag === undefined || rawDbFlag.trim().length === 0 ? undefined : rawDbFlag;
|
|
@@ -22377,7 +23306,7 @@ function createContext(parsed, options = {}) {
|
|
|
22377
23306
|
configError = error51 instanceof Error ? error51.message : "invalid configuration";
|
|
22378
23307
|
}
|
|
22379
23308
|
const configuredPath = dbFlag ?? config2?.databasePath ?? "omnigateway.db";
|
|
22380
|
-
const databasePath = isAbsolute(configuredPath) ? configuredPath :
|
|
23309
|
+
const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve3(root.root, configuredPath);
|
|
22381
23310
|
const warnings = suppressedDbPath === null ? [] : [
|
|
22382
23311
|
`ignoring OMNI_DB_PATH=${suppressedDbPath} from the environment because --root was given; using ${databasePath}`
|
|
22383
23312
|
];
|
|
@@ -22603,21 +23532,21 @@ async function pollUntilAuthorized(flows, start, deps) {
|
|
|
22603
23532
|
}
|
|
22604
23533
|
|
|
22605
23534
|
// apps/cli/src/service.ts
|
|
22606
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as
|
|
23535
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22607
23536
|
import { homedir as homedir2 } from "os";
|
|
22608
|
-
import { dirname as dirname3, join as
|
|
23537
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
22609
23538
|
var UNIT_NAME2 = "omnigateway.service";
|
|
22610
23539
|
function unitPath(scope) {
|
|
22611
|
-
return scope === "system" ?
|
|
23540
|
+
return scope === "system" ? join6("/etc/systemd/system", UNIT_NAME2) : join6(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
|
|
22612
23541
|
}
|
|
22613
23542
|
function systemctlArgs(scope, args) {
|
|
22614
23543
|
return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
|
|
22615
23544
|
}
|
|
22616
23545
|
function pidFile(stateDir) {
|
|
22617
|
-
return
|
|
23546
|
+
return join6(stateDir, "gateway.pid");
|
|
22618
23547
|
}
|
|
22619
23548
|
function logFile(stateDir) {
|
|
22620
|
-
return
|
|
23549
|
+
return join6(stateDir, "gateway.log");
|
|
22621
23550
|
}
|
|
22622
23551
|
function supervisedLogFile(deps) {
|
|
22623
23552
|
const configured = deps.logFile?.trim();
|
|
@@ -22625,8 +23554,8 @@ function supervisedLogFile(deps) {
|
|
|
22625
23554
|
}
|
|
22626
23555
|
function defaultStateDir(env2) {
|
|
22627
23556
|
const xdg = env2.XDG_STATE_HOME;
|
|
22628
|
-
const base = typeof xdg === "string" && xdg.length > 0 ? xdg :
|
|
22629
|
-
return
|
|
23557
|
+
const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join6(homedir2(), ".local", "state");
|
|
23558
|
+
return join6(base, "omnigateway");
|
|
22630
23559
|
}
|
|
22631
23560
|
function unitFile(input) {
|
|
22632
23561
|
return `[Unit]
|
|
@@ -22637,7 +23566,7 @@ Wants=network-online.target
|
|
|
22637
23566
|
[Service]
|
|
22638
23567
|
Type=simple
|
|
22639
23568
|
WorkingDirectory=${input.root}
|
|
22640
|
-
EnvironmentFile=${
|
|
23569
|
+
EnvironmentFile=${join6(input.root, ".env")}
|
|
22641
23570
|
ExecStart=${input.bun} ${input.entrypoint}
|
|
22642
23571
|
Restart=on-failure
|
|
22643
23572
|
RestartSec=2
|
|
@@ -22653,7 +23582,7 @@ function readPid(stateDir) {
|
|
|
22653
23582
|
const file2 = pidFile(stateDir);
|
|
22654
23583
|
if (!existsSync2(file2))
|
|
22655
23584
|
return null;
|
|
22656
|
-
const pid = Number(
|
|
23585
|
+
const pid = Number(readFileSync3(file2, "utf8").trim());
|
|
22657
23586
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
22658
23587
|
}
|
|
22659
23588
|
function livePid(deps) {
|
|
@@ -22719,7 +23648,7 @@ async function start(deps, input) {
|
|
|
22719
23648
|
env: { ...input.env, OMNI_LOG_FILE: file2 },
|
|
22720
23649
|
logFile: file2
|
|
22721
23650
|
});
|
|
22722
|
-
|
|
23651
|
+
writeFileSync2(pidFile(deps.stateDir), `${pid}
|
|
22723
23652
|
`);
|
|
22724
23653
|
return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
|
|
22725
23654
|
}
|
|
@@ -22765,7 +23694,7 @@ async function install(deps, input) {
|
|
|
22765
23694
|
throw new Error(`${path} already exists; pass --force to replace it`);
|
|
22766
23695
|
}
|
|
22767
23696
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
22768
|
-
|
|
23697
|
+
writeFileSync2(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
|
|
22769
23698
|
const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
|
|
22770
23699
|
let enabled = false;
|
|
22771
23700
|
if (input.enable) {
|
|
@@ -23716,6 +24645,168 @@ ${candidates}${excluded}`;
|
|
|
23716
24645
|
});
|
|
23717
24646
|
}
|
|
23718
24647
|
};
|
|
24648
|
+
// packages/plugin-api/src/events.ts
|
|
24649
|
+
var WINDOW_MS2 = {
|
|
24650
|
+
"1m": 60000,
|
|
24651
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
24652
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
24653
|
+
};
|
|
24654
|
+
// apps/cli/src/commands/plugins.ts
|
|
24655
|
+
function pluginDeps() {
|
|
24656
|
+
return { fs: nodePluginFs(), sdkVersion: DASHBOARD_SDK_VERSION };
|
|
24657
|
+
}
|
|
24658
|
+
function installDeps(ctx, args) {
|
|
24659
|
+
const flag = stringFlag(args.values, "registry");
|
|
24660
|
+
const raw = flag ?? ctx.env.OMNI_PLUGIN_REGISTRY;
|
|
24661
|
+
const registry2 = raw === undefined || raw.trim().length === 0 ? undefined : raw.trim();
|
|
24662
|
+
return {
|
|
24663
|
+
...pluginDeps(),
|
|
24664
|
+
fetchBytes: nodeFetchBytes(),
|
|
24665
|
+
...registry2 === undefined ? {} : { registry: registry2 }
|
|
24666
|
+
};
|
|
24667
|
+
}
|
|
24668
|
+
function pluginState(ctx, plugin) {
|
|
24669
|
+
if (!plugin.loadable)
|
|
24670
|
+
return state(ctx, false, "will not load");
|
|
24671
|
+
if (plugin.problems.length > 0)
|
|
24672
|
+
return paint(ctx, "yellow", "loads, with warnings");
|
|
24673
|
+
return state(ctx, true, "ok");
|
|
24674
|
+
}
|
|
24675
|
+
var pluginList = {
|
|
24676
|
+
usage: "plugin list",
|
|
24677
|
+
summary: "List installed plugins and whether the gateway would load each one",
|
|
24678
|
+
async run(_args, { ctx, writer }) {
|
|
24679
|
+
const root = ctx.root.root;
|
|
24680
|
+
const plugins = listPlugins(pluginDeps(), root);
|
|
24681
|
+
emit(ctx, writer, { pluginsDir: pluginsDir(root), plugins }, () => {
|
|
24682
|
+
if (plugins.length === 0) {
|
|
24683
|
+
return `no plugins installed; unpack one into ${pluginsDir(root)} with: omni plugin install <path-or-package>`;
|
|
24684
|
+
}
|
|
24685
|
+
const rows = plugins.map((plugin) => [
|
|
24686
|
+
plugin.id,
|
|
24687
|
+
plugin.name ?? "\u2014",
|
|
24688
|
+
plugin.version ?? "\u2014",
|
|
24689
|
+
plugin.api === null ? "\u2014" : String(plugin.api),
|
|
24690
|
+
plugin.sdk ?? "\u2014",
|
|
24691
|
+
plugin.capabilities.length === 0 ? "\u2014" : plugin.capabilities.join(","),
|
|
24692
|
+
pluginState(ctx, plugin)
|
|
24693
|
+
]);
|
|
24694
|
+
const body2 = table([
|
|
24695
|
+
{ header: "ID" },
|
|
24696
|
+
{ header: "NAME" },
|
|
24697
|
+
{ header: "VERSION" },
|
|
24698
|
+
{ header: "API" },
|
|
24699
|
+
{ header: "SDK" },
|
|
24700
|
+
{ header: "CAPABILITIES" },
|
|
24701
|
+
{ header: "STATE" }
|
|
24702
|
+
], rows);
|
|
24703
|
+
const notes = plugins.flatMap((plugin) => plugin.problems.map((problem) => `${plugin.id}: ${paint(ctx, problem.fatal ? "red" : "yellow", problem.reason)}`));
|
|
24704
|
+
return notes.length === 0 ? body2 : `${body2}
|
|
24705
|
+
|
|
24706
|
+
${notes.join(`
|
|
24707
|
+
`)}`;
|
|
24708
|
+
});
|
|
24709
|
+
}
|
|
24710
|
+
};
|
|
24711
|
+
var pluginVerify = {
|
|
24712
|
+
usage: "plugin verify <id>",
|
|
24713
|
+
summary: "Run every load-time check on one plugin without loading it",
|
|
24714
|
+
async run(args, { ctx, writer }) {
|
|
24715
|
+
const id = requirePositional(args, 0, "plugin id");
|
|
24716
|
+
const report = verifyPlugin(pluginDeps(), ctx.root.root, id);
|
|
24717
|
+
emit(ctx, writer, report, () => {
|
|
24718
|
+
const manifest = report.manifest;
|
|
24719
|
+
const head = fields([
|
|
24720
|
+
["id", report.id],
|
|
24721
|
+
["path", report.path],
|
|
24722
|
+
["name", manifest?.name ?? "\u2014"],
|
|
24723
|
+
["version", manifest?.version ?? "\u2014"],
|
|
24724
|
+
["api", manifest === null ? "\u2014" : String(manifest.api)],
|
|
24725
|
+
["sdk", manifest?.sdk ?? "\u2014"],
|
|
24726
|
+
["server", manifest?.server ?? "\u2014"],
|
|
24727
|
+
["ui", manifest?.ui ?? "\u2014"],
|
|
24728
|
+
[
|
|
24729
|
+
"capabilities",
|
|
24730
|
+
manifest === null || manifest.capabilities.length === 0 ? "\u2014" : manifest.capabilities.join(",")
|
|
24731
|
+
],
|
|
24732
|
+
["origins", manifest?.origins?.join(",") ?? "\u2014"]
|
|
24733
|
+
]);
|
|
24734
|
+
if (report.problems.length === 0) {
|
|
24735
|
+
return `${head}
|
|
24736
|
+
|
|
24737
|
+
${state(ctx, true, "ok")}: this plugin would load`;
|
|
24738
|
+
}
|
|
24739
|
+
const lines = report.problems.map((problem) => ` ${paint(ctx, problem.fatal ? "red" : "yellow", problem.check)} ${problem.reason}`);
|
|
24740
|
+
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");
|
|
24741
|
+
return `${head}
|
|
24742
|
+
|
|
24743
|
+
${lines.join(`
|
|
24744
|
+
`)}
|
|
24745
|
+
|
|
24746
|
+
${verdict}`;
|
|
24747
|
+
});
|
|
24748
|
+
if (!report.loadable)
|
|
24749
|
+
throw new CliError(`plugin "${id}" would not load`);
|
|
24750
|
+
}
|
|
24751
|
+
};
|
|
24752
|
+
var pluginInstall = {
|
|
24753
|
+
usage: "plugin install <path|tarball|https url|package[@version]> [--registry <url>]",
|
|
24754
|
+
summary: "Unpack a plugin into this installation, running nothing from it",
|
|
24755
|
+
options: { registry: { type: "string" } },
|
|
24756
|
+
async run(args, { ctx, writer }) {
|
|
24757
|
+
const spec = requirePositional(args, 0, "plugin directory, tarball, url, or package name");
|
|
24758
|
+
const result = await installPlugin(installDeps(ctx, args), ctx.root.root, spec);
|
|
24759
|
+
emit(ctx, writer, result, () => {
|
|
24760
|
+
note(ctx, writer, paint(ctx, "dim", "no code from the package was executed; verify it before restarting"));
|
|
24761
|
+
const verb = result.replaced ? "replaced" : "installed";
|
|
24762
|
+
return [
|
|
24763
|
+
`${verb} ${result.name} ${result.version} at ${result.path}`,
|
|
24764
|
+
paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart")
|
|
24765
|
+
].join(`
|
|
24766
|
+
`);
|
|
24767
|
+
});
|
|
24768
|
+
}
|
|
24769
|
+
};
|
|
24770
|
+
var pluginRemove = {
|
|
24771
|
+
usage: "plugin remove <id> [--purge]",
|
|
24772
|
+
summary: "Remove a plugin, optionally dropping its stored data too",
|
|
24773
|
+
options: { purge: { type: "boolean" } },
|
|
24774
|
+
async run(args, { ctx, writer, prompt }) {
|
|
24775
|
+
const id = requirePositional(args, 0, "plugin id");
|
|
24776
|
+
const purge = boolFlag(args.values, "purge");
|
|
24777
|
+
const question = purge ? `remove plugin "${id}" AND permanently drop its stored data?` : `remove plugin "${id}"? its stored data is kept`;
|
|
24778
|
+
if (!await prompt.confirm(question))
|
|
24779
|
+
throw new CliError("cancelled");
|
|
24780
|
+
const deps = purge ? { fs: nodePluginFs(), store: await ctx.store() } : { fs: nodePluginFs() };
|
|
24781
|
+
const result = removePlugin(deps, ctx.root.root, id, { purge });
|
|
24782
|
+
emit(ctx, writer, result, () => {
|
|
24783
|
+
const lines = [];
|
|
24784
|
+
lines.push(result.removed ? `removed ${id}` : `no directory for ${id} to remove`);
|
|
24785
|
+
if (purge) {
|
|
24786
|
+
lines.push(result.droppedTables.length === 0 ? "no stored tables to drop" : `dropped ${result.droppedTables.length} table(s): ${result.droppedTables.join(", ")}`);
|
|
24787
|
+
} else {
|
|
24788
|
+
lines.push(paint(ctx, "dim", "stored tables and migration history kept; --purge drops them"));
|
|
24789
|
+
}
|
|
24790
|
+
lines.push(paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart"));
|
|
24791
|
+
return lines.join(`
|
|
24792
|
+
`);
|
|
24793
|
+
});
|
|
24794
|
+
}
|
|
24795
|
+
};
|
|
24796
|
+
function doctorPluginDeps() {
|
|
24797
|
+
return pluginDeps();
|
|
24798
|
+
}
|
|
24799
|
+
function pluginDoctorLines(ctx, plugins) {
|
|
24800
|
+
return plugins.map((plugin) => {
|
|
24801
|
+
const version2 = plugin.version === null ? "" : ` ${plugin.version}`;
|
|
24802
|
+
const api2 = plugin.api === null ? "api ?" : `api ${plugin.api}`;
|
|
24803
|
+
const sdk = plugin.sdk === null ? "no ui" : `sdk ${plugin.sdk}`;
|
|
24804
|
+
const reasons = plugin.problems.filter((problem) => problem.fatal).map((p) => p.reason);
|
|
24805
|
+
const warnings = plugin.problems.length - reasons.length;
|
|
24806
|
+
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");
|
|
24807
|
+
return `${plugin.id}${version2} (${api2}, ${sdk}) ${verdict}`;
|
|
24808
|
+
});
|
|
24809
|
+
}
|
|
23719
24810
|
|
|
23720
24811
|
// apps/cli/src/commands/quota.ts
|
|
23721
24812
|
var WINDOW_ORDER = {
|
|
@@ -23820,7 +24911,7 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
23820
24911
|
|
|
23821
24912
|
// apps/cli/src/runtime.ts
|
|
23822
24913
|
import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
|
|
23823
|
-
import { join as
|
|
24914
|
+
import { join as join7 } from "path";
|
|
23824
24915
|
var runCommand = async (argv) => {
|
|
23825
24916
|
const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
|
|
23826
24917
|
const [stdout, stderr, code] = await Promise.all([
|
|
@@ -23877,9 +24968,9 @@ function processAlive(pid) {
|
|
|
23877
24968
|
}
|
|
23878
24969
|
function gatewayEntrypoint(root, cliDir = import.meta.dir) {
|
|
23879
24970
|
const candidates = [
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
24971
|
+
join7(root, "apps", "gateway", "src", "index.ts"),
|
|
24972
|
+
join7(cliDir, "..", "gateway.js"),
|
|
24973
|
+
join7(cliDir, "gateway.js")
|
|
23883
24974
|
];
|
|
23884
24975
|
return candidates.find((path) => existsSync4(path)) ?? null;
|
|
23885
24976
|
}
|
|
@@ -24016,6 +25107,15 @@ async function rollupState(ctx) {
|
|
|
24016
25107
|
return null;
|
|
24017
25108
|
}
|
|
24018
25109
|
}
|
|
25110
|
+
async function orphanTables(ctx) {
|
|
25111
|
+
if (ctx.configError !== null || !existsSync5(ctx.databasePath))
|
|
25112
|
+
return null;
|
|
25113
|
+
try {
|
|
25114
|
+
return orphanPluginTables(doctorPluginDeps(), ctx.root.root, await ctx.store());
|
|
25115
|
+
} catch {
|
|
25116
|
+
return null;
|
|
25117
|
+
}
|
|
25118
|
+
}
|
|
24019
25119
|
var doctor = {
|
|
24020
25120
|
usage: "doctor",
|
|
24021
25121
|
summary: "Check what this CLI resolved, and whether it can do anything with it",
|
|
@@ -24026,6 +25126,8 @@ var doctor = {
|
|
|
24026
25126
|
const unit = unitInstalled(deps);
|
|
24027
25127
|
const status2 = await status(deps);
|
|
24028
25128
|
const usageRollup = await rollupState(ctx);
|
|
25129
|
+
const plugins = listPlugins(doctorPluginDeps(), deps.root);
|
|
25130
|
+
const orphans = await orphanTables(ctx);
|
|
24029
25131
|
const checks3 = {
|
|
24030
25132
|
root: deps.root,
|
|
24031
25133
|
rootSource: ctx.root.source,
|
|
@@ -24040,6 +25142,9 @@ var doctor = {
|
|
|
24040
25142
|
supervisor: status2.supervisor,
|
|
24041
25143
|
running: status2.running,
|
|
24042
25144
|
consoleSource: consoleSource(deps).source,
|
|
25145
|
+
pluginsDir: pluginsDir(deps.root),
|
|
25146
|
+
plugins,
|
|
25147
|
+
orphanPluginTables: orphans,
|
|
24043
25148
|
warnings: ctx.warnings
|
|
24044
25149
|
};
|
|
24045
25150
|
emit(ctx, writer, checks3, () => {
|
|
@@ -24061,6 +25166,11 @@ var doctor = {
|
|
|
24061
25166
|
["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
|
|
24062
25167
|
["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
|
|
24063
25168
|
["console log", sourceHint(checks3.consoleSource)],
|
|
25169
|
+
...plugins.length === 0 ? [["plugins", paint(ctx, "dim", "none")]] : pluginDoctorLines(ctx, plugins).map((line, index) => [index === 0 ? "plugins" : "", line]),
|
|
25170
|
+
[
|
|
25171
|
+
"orphan plugin tables",
|
|
25172
|
+
orphans === null ? paint(ctx, "dim", "not checked") : orphans.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${orphans.length}: ${orphans.join(", ")}`)
|
|
25173
|
+
],
|
|
24064
25174
|
...checks3.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
|
|
24065
25175
|
]);
|
|
24066
25176
|
});
|
|
@@ -24141,7 +25251,7 @@ var settingsSet = {
|
|
|
24141
25251
|
};
|
|
24142
25252
|
|
|
24143
25253
|
// apps/cli/src/commands/setup.ts
|
|
24144
|
-
import { join as
|
|
25254
|
+
import { join as join8 } from "path";
|
|
24145
25255
|
var OPTIONS = {
|
|
24146
25256
|
dir: { type: "string" },
|
|
24147
25257
|
key: { type: "string" },
|
|
@@ -24173,7 +25283,7 @@ ${f.contents}`).join(`
|
|
|
24173
25283
|
}
|
|
24174
25284
|
}
|
|
24175
25285
|
function at(dir, file2) {
|
|
24176
|
-
return { path:
|
|
25286
|
+
return { path: join8(dir, file2.path), contents: file2.contents };
|
|
24177
25287
|
}
|
|
24178
25288
|
async function promptMapping(models, prompt) {
|
|
24179
25289
|
const choices = models.map(({ model }) => model.id).join(", ");
|
|
@@ -24207,11 +25317,11 @@ var setupClaude = {
|
|
|
24207
25317
|
options: OPTIONS,
|
|
24208
25318
|
async run(args, { ctx, writer, prompt, setupFs }) {
|
|
24209
25319
|
const models = await described(ctx);
|
|
24210
|
-
const dir = stringFlag(args.values, "dir") ??
|
|
25320
|
+
const dir = stringFlag(args.values, "dir") ?? join8(setupFs.homeDir, ".claude");
|
|
24211
25321
|
const key = stringFlag(args.values, "key");
|
|
24212
25322
|
const dryRun2 = boolFlag(args.values, "dry-run");
|
|
24213
25323
|
const mapping = await promptMapping(models, prompt);
|
|
24214
|
-
const path =
|
|
25324
|
+
const path = join8(dir, "settings.json");
|
|
24215
25325
|
const file2 = claudeSettings(models, {
|
|
24216
25326
|
baseUrl: baseUrl(ctx),
|
|
24217
25327
|
discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
|
|
@@ -24466,6 +25576,10 @@ var COMMANDS = {
|
|
|
24466
25576
|
"keys create": keysCreate,
|
|
24467
25577
|
"keys limits": keysLimits,
|
|
24468
25578
|
"keys revoke": keysRevoke,
|
|
25579
|
+
"plugin list": pluginList,
|
|
25580
|
+
"plugin verify": pluginVerify,
|
|
25581
|
+
"plugin install": pluginInstall,
|
|
25582
|
+
"plugin remove": pluginRemove,
|
|
24469
25583
|
"settings get": settingsGet,
|
|
24470
25584
|
"settings set": settingsSet,
|
|
24471
25585
|
"setup claude": setupClaude,
|
|
@@ -24521,6 +25635,7 @@ var GROUPS = [
|
|
|
24521
25635
|
{ title: "Accounts", prefixes: ["connect", "credentials "] },
|
|
24522
25636
|
{ title: "Models", prefixes: ["models "] },
|
|
24523
25637
|
{ title: "Keys", prefixes: ["keys "] },
|
|
25638
|
+
{ title: "Plugins", prefixes: ["plugin "] },
|
|
24524
25639
|
{ title: "Configuration", prefixes: ["settings ", "admin ", "db "] },
|
|
24525
25640
|
{ title: "Reports", prefixes: ["usage", "quota"] }
|
|
24526
25641
|
];
|
|
@@ -24577,10 +25692,10 @@ async function readHidden() {
|
|
|
24577
25692
|
stdin.setRawMode?.(true);
|
|
24578
25693
|
stdin.resume();
|
|
24579
25694
|
try {
|
|
24580
|
-
const
|
|
25695
|
+
const decoder3 = new TextDecoder;
|
|
24581
25696
|
let line = "";
|
|
24582
25697
|
for await (const chunk of stdin) {
|
|
24583
|
-
const text =
|
|
25698
|
+
const text = decoder3.decode(chunk, { stream: true });
|
|
24584
25699
|
for (const char of text) {
|
|
24585
25700
|
if (char === "\r" || char === `
|
|
24586
25701
|
`)
|
|
@@ -24601,10 +25716,10 @@ async function readHidden() {
|
|
|
24601
25716
|
}
|
|
24602
25717
|
}
|
|
24603
25718
|
async function readLine() {
|
|
24604
|
-
const
|
|
25719
|
+
const decoder3 = new TextDecoder;
|
|
24605
25720
|
let line = "";
|
|
24606
25721
|
for await (const chunk of process.stdin) {
|
|
24607
|
-
const text =
|
|
25722
|
+
const text = decoder3.decode(chunk, { stream: true });
|
|
24608
25723
|
const newline = text.indexOf(`
|
|
24609
25724
|
`);
|
|
24610
25725
|
if (newline >= 0)
|
|
@@ -24700,10 +25815,10 @@ async function run(argv, writer, options = {}) {
|
|
|
24700
25815
|
setupFs: options.setupFs ?? {
|
|
24701
25816
|
homeDir: homedir3(),
|
|
24702
25817
|
cwd: process.cwd(),
|
|
24703
|
-
read: (path) => existsSync6(path) ?
|
|
25818
|
+
read: (path) => existsSync6(path) ? readFileSync4(path, "utf8") : null,
|
|
24704
25819
|
write: (path, contents) => atomicWriteFile(path, contents, {
|
|
24705
25820
|
mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
|
|
24706
|
-
write: (temporary, data) =>
|
|
25821
|
+
write: (temporary, data) => writeFileSync3(temporary, data),
|
|
24707
25822
|
rename: renameSync2,
|
|
24708
25823
|
remove: (temporary) => rmSync3(temporary, { force: true })
|
|
24709
25824
|
})
|