@ai-ecoverse/slicc-kernel 1.176.2 → 1.178.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.
@@ -1611,10 +1611,10 @@ var RemoteWatch = class {
1611
1611
 
1612
1612
  // src/fs/view.ts
1613
1613
  function view(target, overrides) {
1614
- const keys = new Set(Object.keys(overrides));
1614
+ const keys2 = new Set(Object.keys(overrides));
1615
1615
  return new Proxy(target, {
1616
1616
  get(t, prop, receiver) {
1617
- if (keys.has(prop)) return overrides[prop];
1617
+ if (keys2.has(prop)) return overrides[prop];
1618
1618
  return Reflect.get(t, prop, receiver);
1619
1619
  },
1620
1620
  set(t, prop, value2) {
@@ -2796,8 +2796,8 @@ var OpfsFs = class {
2796
2796
  const [entry] = await this.meta.get([target]);
2797
2797
  if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
2798
2798
  const kept = fresh3 ? cached : this.files.get(target);
2799
- const usable2 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
2800
- return [usable2 ? kept : void 0, target, true];
2799
+ const usable3 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
2800
+ return [usable3 ? kept : void 0, target, true];
2801
2801
  }
2802
2802
  async writeAt(handle, target, path, offset, bytes) {
2803
2803
  await this.counting(target);
@@ -3599,8 +3599,8 @@ var OpfsFs = class {
3599
3599
  renamingDirs = /* @__PURE__ */ new Set();
3600
3600
  intents = 0;
3601
3601
  async intend(rename, at = `${RENAMES2}/${crypto.randomUUID()}`) {
3602
- const record2 = { ...rename, at: Date.now() * 1e3 + this.intents++ % 1e3 };
3603
- await this.meta.update(at, () => ({ path: at, rename: record2 }));
3602
+ const record3 = { ...rename, at: Date.now() * 1e3 + this.intents++ % 1e3 };
3603
+ await this.meta.update(at, () => ({ path: at, rename: record3 }));
3604
3604
  return at;
3605
3605
  }
3606
3606
  async rollBack(from, to, intents) {
@@ -4145,9 +4145,9 @@ async function same(a, b) {
4145
4145
  return false;
4146
4146
  }
4147
4147
  }
4148
- async function matches(record2, root, at) {
4149
- if (record2.handle) return same(record2.handle, root);
4150
- return "path" in at && record2.path !== void 0 && samePath(record2.path, at.path);
4148
+ async function matches(record3, root, at) {
4149
+ if (record3.handle) return same(record3.handle, root);
4150
+ return "path" in at && record3.path !== void 0 && samePath(record3.path, at.path);
4151
4151
  }
4152
4152
  var nameOf = (id2) => `${META_DB}:${id2}`;
4153
4153
  function done(tx) {
@@ -4190,7 +4190,7 @@ async function rootId(factory, root, locks = globalThis.navigator?.locks, top) {
4190
4190
  const all = await request(
4191
4191
  db.transaction(ROOTS_STORE, "readonly").objectStore(ROOTS_STORE).getAll()
4192
4192
  );
4193
- for (const record2 of all) if (await matches(record2, root, at)) return record2.id;
4193
+ for (const record3 of all) if (await matches(record3, root, at)) return record3.id;
4194
4194
  const id2 = crypto.randomUUID();
4195
4195
  if (await used(root)) await seed(factory, nameOf(id2));
4196
4196
  const tx = db.transaction(ROOTS_STORE, "readwrite");
@@ -5673,9 +5673,9 @@ function changeCred(cur, change) {
5673
5673
  }
5674
5674
  const privileged = cur.euid === 0;
5675
5675
  const next3 = copyCred(cur);
5676
- for (const keys of [UIDS, GIDS]) {
5677
- const own = keys.map((key) => cur[key]);
5678
- for (const key of keys) {
5676
+ for (const keys2 of [UIDS, GIDS]) {
5677
+ const own = keys2.map((key) => cur[key]);
5678
+ for (const key of keys2) {
5679
5679
  const value2 = id(change[key]);
5680
5680
  if (value2 === void 0) continue;
5681
5681
  if (!privileged && !own.includes(value2)) throw fsError("EPERM", `${key} ${value2}`);
@@ -8682,12 +8682,12 @@ function next(p) {
8682
8682
  var ANY = [[">=", [0, 0, 0, []]]];
8683
8683
  var NOTHING = [["<", [0, 0, 0, ["0"]]]];
8684
8684
  function plain(op, p) {
8685
- const exact = !p.parts.includes(void 0);
8685
+ const exact2 = !p.parts.includes(void 0);
8686
8686
  const low = floor(p);
8687
8687
  const high = next(p);
8688
8688
  if (op === ">=") return [[">=", low]];
8689
- if (op === "<") return [["<", exact ? low : [low[0], low[1], low[2], ["0"]]]];
8690
- if (exact) return [[op === "" ? "=" : op, low]];
8689
+ if (op === "<") return [["<", exact2 ? low : [low[0], low[1], low[2], ["0"]]]];
8690
+ if (exact2) return [[op === "" ? "=" : op, low]];
8691
8691
  if (op === ">") return high ? [[">=", high]] : NOTHING;
8692
8692
  if (op === "<=") return high ? [["<", high]] : ANY;
8693
8693
  return high ? [
@@ -8774,7 +8774,7 @@ function satisfies(version, range2) {
8774
8774
  }
8775
8775
 
8776
8776
  // src/version.ts
8777
- var KERNEL_VERSION = true ? "1.176.2" : "0.0.0-dev";
8777
+ var KERNEL_VERSION = true ? "1.178.0" : "0.0.0-dev";
8778
8778
  var OS_RELEASE = "6.0.0-slicc";
8779
8779
  function procVersion() {
8780
8780
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -9364,7 +9364,7 @@ function intlZone(name2) {
9364
9364
  minute: "numeric",
9365
9365
  second: "numeric"
9366
9366
  });
9367
- const exact = (at) => {
9367
+ const exact2 = (at) => {
9368
9368
  const parts2 = {};
9369
9369
  for (const part of format.formatToParts(at)) parts2[part.type] = Number(part.value);
9370
9370
  const wall = Date.UTC(
@@ -9384,12 +9384,12 @@ function intlZone(name2) {
9384
9384
  const bucket = Math.floor(at / BUCKET_MS);
9385
9385
  let known = buckets.get(bucket);
9386
9386
  if (known === void 0) {
9387
- const start2 = exact(bucket * BUCKET_MS);
9388
- known = start2 === exact(bucket * BUCKET_MS + BUCKET_MS - MINUTE_MS) ? start2 : null;
9387
+ const start2 = exact2(bucket * BUCKET_MS);
9388
+ known = start2 === exact2(bucket * BUCKET_MS + BUCKET_MS - MINUTE_MS) ? start2 : null;
9389
9389
  if (buckets.size >= CACHE_MAX) buckets.clear();
9390
9390
  buckets.set(bucket, known);
9391
9391
  }
9392
- return known ?? exact(at);
9392
+ return known ?? exact2(at);
9393
9393
  }
9394
9394
  };
9395
9395
  }
@@ -10619,11 +10619,11 @@ var CronService = class {
10619
10619
  const now = this.clock.now();
10620
10620
  const system = this.host.timeZone;
10621
10621
  const due2 = [];
10622
- const keys = /* @__PURE__ */ new Set();
10622
+ const keys2 = /* @__PURE__ */ new Set();
10623
10623
  let next3 = now + WAKE_MS;
10624
10624
  for (const entry of loaded) {
10625
10625
  const key = keyOf2(entry);
10626
- keys.add(key);
10626
+ keys2.add(key);
10627
10627
  const { when } = entry.job;
10628
10628
  if (when.reboot) {
10629
10629
  if (this.rebooted || stamp.reboot[key] === this.host.bootId) continue;
@@ -10649,7 +10649,7 @@ var CronService = class {
10649
10649
  }
10650
10650
  this.rebooted = true;
10651
10651
  for (const map of [stamp.jobs, stamp.reboot, stamp.when])
10652
- for (const key of Object.keys(map)) if (!keys.has(key)) delete map[key];
10652
+ for (const key of Object.keys(map)) if (!keys2.has(key)) delete map[key];
10653
10653
  await this.writeStamp(stamp);
10654
10654
  for (const each of due2) void this.spawn(each);
10655
10655
  return next3;
@@ -11755,16 +11755,16 @@ function missing4(err) {
11755
11755
  return err?.code === "ENOENT";
11756
11756
  }
11757
11757
  function synthetic(directory, path) {
11758
- const epoch = /* @__PURE__ */ new Date(0);
11758
+ const epoch2 = /* @__PURE__ */ new Date(0);
11759
11759
  return {
11760
11760
  isFile: !directory,
11761
11761
  isDirectory: directory,
11762
11762
  isSymbolicLink: false,
11763
11763
  size: 0,
11764
11764
  mode: (directory ? S_IFDIR2 : S_IFREG2) | 493,
11765
- mtime: epoch,
11766
- atime: epoch,
11767
- ctime: epoch,
11765
+ mtime: epoch2,
11766
+ atime: epoch2,
11767
+ ctime: epoch2,
11768
11768
  ino: inodeOf(path)
11769
11769
  };
11770
11770
  }
@@ -11983,16 +11983,16 @@ function missing5(err) {
11983
11983
  return err?.code === "ENOENT";
11984
11984
  }
11985
11985
  function statOf(path, bytes) {
11986
- const epoch = /* @__PURE__ */ new Date(0);
11986
+ const epoch2 = /* @__PURE__ */ new Date(0);
11987
11987
  return {
11988
11988
  isFile: true,
11989
11989
  isDirectory: false,
11990
11990
  isSymbolicLink: false,
11991
11991
  size: bytes.length,
11992
11992
  mode: S_IFREG3 | 420,
11993
- mtime: epoch,
11994
- atime: epoch,
11995
- ctime: epoch,
11993
+ mtime: epoch2,
11994
+ atime: epoch2,
11995
+ ctime: epoch2,
11996
11996
  ino: inodeOf(path),
11997
11997
  nlink: 1,
11998
11998
  uid: 0,
@@ -14048,11 +14048,11 @@ var HeldPaths = class extends Map {
14048
14048
  handlesAt(path) {
14049
14049
  return [...this.handles].filter(([, key]) => key === path).map(([id2]) => id2);
14050
14050
  }
14051
- doubt(over, exact = false) {
14051
+ doubt(over, exact2 = false) {
14052
14052
  if (this.bound.size === 0) return;
14053
14053
  for (const [handle, key] of this.handles) {
14054
14054
  if (!this.bound.has(handle) || this.keptHandles.has(handle)) continue;
14055
- if (exact ? key === over : within7(key, over)) this.doubted.set(handle, ++this.doubts);
14055
+ if (exact2 ? key === over : within7(key, over)) this.doubted.set(handle, ++this.doubts);
14056
14056
  }
14057
14057
  }
14058
14058
  boundIno(handle) {
@@ -17907,8 +17907,8 @@ function refused2(engine, session, err, opening2) {
17907
17907
  const code = verifyCode(flags2);
17908
17908
  if (code && flags2 !== -1)
17909
17909
  return new KernelError("EPROTO", `${code}: ${engine.verifyInfo(flags2).trim()}`);
17910
- const record2 = recordRefusal(opening2);
17911
- if (record2) return record2;
17910
+ const record3 = recordRefusal(opening2);
17911
+ if (record3) return record3;
17912
17912
  const text = err instanceof TlsError ? err.message : String(err);
17913
17913
  return new KernelError("EPROTO", `ERR_SSL_HANDSHAKE_FAILURE: ${text}${alertOf(session)}`);
17914
17914
  }
@@ -18801,8 +18801,8 @@ var SCRUB_IDLE_MS = 50;
18801
18801
  var SCRUB_NONBLOCK_MAX = 64 * 1024;
18802
18802
  var encoder5 = new TextEncoder();
18803
18803
  function scrubberOf(secrets, sameLength = false) {
18804
- const usable2 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
18805
- return usable2.length > 0 ? compile(usable2, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
18804
+ const usable3 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
18805
+ return usable3.length > 0 ? compile(usable3, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
18806
18806
  }
18807
18807
  async function writeAll2(file, bytes) {
18808
18808
  const write = file.write;
@@ -19023,6 +19023,120 @@ var SessionScrubs = class {
19023
19023
  }
19024
19024
  };
19025
19025
 
19026
+ // src/kernel/sealed-oauth.ts
19027
+ var SEALED_OAUTH_TIMEOUT_MS = 45e3;
19028
+ var SEALED_OAUTH_OPS = /* @__PURE__ */ new Set([
19029
+ "oauthGrant",
19030
+ "oauthStatus",
19031
+ "oauthRefresh",
19032
+ "oauthRemove"
19033
+ ]);
19034
+ var MESSAGES = {
19035
+ EINVAL: "Invalid OAuth request",
19036
+ ENOENT: "OAuth credential not found",
19037
+ EEXIST: "OAuth credential identity conflicts",
19038
+ EACCES: "OAuth operation refused",
19039
+ EAGAIN: "OAuth credential changed; try again",
19040
+ ENOTSUP: "OAuth operation is not supported",
19041
+ ENOTCONN: "Vault is unavailable",
19042
+ ETIMEDOUT: "Vault request timed out",
19043
+ EIO: "Invalid vault reply"
19044
+ };
19045
+ var CAUSES = {
19046
+ "vault-locked": "Vault is locked",
19047
+ "oauth-reauth-required": "OAuth sign-in is required",
19048
+ "oauth-refresh-failed": "OAuth refresh failed"
19049
+ };
19050
+ var COMMON = ["v", "op", "caller"];
19051
+ var IDENTITY = [...COMMON, "name", "provider", "account"];
19052
+ var GRANT = [...IDENTITY, "accessToken", "refreshToken", "tokenType", "expiresAt", "oauth"];
19053
+ var CONFIG = ["tokenUrl", "clientId", "authMethod", "scope", "resource"];
19054
+ var UNSUPPORTED = ["auth", "clientSecret", "fields"];
19055
+ var STATES2 = /* @__PURE__ */ new Set([
19056
+ "ready",
19057
+ "refreshing",
19058
+ "expired",
19059
+ "reauth-required",
19060
+ "removed",
19061
+ "locked"
19062
+ ]);
19063
+ var NAME3 = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
19064
+ var PROVIDER = /^[a-z0-9][a-z0-9-]{0,127}$/;
19065
+ var PLACEHOLDER2 = /^fnox_proxy_[0-9a-f]{24}$/;
19066
+ var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
19067
+ function record(value2) {
19068
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
19069
+ }
19070
+ function keys(value2, allowed) {
19071
+ return Object.keys(value2).every((key) => allowed.includes(key));
19072
+ }
19073
+ function exact(value2, fields) {
19074
+ return Object.keys(value2).length === fields.length && keys(value2, fields);
19075
+ }
19076
+ function nonempty(value2) {
19077
+ return typeof value2 === "string" && value2.length > 0;
19078
+ }
19079
+ function epoch(value2) {
19080
+ return typeof value2 === "number" && Number.isSafeInteger(value2) && value2 >= 0;
19081
+ }
19082
+ function sealedOAuthOperation(op) {
19083
+ return op === "capabilities" || SEALED_OAUTH_OPS.has(op);
19084
+ }
19085
+ function validateSealedOAuth(req) {
19086
+ const config = req.oauth;
19087
+ if (UNSUPPORTED.some((key) => Object.hasOwn(req, key)) || record(config) && UNSUPPORTED.some((key) => Object.hasOwn(config, key)))
19088
+ throw new KernelError("ENOTSUP");
19089
+ const allowed = req.op === "capabilities" ? COMMON : req.op === "oauthGrant" ? GRANT : IDENTITY;
19090
+ if (!keys(req, allowed)) throw new KernelError("EINVAL");
19091
+ if (req.op === "capabilities") return;
19092
+ if (typeof req.name !== "string" || !NAME3.test(req.name) || typeof req.provider !== "string" || !PROVIDER.test(req.provider) || !nonempty(req.account) || req.account.length > 256 || CONTROL.test(req.account))
19093
+ throw new KernelError("EINVAL");
19094
+ if (req.op !== "oauthGrant") return;
19095
+ if (!nonempty(req.accessToken) || req.refreshToken !== void 0 && !nonempty(req.refreshToken) || req.tokenType !== void 0 && typeof req.tokenType !== "string" || req.expiresAt !== void 0 && !epoch(req.expiresAt) || !record(config) || !keys(config, CONFIG) || !nonempty(config.tokenUrl) || !nonempty(config.clientId) || typeof config.authMethod !== "string" || config.scope !== void 0 && typeof config.scope !== "string" || config.resource !== void 0 && !nonempty(config.resource))
19096
+ throw new KernelError("EINVAL");
19097
+ if (config.authMethod !== "none" || typeof req.tokenType === "string" && req.tokenType.toLowerCase() !== "bearer")
19098
+ throw new KernelError("ENOTSUP");
19099
+ }
19100
+ function sealedOAuthError(code = "EIO", cause) {
19101
+ return {
19102
+ v: 1,
19103
+ error: { code, ...cause ? { cause } : {}, message: cause ? CAUSES[cause] : MESSAGES[code] }
19104
+ };
19105
+ }
19106
+ function sealedOAuthReply(op, value2) {
19107
+ try {
19108
+ return checkedReply(op, value2) ?? sealedOAuthError();
19109
+ } catch {
19110
+ return sealedOAuthError();
19111
+ }
19112
+ }
19113
+ function checkedError(value2) {
19114
+ const error = value2.error;
19115
+ if (!exact(value2, ["v", "error"]) || !record(error) || !Object.hasOwn(error, "code") || !Object.hasOwn(error, "message"))
19116
+ return void 0;
19117
+ const { code, cause, message: message3 } = error;
19118
+ if (!keys(error, ["code", "cause", "message"]) || typeof code !== "string" || !Object.hasOwn(MESSAGES, code) || typeof message3 !== "string" || cause !== void 0 && (typeof cause !== "string" || !Object.hasOwn(CAUSES, cause)))
19119
+ return void 0;
19120
+ return sealedOAuthError(code, cause);
19121
+ }
19122
+ function checkedReply(op, value2) {
19123
+ if (!record(value2) || !Object.hasOwn(value2, "v") || value2.v !== 1) return void 0;
19124
+ if (Object.hasOwn(value2, "error")) return checkedError(value2);
19125
+ if (op === "capabilities") {
19126
+ const caps = value2.capabilities;
19127
+ if (!exact(value2, ["v", "capabilities"]) || !record(caps) || !keys(caps, ["kernelOAuth"]))
19128
+ return void 0;
19129
+ if (Object.hasOwn(caps, "kernelOAuth") && caps.kernelOAuth !== 1) return void 0;
19130
+ return { v: 1, capabilities: Object.hasOwn(caps, "kernelOAuth") ? { kernelOAuth: 1 } : {} };
19131
+ }
19132
+ if (op === "oauthRemove")
19133
+ return exact(value2, ["v", "removed"]) && value2.removed === true ? { v: 1, removed: true } : void 0;
19134
+ const { placeholder, state, expiresAt, refreshable } = value2;
19135
+ if (!exact(value2, ["v", "placeholder", "state", "expiresAt", "refreshable"]) || typeof placeholder !== "string" || !PLACEHOLDER2.test(placeholder) || typeof state !== "string" || !STATES2.has(state) || expiresAt !== null && !epoch(expiresAt) || typeof refreshable !== "boolean")
19136
+ return void 0;
19137
+ return { v: 1, placeholder, state, expiresAt, refreshable };
19138
+ }
19139
+
19026
19140
  // src/kernel/sealed-session.ts
19027
19141
  var SESSION_WINDOW_MS = 6e4;
19028
19142
  var SESSION_PLACEHOLDERS = 16;
@@ -19133,8 +19247,8 @@ function sessionValues(reply, asked) {
19133
19247
  const { error, values } = reply;
19134
19248
  if (error !== void 0 || typeof values !== "object" || values === null) return void 0;
19135
19249
  const out = {};
19136
- const keys = Object.keys(values);
19137
- const wanted = asked ?? (keys.length === 1 && SESSION_PLACEHOLDER.test(keys[0]) ? keys : []);
19250
+ const keys2 = Object.keys(values);
19251
+ const wanted = asked ?? (keys2.length === 1 && SESSION_PLACEHOLDER.test(keys2[0]) ? keys2 : []);
19138
19252
  for (const ph of wanted) {
19139
19253
  const value2 = values[ph];
19140
19254
  if (typeof value2 === "string" && value2.length > 0 && !UNSAFE.test(value2)) out[ph] = value2;
@@ -19158,7 +19272,9 @@ var SEALED_OPS = /* @__PURE__ */ new Set([
19158
19272
  "request",
19159
19273
  "status",
19160
19274
  "cancel",
19161
- "materialize"
19275
+ "materialize",
19276
+ "capabilities",
19277
+ ...SEALED_OAUTH_OPS
19162
19278
  ]);
19163
19279
  function tagFields(tags) {
19164
19280
  const [tag] = tags ?? [];
@@ -19193,6 +19309,7 @@ function parseSealed(bytes) {
19193
19309
  throw new KernelError("EINVAL");
19194
19310
  if (req.op === "materialize" && !materializable(req)) throw new KernelError("EINVAL");
19195
19311
  if (req.op === "ask" && !askable(req)) throw new KernelError("EINVAL");
19312
+ if (sealedOAuthOperation(req.op)) validateSealedOAuth(req);
19196
19313
  return req;
19197
19314
  } finally {
19198
19315
  bytes.fill(0);
@@ -19271,7 +19388,7 @@ var SealedChannel = class {
19271
19388
  const ask = fields.op === "ask";
19272
19389
  const timer = setTimeout(
19273
19390
  () => control.abort(ask ? "expired" : "timeout"),
19274
- ask ? this.broker.askTimeoutMs : this.broker.timeoutMs
19391
+ ask ? this.broker.askTimeoutMs : SEALED_OAUTH_OPS.has(fields.op) ? this.broker.oauthTimeoutMs : this.broker.timeoutMs
19275
19392
  );
19276
19393
  const req = { ...fields, v: 1, op: fields.op, caller };
19277
19394
  const asked = ask ? this.asking(fields, caller, requester, control.signal) : void 0;
@@ -19294,24 +19411,26 @@ var SealedChannel = class {
19294
19411
  if (!facts) return errorReply("EIO", "the kernel has no record of this process");
19295
19412
  return this.answer(askRequest(fields, caller, facts), signal);
19296
19413
  }
19297
- stopped(signal) {
19414
+ stopped(signal, oauth = false) {
19298
19415
  if (signal.reason === "timeout")
19299
- return errorReply("ETIMEDOUT", "the vault did not answer within 10 s");
19416
+ return oauth ? sealedOAuthError("ETIMEDOUT") : errorReply("ETIMEDOUT", "the vault did not answer within 10 s");
19300
19417
  return signal.reason === "expired" ? { v: 1, state: "expired" } : void 0;
19301
19418
  }
19302
19419
  async answer(req, signal) {
19303
19420
  const hook = this.broker.hook;
19304
- if (!hook) return errorReply("ENOTCONN", "no vault is connected");
19421
+ const oauth = sealedOAuthOperation(req.op);
19422
+ if (!hook)
19423
+ return oauth ? sealedOAuthError("ENOTCONN") : errorReply("ENOTCONN", "no vault is connected");
19305
19424
  const stopped = new Promise((resolve2) => {
19306
19425
  signal.addEventListener("abort", () => resolve2(void 0), { once: true });
19307
19426
  });
19308
19427
  if (signal.aborted) return void 0;
19309
19428
  const asked = Promise.resolve().then(() => hook(req, { signal })).then(
19310
- (reply2) => isRecord(reply2) ? reply2 : errorReply("EIO", "the vault gave no reply"),
19311
- (err) => errorReply("EIO", lineOf3(err))
19429
+ (reply2) => oauth ? sealedOAuthReply(req.op, reply2) : isRecord(reply2) ? reply2 : errorReply("EIO", "the vault gave no reply"),
19430
+ (err) => oauth ? sealedOAuthError() : errorReply("EIO", lineOf3(err))
19312
19431
  );
19313
19432
  const reply = await Promise.race([asked, stopped]);
19314
- return signal.aborted ? this.stopped(signal) : reply;
19433
+ return signal.aborted ? this.stopped(signal, oauth) : reply;
19315
19434
  }
19316
19435
  drop() {
19317
19436
  this.reply?.fill(0);
@@ -19374,6 +19493,7 @@ var SealedBroker = class {
19374
19493
  tickets = new TicketBook();
19375
19494
  sessions = new SessionScrubs();
19376
19495
  askTimeoutMs = SEALED_ASK_TIMEOUT_MS;
19496
+ oauthTimeoutMs = SEALED_OAUTH_TIMEOUT_MS;
19377
19497
  userOf = async () => void 0;
19378
19498
  factsOf = async () => void 0;
19379
19499
  constructor(hook, users, timeoutMs = SEALED_TIMEOUT_MS, tagOf = () => void 0) {
@@ -19441,10 +19561,60 @@ var SealedBroker = class {
19441
19561
  }
19442
19562
  };
19443
19563
 
19564
+ // src/sudo-timestamp.ts
19565
+ var TIMESTAMPS_MAX = 64;
19566
+ var MINUTE_MS2 = 6e4;
19567
+ function timestampKey(owner, type) {
19568
+ if (type === "global") return "global";
19569
+ const { terminal, parent } = owner;
19570
+ if (type === "tty" && terminal) {
19571
+ return JSON.stringify(["tty", terminal.tty, terminal.sid, terminal.started]);
19572
+ }
19573
+ return parent ? JSON.stringify(["ppid", parent.pid, parent.started]) : void 0;
19574
+ }
19575
+ var Timestamps = class {
19576
+ users = /* @__PURE__ */ new Map();
19577
+ now;
19578
+ constructor(now) {
19579
+ this.now = now;
19580
+ }
19581
+ current(owner, policy) {
19582
+ if (policy.minutes === 0) return false;
19583
+ const key = timestampKey(owner, policy.type);
19584
+ const at = key === void 0 ? void 0 : this.users.get(owner.uid)?.get(key);
19585
+ if (at === void 0) return false;
19586
+ const now = this.now();
19587
+ return at <= now && (policy.minutes < 0 || now - at < policy.minutes * MINUTE_MS2);
19588
+ }
19589
+ stamp(owner, policy) {
19590
+ const key = policy.minutes === 0 ? void 0 : timestampKey(owner, policy.type);
19591
+ if (key === void 0) return false;
19592
+ const now = this.now();
19593
+ const records = this.users.get(owner.uid) ?? /* @__PURE__ */ new Map();
19594
+ this.users.set(owner.uid, records);
19595
+ records.delete(key);
19596
+ records.set(key, now);
19597
+ for (const [old, at] of records) {
19598
+ const stale = policy.minutes > 0 && (at > now || now - at >= policy.minutes * MINUTE_MS2);
19599
+ if (stale || records.size > TIMESTAMPS_MAX) records.delete(old);
19600
+ }
19601
+ return true;
19602
+ }
19603
+ invalidate(owner, type) {
19604
+ const key = timestampKey(owner, type);
19605
+ const records = this.users.get(owner.uid);
19606
+ if (key !== void 0) records?.delete(key);
19607
+ if (records?.size === 0) this.users.delete(owner.uid);
19608
+ }
19609
+ remove(uid) {
19610
+ this.users.delete(uid);
19611
+ }
19612
+ };
19613
+
19444
19614
  // src/sudoers.ts
19445
19615
  var SUDOERS = "/etc/sudoers";
19446
19616
  var SUDOERS_D = "/etc/sudoers.d";
19447
- var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
19617
+ var NAME4 = /^[a-z_][a-z0-9_.-]*$/;
19448
19618
  var WORD = String.raw`(?:'[^']*'|"(?:[^"\\]|\\[\s\S])*"|\\[\s\S]|[^\s'"\\])+`;
19449
19619
  var KERNEL_FORM = new RegExp(
19450
19620
  String.raw`^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()]*)\)\s+)?(?:Cwd\s+(${WORD})\s+)?(?:Stdin\s+(\S+)\s+)?Cmnd(?:\s+(.*))?$`
@@ -19454,14 +19624,16 @@ var RUNAS = /^\(([^()]*)\)\s*/;
19454
19624
  var TAG = /^([A-Z_]+)\s*:\s*/;
19455
19625
  var OPTION = /^([A-Z_]+)=/;
19456
19626
  var PLAIN2 = /^[A-Za-z0-9_@%+=:,./-]+$/;
19457
- var CONTROL = /[\p{Cc}\u2028\u2029]/u;
19627
+ var CONTROL2 = /[\p{Cc}\u2028\u2029]/u;
19458
19628
  var CONTROLS = /[\p{Cc}\u2028\u2029]/gu;
19459
19629
  var SHOWN_MAX = 60;
19460
19630
  var KERNEL_SYNTAX = "[NOPASSWD] <user|%group|ALL> [(<target>)] [Cwd <dir>] [Stdin none] Cmnd <command\u2026|*>";
19461
19631
  var SUDOERS_SYNTAX = "<user|%group|ALL> ALL=[(<target>[,<target>\u2026])] [NOPASSWD:] <command\u2026|ALL>";
19462
19632
  var EXPECTED = `expected '${KERNEL_SYNTAX}' or '${SUDOERS_SYNTAX}'`;
19633
+ var TIMESTAMP_TIMEOUT = 5;
19634
+ var TIMESTAMP_TYPE = "tty";
19463
19635
  function hasControl(text) {
19464
- return CONTROL.test(text);
19636
+ return CONTROL2.test(text);
19465
19637
  }
19466
19638
  function withoutControl(text) {
19467
19639
  return text.replace(CONTROLS, (ch) => {
@@ -19599,7 +19771,7 @@ function commandText(cmnd) {
19599
19771
  }
19600
19772
  function validWho(who) {
19601
19773
  if (who === "ALL") return true;
19602
- return NAME3.test(who.startsWith("%") ? who.slice(1) : who);
19774
+ return NAME4.test(who.startsWith("%") ? who.slice(1) : who);
19603
19775
  }
19604
19776
  function whoError(who) {
19605
19777
  if (validWho(who)) return void 0;
@@ -19632,7 +19804,7 @@ function kernelRules(m) {
19632
19804
  const [, nopasswd, who = "", target = "root", dir, stdin, rest2 = ""] = m;
19633
19805
  const bad = whoError(who);
19634
19806
  if (bad !== void 0) return bad;
19635
- if (target !== "ALL" && !NAME3.test(target)) return targetError(target);
19807
+ if (target !== "ALL" && !NAME4.test(target)) return targetError(target);
19636
19808
  const cwd = dir === void 0 ? void 0 : cwdOf(dir);
19637
19809
  if (cwd === "") return `Cwd expects one absolute directory, not ${shown(dir)}`;
19638
19810
  if (stdin !== void 0 && stdin !== "none") return `Stdin takes none, not ${shown(stdin)}`;
@@ -19656,11 +19828,11 @@ function runasOf(list) {
19656
19828
  return colon < 0 ? "() names no user to run as: name one, or leave the (\u2026) out for root" : `(${list}) runs as the caller with another group, which the kernel's sudo can't do`;
19657
19829
  }
19658
19830
  for (const name2 of names2) {
19659
- if (name2 === "ALL" || NAME3.test(name2)) continue;
19831
+ if (name2 === "ALL" || NAME4.test(name2)) continue;
19660
19832
  return `expected user names or ALL to run as, not ${shown(name2)} (no %group, #uid or !user)`;
19661
19833
  }
19662
19834
  for (const group of groups) {
19663
- if (group === "" || group === "ALL" || NAME3.test(group)) continue;
19835
+ if (group === "" || group === "ALL" || NAME4.test(group)) continue;
19664
19836
  return `expected group names or ALL after ':' in (\u2026), not ${shown(group)}`;
19665
19837
  }
19666
19838
  return names2.includes("ALL") ? ["ALL"] : [...new Set(names2)];
@@ -19742,9 +19914,6 @@ function directiveError(text) {
19742
19914
  return /^#-?\d+\s+\S+\s*=/.test(text) ? "a #uid isn't supported: name the user, %group or ALL" : null;
19743
19915
  }
19744
19916
  const first = /^\S+/.exec(text)?.[0] ?? "";
19745
- if (/^Defaults([:@>!]|$)/.test(first)) {
19746
- return "Defaults aren't supported: the kernel's sudo has no settings, so this changes nothing";
19747
- }
19748
19917
  if (/^[A-Z][A-Za-z]*_Alias$/.test(first)) {
19749
19918
  return "aliases aren't supported: name the users, targets and commands in each line";
19750
19919
  }
@@ -19752,7 +19921,7 @@ function directiveError(text) {
19752
19921
  }
19753
19922
  function recordedBare(line, file, rule) {
19754
19923
  if (rule.cmnd === "*" || rule.cmnd.args !== void 0 || !rule.nopasswd) return false;
19755
- if (file !== `${SUDOERS_D}/${rule.who}` || !NAME3.test(rule.who) || !NAME3.test(rule.target)) {
19924
+ if (file !== `${SUDOERS_D}/${rule.who}` || !NAME4.test(rule.who) || !NAME4.test(rule.target)) {
19756
19925
  return false;
19757
19926
  }
19758
19927
  const head = `NOPASSWD ${rule.who} (${rule.target}) `;
@@ -19770,9 +19939,90 @@ function kernelLine(m, line, file) {
19770
19939
  )
19771
19940
  };
19772
19941
  }
19942
+ var DEFAULTS = /^Defaults(?=[:@>!\s]|$)/;
19943
+ var DEFAULTS_ONLY = `Defaults aren't supported, except timestamp_timeout and timestamp_type for everyone, a user or a %group: this line changes nothing`;
19944
+ var PARAM = /^(!*)([A-Za-z_]+)(?:\s*([-+]?=)\s*(.*))?$/s;
19945
+ var MINUTES = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/;
19946
+ var TYPES = ["tty", "ppid", "global"];
19947
+ function defaultsValue(raw2) {
19948
+ const quoted = /^"(.*)"$/s.exec(raw2);
19949
+ return quoted ? quoted[1] : raw2;
19950
+ }
19951
+ function timeoutParam({ text, negated, op, value: value2 }) {
19952
+ if (negated && op === void 0) return { timestampTimeout: 0 };
19953
+ if (negated || op !== "=" || !MINUTES.test(value2) || !Number.isFinite(Number(value2))) {
19954
+ return `timestamp_timeout takes minutes, such as =5 or =2.5 (0 always asks, below 0 never expires), not ${shown(text)}`;
19955
+ }
19956
+ return { timestampTimeout: Number(value2) };
19957
+ }
19958
+ function typeParam({ text, negated, op, value: value2 }) {
19959
+ if (!negated && op === "=" && TYPES.includes(value2)) {
19960
+ return { timestampType: value2 };
19961
+ }
19962
+ if (value2 === "kernel") return "timestamp_type=kernel isn't supported: use tty, ppid or global";
19963
+ return `timestamp_type takes =tty, =ppid or =global, not ${shown(text)}`;
19964
+ }
19965
+ function defaultsParam(param) {
19966
+ const m = PARAM.exec(param);
19967
+ if (!m) return DEFAULTS_ONLY;
19968
+ const [, bangs = "", name2, op, raw2 = ""] = m;
19969
+ const read = { text: param, negated: bangs !== "", op, value: defaultsValue(raw2.trim()) };
19970
+ const settings = name2 === "timestamp_timeout" ? timeoutParam(read) : name2 === "timestamp_type" ? typeParam(read) : DEFAULTS_ONLY;
19971
+ if (typeof settings === "string") return settings;
19972
+ const value2 = settings.timestampTimeout ?? settings.timestampType;
19973
+ return { settings, text: `${name2}=${value2}` };
19974
+ }
19975
+ function defaultsWho(binding) {
19976
+ if (binding === "") return null;
19977
+ if (!binding.startsWith(":")) return DEFAULTS_ONLY;
19978
+ const who = binding.slice(1).split(",");
19979
+ for (const name2 of who) {
19980
+ if (!validWho(name2))
19981
+ return `expected users, %groups or ALL after Defaults:, not ${shown(name2)}`;
19982
+ }
19983
+ return who;
19984
+ }
19985
+ function defaultsLine(text) {
19986
+ const m = /^Defaults(\S*)(?:\s+(.*))?$/s.exec(text);
19987
+ const who = defaultsWho(m?.[1] ?? "");
19988
+ if (typeof who === "string") return { error: who };
19989
+ const list = (m?.[2] ?? "").trim();
19990
+ if (list === "")
19991
+ return {
19992
+ error: "expected timestamp_timeout=<minutes> or timestamp_type=<type> after Defaults"
19993
+ };
19994
+ const settings = {};
19995
+ const params = [];
19996
+ for (const param of list.split(",")) {
19997
+ const read = defaultsParam(param.trim());
19998
+ if (typeof read === "string") return { error: read };
19999
+ Object.assign(settings, read.settings);
20000
+ params.push(read.text);
20001
+ }
20002
+ return { defaults: { who, settings, params } };
20003
+ }
20004
+ function resolveDefaults(entries, applies) {
20005
+ const everyone = entries.filter((e) => e.who === null);
20006
+ const mine = entries.filter((e) => e.who?.some(applies));
20007
+ const matching = [...everyone, ...mine];
20008
+ const settings = Object.assign({}, ...matching.map((e) => e.settings));
20009
+ return {
20010
+ timestampTimeout: settings.timestampTimeout ?? TIMESTAMP_TIMEOUT,
20011
+ timestampType: settings.timestampType ?? TIMESTAMP_TYPE,
20012
+ matching
20013
+ };
20014
+ }
20015
+ function defaultsText(entries) {
20016
+ const all = resolveDefaults(entries, () => false);
20017
+ return [
20018
+ `Defaults timestamp_timeout=${all.timestampTimeout}, timestamp_type=${all.timestampType}`,
20019
+ ...entries.filter((e) => e.who !== null).map((e) => `Defaults:${e.who.join(",")} ${e.params.join(", ")}`)
20020
+ ];
20021
+ }
19773
20022
  function parseLine(line, file) {
19774
20023
  const text = line.trim();
19775
20024
  if (text === "") return null;
20025
+ if (DEFAULTS.test(text)) return defaultsLine(text);
19776
20026
  const directive = directiveError(text);
19777
20027
  if (directive !== void 0) return directive === null ? null : { error: directive };
19778
20028
  const kernel = KERNEL_FORM.exec(text);
@@ -19784,14 +20034,16 @@ function parseLine(line, file) {
19784
20034
  }
19785
20035
  function parsePolicy(text, file) {
19786
20036
  const rules = [];
20037
+ const defaults2 = [];
19787
20038
  const errors = [];
19788
20039
  for (const [n, line] of text.split("\n").entries()) {
19789
20040
  const parsed2 = parseLine(line, file);
19790
20041
  if (parsed2 === null) continue;
19791
20042
  if ("error" in parsed2) errors.push({ line: n + 1, message: parsed2.error });
20043
+ else if ("defaults" in parsed2) defaults2.push(parsed2.defaults);
19792
20044
  else rules.push(...parsed2.rules);
19793
20045
  }
19794
- return { rules, errors };
20046
+ return { rules, defaults: defaults2, errors };
19795
20047
  }
19796
20048
 
19797
20049
  // src/sudo.ts
@@ -19799,7 +20051,14 @@ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
19799
20051
  var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
19800
20052
  var SUDO_TIMEOUT_MS = 3e5;
19801
20053
  var TERM = /^[A-Za-z0-9._+-]+$/;
19802
- var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
20054
+ var USAGE = [
20055
+ "usage: sudo -K | -k",
20056
+ " sudo -v [-kn] [-u user]",
20057
+ " sudo -l [-kn]",
20058
+ " sudo [-kn] [-u user] [--] command [args...]"
20059
+ ].join("\n");
20060
+ var DONE = { done: true };
20061
+ var NOT_STAMPED = "sudo: approved, but the approver didn't say a person decided, so no timestamp was recorded";
19803
20062
  var DENIED2 = "sudo: approval denied";
19804
20063
  var DENIAL_MAX = 500;
19805
20064
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
@@ -19873,6 +20132,17 @@ async function answerOf(approve, req, signal) {
19873
20132
  return cut === void 0 ? "deny" : await cut;
19874
20133
  }
19875
20134
  }
20135
+ function approvalOf(answer2) {
20136
+ if (answer2 === "once" || answer2 === "always") return { answer: answer2, person: false };
20137
+ if (typeof answer2 !== "object" || answer2 === null) return void 0;
20138
+ try {
20139
+ const { answer: kind, timestamp } = answer2;
20140
+ if (kind !== "once" && kind !== "always" || timestamp !== true) return void 0;
20141
+ return { answer: kind, person: true };
20142
+ } catch {
20143
+ return void 0;
20144
+ }
20145
+ }
19876
20146
  function recordedCommand(argv) {
19877
20147
  const [path = "", ...args] = argv;
19878
20148
  return commandText({ path, args: args.map((a) => [a]) });
@@ -19882,6 +20152,9 @@ function flags(parsed2, arg, next3) {
19882
20152
  const flag = arg[j];
19883
20153
  if (flag === "n") parsed2.nonInteractive = true;
19884
20154
  else if (flag === "l") parsed2.list = true;
20155
+ else if (flag === "v") parsed2.validate = true;
20156
+ else if (flag === "k") parsed2.reset = true;
20157
+ else if (flag === "K") parsed2.removeAll = true;
19885
20158
  else if (flag !== "u") return `sudo: invalid option -- '${flag}'
19886
20159
  ${USAGE}`;
19887
20160
  else {
@@ -19895,7 +20168,15 @@ ${USAGE}`;
19895
20168
  return 0;
19896
20169
  }
19897
20170
  function parseArgs(args) {
19898
- const parsed2 = { list: false, nonInteractive: false, user: "root", command: [] };
20171
+ const parsed2 = {
20172
+ list: false,
20173
+ nonInteractive: false,
20174
+ validate: false,
20175
+ reset: false,
20176
+ removeAll: false,
20177
+ user: "root",
20178
+ command: []
20179
+ };
19899
20180
  let i = 0;
19900
20181
  for (; i < args.length; i++) {
19901
20182
  const arg = args[i];
@@ -19909,8 +20190,17 @@ function parseArgs(args) {
19909
20190
  i += used2;
19910
20191
  }
19911
20192
  parsed2.command = args.slice(i);
19912
- if (parsed2.list ? parsed2.command.length > 0 : parsed2.command.length === 0) return USAGE;
19913
- return parsed2;
20193
+ return usable(parsed2) ? parsed2 : USAGE;
20194
+ }
20195
+ function usable(parsed2) {
20196
+ const command = parsed2.command.length > 0;
20197
+ const modes = [parsed2.list, parsed2.validate, parsed2.removeAll].filter(Boolean).length;
20198
+ if (modes > 1 || parsed2.removeAll && parsed2.reset) return false;
20199
+ if (modes === 1) return !command;
20200
+ return command || parsed2.reset;
20201
+ }
20202
+ function policyOf(settings) {
20203
+ return { type: settings.timestampType, minutes: settings.timestampTimeout };
19914
20204
  }
19915
20205
  function described(rule) {
19916
20206
  const cmnd = rule.cmnd === "*" ? "ALL" : commandText(rule.cmnd);
@@ -19922,9 +20212,11 @@ var Sudo = class {
19922
20212
  o;
19923
20213
  warned = /* @__PURE__ */ new Set();
19924
20214
  pending = /* @__PURE__ */ new Set();
20215
+ timestamps;
19925
20216
  next = 0;
19926
20217
  constructor(options) {
19927
20218
  this.o = options;
20219
+ this.timestamps = new Timestamps(options.now ?? (() => performance.now()));
19928
20220
  }
19929
20221
  warn(message3) {
19930
20222
  if (this.warned.has(message3)) return;
@@ -20011,8 +20303,8 @@ var Sudo = class {
20011
20303
  const code = err.code;
20012
20304
  return skipped(`it can't be read (${typeof code === "string" ? code : "error"})`);
20013
20305
  }
20014
- const { rules, errors } = parsePolicy(text, path);
20015
- return { path, rules, problems: errors, ok: true };
20306
+ const { rules, defaults: defaults2, errors } = parsePolicy(text, path);
20307
+ return { path, rules, defaults: defaults2, problems: errors, ok: true };
20016
20308
  }
20017
20309
  async policy() {
20018
20310
  const files = [await this.policyFile(SUDOERS)];
@@ -20041,7 +20333,10 @@ var Sudo = class {
20041
20333
  }
20042
20334
  }
20043
20335
  if (!detailed && skipped > 0) await caller.report?.(skippedNotice(skipped));
20044
- return files.flatMap((f) => f.rules);
20336
+ return {
20337
+ rules: files.flatMap((f) => f.rules),
20338
+ defaults: files.flatMap((f) => f.defaults ?? [])
20339
+ };
20045
20340
  }
20046
20341
  async check(cred, call) {
20047
20342
  const lines2 = [];
@@ -20054,17 +20349,26 @@ var Sudo = class {
20054
20349
  if (read && problems.length === 0) lines2.push([1, `${path}: parsed OK`]);
20055
20350
  if (problems.length > 0) ok4 = false;
20056
20351
  };
20352
+ const values = (defaults2) => {
20353
+ for (const line of defaultsText(defaults2)) lines2.push([1, line]);
20354
+ };
20057
20355
  if (call.files !== void 0) {
20356
+ const defaults2 = [];
20058
20357
  for (const { name: name2, text } of call.files) {
20059
- report({ path: name2, rules: [], problems: parsePolicy(text).errors, ok: true });
20358
+ const parsed2 = parsePolicy(text);
20359
+ defaults2.push(...parsed2.defaults);
20360
+ report({ path: name2, rules: [], problems: parsed2.errors, ok: true });
20060
20361
  }
20362
+ if (defaults2.length > 0) values(defaults2);
20061
20363
  return { lines: lines2, ok: ok4 };
20062
20364
  }
20063
20365
  if (this.o.mode === "enforce" && cred.euid !== 0) {
20064
20366
  const hint = "check the policy as root (sudo visudo -c), or a file with visudo -c -f <file>";
20065
20367
  return { lines: [[2, `visudo: ${SUDOERS}: Permission denied: ${hint}`]], ok: false };
20066
20368
  }
20067
- for (const file of await this.policy()) report(file);
20369
+ const files = await this.policy();
20370
+ for (const file of files) report(file);
20371
+ values(files.flatMap((f) => f.defaults ?? []));
20068
20372
  return { lines: lines2, ok: ok4 };
20069
20373
  }
20070
20374
  async groupIds() {
@@ -20073,9 +20377,9 @@ var Sudo = class {
20073
20377
  async applies(caller, cred) {
20074
20378
  const gids = await this.groupIds();
20075
20379
  const mine = /* @__PURE__ */ new Set([cred.rgid, cred.egid, ...cred.groups]);
20076
- return (rule) => {
20077
- if (rule.who === "ALL" || rule.who === caller.name) return true;
20078
- const gid = rule.who.startsWith("%") ? gids.get(rule.who.slice(1)) : void 0;
20380
+ return (who) => {
20381
+ if (who === "ALL" || who === caller.name) return true;
20382
+ const gid = who.startsWith("%") ? gids.get(who.slice(1)) : void 0;
20079
20383
  return gid !== void 0 && mine.has(gid);
20080
20384
  };
20081
20385
  }
@@ -20120,18 +20424,40 @@ var Sudo = class {
20120
20424
  const mode = this.o.mode;
20121
20425
  if (parsed2.list) return { list: `sudo: users are '${mode}': commands run as the caller
20122
20426
  ` };
20123
- return { run: parsed2.command };
20427
+ return parsed2.command.length > 0 ? { run: parsed2.command } : DONE;
20124
20428
  }
20125
20429
  const { cred } = caller;
20126
20430
  const policy = await this.policyRules(caller);
20127
20431
  const self = await this.o.users.lookup(cred.ruid);
20128
20432
  if (!self) return failed2("EPERM", `sudo: uid ${cred.ruid} is not in the passwd database`);
20433
+ const owner = {
20434
+ uid: cred.ruid,
20435
+ terminal: caller.terminal,
20436
+ parent: caller.parent
20437
+ };
20438
+ const applies = await this.applies(self, cred);
20439
+ const settings = resolveDefaults(policy.defaults, applies);
20440
+ if (parsed2.removeAll) {
20441
+ this.timestamps.remove(owner.uid);
20442
+ return DONE;
20443
+ }
20444
+ if (parsed2.reset && !parsed2.validate && !parsed2.list && parsed2.command.length === 0) {
20445
+ this.timestamps.invalidate(owner, settings.timestampType);
20446
+ return DONE;
20447
+ }
20129
20448
  const target = await this.lookup(parsed2.user);
20130
20449
  if (!target) return failed2("ENOENT", `sudo: unknown user ${parsed2.user}`);
20131
20450
  const root = cred.euid === 0;
20132
- const rules = root ? [] : policy;
20133
- const applies = await this.applies(self, cred);
20134
- if (parsed2.list) return this.listed(self, root, rules.filter(applies));
20451
+ const rules = root ? [] : policy.rules;
20452
+ const mine = rules.filter((rule) => applies(rule.who));
20453
+ if (parsed2.list) return this.listed(self, root, mine, settings.matching);
20454
+ const session = { caller, self, target, owner, policy: policyOf(settings), parsed: parsed2, signal };
20455
+ if (parsed2.validate) return this.validate(session, root, mine, call.cwd);
20456
+ return this.command(session, root, mine, call);
20457
+ }
20458
+ async command(s, root, mine, call) {
20459
+ const { caller, self, target, parsed: parsed2 } = s;
20460
+ const { cred } = caller;
20135
20461
  const [word = "", ...args] = parsed2.command;
20136
20462
  const cwd = await this.canonical(call.cwd);
20137
20463
  if (cwd === void 0 || !await this.reachable(cred, cwd)) {
@@ -20145,8 +20471,8 @@ var Sudo = class {
20145
20471
  const untrusted2 = await this.untrusted(path, cwd, word);
20146
20472
  if (untrusted2) return untrusted2;
20147
20473
  const argv = [path, ...args];
20148
- const matching = rules.filter(
20149
- (rule) => applies(rule) && (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
20474
+ const matching = mine.filter(
20475
+ (rule) => (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
20150
20476
  );
20151
20477
  const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
20152
20478
  if (root || matching.some((rule) => rule.nopasswd)) return grant();
@@ -20157,23 +20483,67 @@ var Sudo = class {
20157
20483
  `sudo: ${self.name} is not allowed to run ${quoted} as ${target.name}`
20158
20484
  );
20159
20485
  }
20486
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
20487
+ this.timestamps.stamp(s.owner, s.policy);
20488
+ this.logged(s, cwd, argv);
20489
+ return grant();
20490
+ }
20160
20491
  if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
20161
- if (this.pending.has(cred.ruid)) return failed2("EBUSY", "sudo: another request is pending");
20162
- this.pending.add(cred.ruid);
20163
- try {
20164
- const answer2 = await this.ask(caller, self, target, parsed2.command, path, cwd, signal);
20165
- if (typeof answer2 !== "string") return answer2;
20492
+ return this.pendingFor(cred.ruid, async () => {
20493
+ const answer2 = await this.ask(s, { argv: parsed2.command, path, cwd });
20494
+ if ("error" in answer2) return answer2;
20166
20495
  if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
20167
20496
  return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
20168
20497
  }
20169
20498
  const changed = await this.untrusted(path, cwd, word);
20170
20499
  if (changed) return changed;
20171
- if (answer2 === "once") return grant();
20172
- return await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
20500
+ const result = answer2.answer === "once" ? grant() : await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
20501
+ if (!("error" in result)) this.stamped(s, answer2);
20502
+ return result;
20503
+ });
20504
+ }
20505
+ async validate(s, root, mine, dir) {
20506
+ const { self, parsed: parsed2 } = s;
20507
+ if (root) return DONE;
20508
+ if (mine.length === 0) {
20509
+ return failed2("EPERM", `Sorry, user ${self.name} may not run sudo on ${this.o.hostname}.`);
20510
+ }
20511
+ if (mine.every((rule) => rule.nopasswd)) return DONE;
20512
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
20513
+ this.timestamps.stamp(s.owner, s.policy);
20514
+ return DONE;
20515
+ }
20516
+ if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
20517
+ const cwd = await this.canonical(dir) ?? normalizePath(dir);
20518
+ return this.pendingFor(s.caller.cred.ruid, async () => {
20519
+ const answer2 = await this.ask(s, { argv: [], path: "", cwd, validate: true });
20520
+ if ("error" in answer2) return answer2;
20521
+ if (!answer2.person) await s.caller.report?.(NOT_STAMPED);
20522
+ this.stamped(s, answer2);
20523
+ return DONE;
20524
+ });
20525
+ }
20526
+ async pendingFor(uid, work) {
20527
+ if (this.pending.has(uid)) return failed2("EBUSY", "sudo: another request is pending");
20528
+ this.pending.add(uid);
20529
+ try {
20530
+ return await work();
20173
20531
  } finally {
20174
- this.pending.delete(cred.ruid);
20532
+ this.pending.delete(uid);
20175
20533
  }
20176
20534
  }
20535
+ stamped(s, answer2) {
20536
+ if (answer2.person && !s.parsed.reset) this.timestamps.stamp(s.owner, s.policy);
20537
+ }
20538
+ logged(s, cwd, argv) {
20539
+ const { caller, self, target, owner, policy } = s;
20540
+ const type = policy.type === "tty" && !owner.terminal ? "ppid" : policy.type;
20541
+ const tty = owner.terminal?.tty ?? caller.tty ?? "unknown";
20542
+ const command = argv.map(quoteWord).join(" ");
20543
+ (this.o.warn ?? console.warn)(
20544
+ `sudo: ${self.name} : TTY=${tty} ; PWD=${quoteWord(cwd)} ; USER=${target.name} ; TIMESTAMP=${type} ; COMMAND=${command}`
20545
+ );
20546
+ }
20177
20547
  async always(self, target, cwd, argv, shown2, grant) {
20178
20548
  if (!shown2 || !recordable(cwd, argv)) return { ...grant(), notice: RAN_ONCE };
20179
20549
  if (!await this.remember(self, target, cwd, argv)) {
@@ -20181,7 +20551,7 @@ var Sudo = class {
20181
20551
  }
20182
20552
  return grant();
20183
20553
  }
20184
- listed(self, root, rules) {
20554
+ listed(self, root, rules, defaults2) {
20185
20555
  const host = this.o.hostname;
20186
20556
  if (root) {
20187
20557
  return {
@@ -20194,11 +20564,20 @@ var Sudo = class {
20194
20564
  return failed2("EPERM", `User ${self.name} is not allowed to run sudo on ${host}.`);
20195
20565
  }
20196
20566
  const lines2 = rules.map(described).join("\n");
20197
- return { list: `User ${self.name} may run the following commands on ${host}:
20567
+ const params = defaults2.flatMap((d) => d.params).join(", ");
20568
+ const matching = params === "" ? "" : `Matching Defaults entries for ${self.name} on ${host}:
20569
+ ${params}
20570
+
20571
+ `;
20572
+ return {
20573
+ list: `${matching}User ${self.name} may run the following commands on ${host}:
20198
20574
  ${lines2}
20199
- ` };
20575
+ `
20576
+ };
20200
20577
  }
20201
- async ask(caller, self, target, argv, path, cwd, requester) {
20578
+ async ask(s, asked) {
20579
+ const { caller, self, target, signal: requester } = s;
20580
+ const { argv, path, cwd } = asked;
20202
20581
  const { approve } = this.o;
20203
20582
  if (!approve || requester.aborted) return failed2("EACCES", DENIED2);
20204
20583
  const createdAt = Date.now();
@@ -20217,9 +20596,11 @@ ${lines2}
20217
20596
  ...caller.tty ? { tty: caller.tty } : {},
20218
20597
  createdAt,
20219
20598
  expiresAt: createdAt + timeout,
20220
- always: recordable(cwd, [path, ...argv]),
20599
+ always: asked.validate ? false : recordable(cwd, [path, ...argv]),
20221
20600
  stdin: caller.stdin,
20222
- rootOnly: await this.rootOnly(cwd, argv.slice(1))
20601
+ rootOnly: asked.validate ? {} : await this.rootOnly(cwd, argv.slice(1)),
20602
+ ...asked.validate ? { validate: true } : {},
20603
+ timestamp: true
20223
20604
  };
20224
20605
  const control = new AbortController();
20225
20606
  const timer = setTimeout(() => control.abort("expired"), timeout);
@@ -20229,9 +20610,8 @@ ${lines2}
20229
20610
  try {
20230
20611
  const answer2 = await answerOf(approve, req, control.signal);
20231
20612
  if (answer2 === ABORTED) return ended();
20232
- if (answer2 === "once" || answer2 === "always") {
20233
- return control.signal.aborted ? ended() : answer2;
20234
- }
20613
+ const approved = approvalOf(answer2);
20614
+ if (approved) return control.signal.aborted ? ended() : approved;
20235
20615
  return failed2("EACCES", denialText(answer2));
20236
20616
  } finally {
20237
20617
  clearTimeout(timer);
@@ -20241,7 +20621,7 @@ ${lines2}
20241
20621
  async remember(self, target, cwd, argv) {
20242
20622
  const { fs } = this.o;
20243
20623
  const path = `${SUDOERS_D}/${self.name}`;
20244
- if (!NAME3.test(self.name)) return false;
20624
+ if (!NAME4.test(self.name)) return false;
20245
20625
  try {
20246
20626
  await this.provision();
20247
20627
  if (!await this.trusted(SUDOERS_D)) return false;
@@ -21610,8 +21990,8 @@ function sameCert(a, b) {
21610
21990
  if (!a || !b) return a === b;
21611
21991
  return a.cert.length === b.cert.length && a.cert.every((byte, i) => byte === b.cert[i]);
21612
21992
  }
21613
- function winner(current, expected, record2) {
21614
- return sameCert(current, expected) ? record2 : current;
21993
+ function winner(current, expected, record3) {
21994
+ return sameCert(current, expected) ? record3 : current;
21615
21995
  }
21616
21996
  var DAY = 24 * 60 * 60 * 1e3;
21617
21997
  var CA_LIFETIME = 3650 * DAY;
@@ -21644,15 +22024,15 @@ function indexedDbCaStore(dbName = "slicc-realm-ca") {
21644
22024
  const store = (await open2()).transaction("ca").objectStore("ca");
21645
22025
  return await request2(store.get(owner));
21646
22026
  },
21647
- async swap(owner, expected, record2) {
22027
+ async swap(owner, expected, record3) {
21648
22028
  const tx = (await open2()).transaction("ca", "readwrite");
21649
22029
  const store = tx.objectStore("ca");
21650
- let stored2 = record2;
22030
+ let stored2 = record3;
21651
22031
  const read = store.get(owner);
21652
22032
  read.onsuccess = () => {
21653
22033
  const current = read.result;
21654
- const kept = winner(current, expected, record2);
21655
- if (kept === record2) store.put(record2, owner);
22034
+ const kept = winner(current, expected, record3);
22035
+ if (kept === record3) store.put(record3, owner);
21656
22036
  else stored2 = kept;
21657
22037
  };
21658
22038
  await new Promise((resolve2, reject) => {
@@ -21672,14 +22052,14 @@ function validLeafName(host) {
21672
22052
  }
21673
22053
  var RealmCa = class _RealmCa {
21674
22054
  record;
21675
- constructor(record2) {
21676
- this.record = record2;
22055
+ constructor(record3) {
22056
+ this.record = record3;
21677
22057
  }
21678
22058
  static async open(owner, store, now = Date.now()) {
21679
22059
  const stored2 = await store.get(owner).catch(() => void 0);
21680
22060
  if (stored2 && stored2.notAfter - now > CA_RENEW_BEFORE) return new _RealmCa(stored2);
21681
- const record2 = await _RealmCa.create(owner, now);
21682
- return new _RealmCa(await store.swap(owner, stored2, record2));
22061
+ const record3 = await _RealmCa.create(owner, now);
22062
+ return new _RealmCa(await store.swap(owner, stored2, record3));
21683
22063
  }
21684
22064
  static async create(owner, now) {
21685
22065
  const pair = await crypto.subtle.generateKey(ECDSA, false, ["sign", "verify"]);
@@ -21906,8 +22286,8 @@ function memoryCaStore() {
21906
22286
  const records = /* @__PURE__ */ new Map();
21907
22287
  return {
21908
22288
  get: async (owner) => records.get(owner),
21909
- swap: async (owner, expected, record2) => {
21910
- const kept = winner(records.get(owner), expected, record2);
22289
+ swap: async (owner, expected, record3) => {
22290
+ const kept = winner(records.get(owner), expected, record3);
21911
22291
  records.set(owner, kept);
21912
22292
  return kept;
21913
22293
  }
@@ -23948,7 +24328,7 @@ function replaced(text, values) {
23948
24328
  for (const [ph, value2] of Object.entries(values)) out = out.split(ph).join(binary(value2));
23949
24329
  return out;
23950
24330
  }
23951
- function usable(values) {
24331
+ function usable2(values) {
23952
24332
  const out = {};
23953
24333
  for (const [ph, value2] of Object.entries(values ?? {}))
23954
24334
  if (encoder8.encode(value2).length >= MIN_VALUE) out[ph] = value2;
@@ -23963,7 +24343,7 @@ async function substituteHead(head, ask) {
23963
24343
  if (!request3 || !found || found.length !== 1) return void 0;
23964
24344
  const at = found[0];
23965
24345
  if (at.placeholders.length > SESSION_PLACEHOLDERS) return void 0;
23966
- const values = usable(
24346
+ const values = usable2(
23967
24347
  await ask(`header:${at.name}`, at.placeholders, {
23968
24348
  method: request3[1],
23969
24349
  path: request3[2]
@@ -24273,7 +24653,7 @@ async function cleartext(message3, asks) {
24273
24653
  const placeholders = placeholdersIn(password);
24274
24654
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS)
24275
24655
  return { bytes: message3, done: true };
24276
- const values = usable(await asks.cleartext(placeholders));
24656
+ const values = usable2(await asks.cleartext(placeholders));
24277
24657
  if (Object.keys(values).length === 0) return { bytes: message3, done: true };
24278
24658
  return { bytes: frame3(112, `${replaced(password, values)}\0`), values, done: true };
24279
24659
  }
@@ -24338,7 +24718,7 @@ var PgClient = class {
24338
24718
  const x = got.exchange;
24339
24719
  const binding = this.header === END_POINT ? this.server.binding : void 0;
24340
24720
  const serverFinal = `c=${channelOf(this.header, binding)},r=${got.nonce}`;
24341
- const values = usable(
24721
+ const values = usable2(
24342
24722
  await this.asks.scram({
24343
24723
  clientFirstBare: x.clientFirstBare,
24344
24724
  serverFirst: x.serverFirst,
@@ -24387,7 +24767,7 @@ function decoded(b642) {
24387
24767
  async function swapped2(text, position, ask) {
24388
24768
  const placeholders = placeholdersIn(text);
24389
24769
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS) return void 0;
24390
- const values = usable(await ask(position, placeholders));
24770
+ const values = usable2(await ask(position, placeholders));
24391
24771
  if (Object.keys(values).length === 0) return void 0;
24392
24772
  return { text: replaced(text, values), values };
24393
24773
  }
@@ -30156,7 +30536,7 @@ function header2(id2, flags2, counts) {
30156
30536
  function opt() {
30157
30537
  return [0, 0, TYPE_OPT, UDP_EDNS >> 8, UDP_EDNS & 255, 0, 0, 0, 0, 0, 0];
30158
30538
  }
30159
- function record(type, rdata) {
30539
+ function record2(type, rdata) {
30160
30540
  const ttl = DNS_STUB_TTL;
30161
30541
  return [
30162
30542
  192,
@@ -30264,14 +30644,14 @@ var DnsStub = class {
30264
30644
  const address = reverseAddress(q.name);
30265
30645
  const name2 = address !== void 0 ? await this.reverse(address) : void 0;
30266
30646
  if (name2 !== void 0)
30267
- return answerReply(query, q, NOERROR, [record(TYPE_PTR, encodeName(name2))], true);
30647
+ return answerReply(query, q, NOERROR, [record2(TYPE_PTR, encodeName(name2))], true);
30268
30648
  return this.forward(query, q);
30269
30649
  }
30270
30650
  if (q.type === TYPE_A || q.type === TYPE_AAAA) {
30271
30651
  const addresses = q.name === "" ? [] : await this.lookup(q.name);
30272
30652
  const answers2 = addresses.flatMap((a) => {
30273
30653
  const data = addressData(a, q.type);
30274
- return data ? [record(q.type, data)] : [];
30654
+ return data ? [record2(q.type, data)] : [];
30275
30655
  });
30276
30656
  if (answers2.length > 0 || local(q.name) || await this.known(q.name)) {
30277
30657
  const rcode = addresses.length > 0 ? NOERROR : NXDOMAIN;
@@ -33034,6 +33414,7 @@ var SUDO_SOURCE = [
33034
33414
  " await say(1, answer.list);",
33035
33415
  " return 0;",
33036
33416
  " }",
33417
+ " if (answer.done === true) return 0;",
33037
33418
  " if (answer.run !== undefined) {",
33038
33419
  " try {",
33039
33420
  " pid = (await ctx.spawn({ argv: answer.run, pgid: 0 })).pid;",
@@ -33673,6 +34054,7 @@ var Launcher = class {
33673
34054
  );
33674
34055
  }
33675
34056
  this.users = new UserDb(this.fs, this.owners);
34057
+ const { sudoClock } = options;
33676
34058
  this.sudoer = new Sudo({
33677
34059
  fs: this.fs,
33678
34060
  users: this.users,
@@ -33682,7 +34064,8 @@ var Launcher = class {
33682
34064
  pnpmHome: this.pnpmHome,
33683
34065
  backing: (path, cwd) => this.backing(path, cwd),
33684
34066
  approve: options.approve,
33685
- timeoutMs: options.sudoTimeoutMs
34067
+ timeoutMs: options.sudoTimeoutMs,
34068
+ now: sudoClock ? () => sudoClock.now() : void 0
33686
34069
  });
33687
34070
  this.cron = this.cronService(options.cron);
33688
34071
  void this.unlinked.sweep("/");
@@ -34253,8 +34636,8 @@ var Launcher = class {
34253
34636
  const caller = this.jobs.shown(ppid);
34254
34637
  const named = typeof req.argv0 === "string" && !planned.script && !planned.target.fixed;
34255
34638
  const argv = named ? [req.argv0, ...req.argv.slice(1)] : req.argv;
34256
- const exact = booked?.pid === caller && req.file === booked.path && await physicalPathAsync(this.base, req.cwd) === booked.cwd && await argvDigest(argv) === booked.argv;
34257
- if (!exact) throw new SpawnError("EACCES");
34639
+ const exact2 = booked?.pid === caller && req.file === booked.path && await physicalPathAsync(this.base, req.cwd) === booked.cwd && await argvDigest(argv) === booked.argv;
34640
+ if (!exact2) throw new SpawnError("EACCES");
34258
34641
  const { requested: path, requestedCwd: cwd, path: resolved2 } = booked;
34259
34642
  const pid = this.nextPid++;
34260
34643
  const ids = { pid: req.exec ? caller : pid, ppid: caller };
@@ -35088,11 +35471,26 @@ var Launcher = class {
35088
35471
  pid: this.jobs.shown(pid),
35089
35472
  tags: this.tagsOf(pid),
35090
35473
  tty: this.described.get(pid)?.tty,
35474
+ ...this.timestampOwner(pid),
35091
35475
  stdin,
35092
35476
  report
35093
35477
  };
35094
35478
  return this.sudoer.request(caller, call, signal);
35095
35479
  }
35480
+ timestampOwner(pid) {
35481
+ const sid = this.jobs.sidOf(pid);
35482
+ const tty = this.jobs.controllingTerminal(pid)?.name;
35483
+ const leader = sid === void 0 ? void 0 : this.startedOf(sid);
35484
+ const host = this.jobs.shownParent(pid) ?? this.jobs.shown(pid);
35485
+ const started = this.startedOf(host);
35486
+ return {
35487
+ ...tty && sid !== void 0 && leader !== void 0 ? { terminal: { tty, sid, started: leader } } : {},
35488
+ ...started === void 0 ? {} : { parent: { pid: host, started } }
35489
+ };
35490
+ }
35491
+ startedOf(shown2) {
35492
+ return this.running(shown2) ? this.described.get(shown2)?.started : void 0;
35493
+ }
35096
35494
  visudo(pid, call) {
35097
35495
  return this.sudoer.check(this.credOf(pid), call);
35098
35496
  }
@@ -35602,9 +36000,9 @@ var Launcher = class {
35602
36000
  }
35603
36001
  pnpmReady() {
35604
36002
  if (this.pnpmMade !== void 0) return this.pnpmMade;
35605
- const epoch = this.pnpmEpoch;
36003
+ const epoch2 = this.pnpmEpoch;
35606
36004
  const made2 = this.base.mkdir(this.pnpmHome, { recursive: true }).then(() => this.owners ? this.stampLayout() : void 0).catch(() => {
35607
- if (this.pnpmEpoch === epoch) this.pnpmMade = void 0;
36005
+ if (this.pnpmEpoch === epoch2) this.pnpmMade = void 0;
35608
36006
  });
35609
36007
  this.pnpmMade = made2;
35610
36008
  return made2;