@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.
package/dist/node.js CHANGED
@@ -969,9 +969,9 @@ function changeCred(cur, change) {
969
969
  }
970
970
  const privileged = cur.euid === 0;
971
971
  const next3 = copyCred(cur);
972
- for (const keys of [UIDS, GIDS]) {
973
- const own = keys.map((key) => cur[key]);
974
- for (const key of keys) {
972
+ for (const keys2 of [UIDS, GIDS]) {
973
+ const own = keys2.map((key) => cur[key]);
974
+ for (const key of keys2) {
975
975
  const value2 = id(change[key]);
976
976
  if (value2 === void 0) continue;
977
977
  if (!privileged && !own.includes(value2)) throw fsError("EPERM", `${key} ${value2}`);
@@ -3617,12 +3617,12 @@ function next(p) {
3617
3617
  var ANY = [[">=", [0, 0, 0, []]]];
3618
3618
  var NOTHING = [["<", [0, 0, 0, ["0"]]]];
3619
3619
  function plain(op, p) {
3620
- const exact = !p.parts.includes(void 0);
3620
+ const exact2 = !p.parts.includes(void 0);
3621
3621
  const low = floor(p);
3622
3622
  const high = next(p);
3623
3623
  if (op === ">=") return [[">=", low]];
3624
- if (op === "<") return [["<", exact ? low : [low[0], low[1], low[2], ["0"]]]];
3625
- if (exact) return [[op === "" ? "=" : op, low]];
3624
+ if (op === "<") return [["<", exact2 ? low : [low[0], low[1], low[2], ["0"]]]];
3625
+ if (exact2) return [[op === "" ? "=" : op, low]];
3626
3626
  if (op === ">") return high ? [[">=", high]] : NOTHING;
3627
3627
  if (op === "<=") return high ? [["<", high]] : ANY;
3628
3628
  return high ? [
@@ -3709,7 +3709,7 @@ function satisfies(version, range2) {
3709
3709
  }
3710
3710
 
3711
3711
  // src/version.ts
3712
- var KERNEL_VERSION = true ? "1.176.2" : "0.0.0-dev";
3712
+ var KERNEL_VERSION = true ? "1.178.0" : "0.0.0-dev";
3713
3713
  var OS_RELEASE = "6.0.0-slicc";
3714
3714
  function procVersion() {
3715
3715
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -4299,7 +4299,7 @@ function intlZone(name2) {
4299
4299
  minute: "numeric",
4300
4300
  second: "numeric"
4301
4301
  });
4302
- const exact = (at) => {
4302
+ const exact2 = (at) => {
4303
4303
  const parts2 = {};
4304
4304
  for (const part of format.formatToParts(at)) parts2[part.type] = Number(part.value);
4305
4305
  const wall = Date.UTC(
@@ -4319,12 +4319,12 @@ function intlZone(name2) {
4319
4319
  const bucket = Math.floor(at / BUCKET_MS);
4320
4320
  let known = buckets.get(bucket);
4321
4321
  if (known === void 0) {
4322
- const start2 = exact(bucket * BUCKET_MS);
4323
- known = start2 === exact(bucket * BUCKET_MS + BUCKET_MS - MINUTE_MS) ? start2 : null;
4322
+ const start2 = exact2(bucket * BUCKET_MS);
4323
+ known = start2 === exact2(bucket * BUCKET_MS + BUCKET_MS - MINUTE_MS) ? start2 : null;
4324
4324
  if (buckets.size >= CACHE_MAX) buckets.clear();
4325
4325
  buckets.set(bucket, known);
4326
4326
  }
4327
- return known ?? exact(at);
4327
+ return known ?? exact2(at);
4328
4328
  }
4329
4329
  };
4330
4330
  }
@@ -5567,11 +5567,11 @@ var CronService = class {
5567
5567
  const now = this.clock.now();
5568
5568
  const system = this.host.timeZone;
5569
5569
  const due2 = [];
5570
- const keys = /* @__PURE__ */ new Set();
5570
+ const keys2 = /* @__PURE__ */ new Set();
5571
5571
  let next3 = now + WAKE_MS;
5572
5572
  for (const entry of loaded) {
5573
5573
  const key = keyOf2(entry);
5574
- keys.add(key);
5574
+ keys2.add(key);
5575
5575
  const { when } = entry.job;
5576
5576
  if (when.reboot) {
5577
5577
  if (this.rebooted || stamp.reboot[key] === this.host.bootId) continue;
@@ -5597,7 +5597,7 @@ var CronService = class {
5597
5597
  }
5598
5598
  this.rebooted = true;
5599
5599
  for (const map of [stamp.jobs, stamp.reboot, stamp.when])
5600
- for (const key of Object.keys(map)) if (!keys.has(key)) delete map[key];
5600
+ for (const key of Object.keys(map)) if (!keys2.has(key)) delete map[key];
5601
5601
  await this.writeStamp(stamp);
5602
5602
  for (const each of due2) void this.spawn(each);
5603
5603
  return next3;
@@ -6883,16 +6883,16 @@ function missing2(err) {
6883
6883
  return err?.code === "ENOENT";
6884
6884
  }
6885
6885
  function synthetic(directory, path) {
6886
- const epoch = /* @__PURE__ */ new Date(0);
6886
+ const epoch2 = /* @__PURE__ */ new Date(0);
6887
6887
  return {
6888
6888
  isFile: !directory,
6889
6889
  isDirectory: directory,
6890
6890
  isSymbolicLink: false,
6891
6891
  size: 0,
6892
6892
  mode: (directory ? S_IFDIR : S_IFREG) | 493,
6893
- mtime: epoch,
6894
- atime: epoch,
6895
- ctime: epoch,
6893
+ mtime: epoch2,
6894
+ atime: epoch2,
6895
+ ctime: epoch2,
6896
6896
  ino: inodeOf(path)
6897
6897
  };
6898
6898
  }
@@ -7111,16 +7111,16 @@ function missing3(err) {
7111
7111
  return err?.code === "ENOENT";
7112
7112
  }
7113
7113
  function statOf(path, bytes) {
7114
- const epoch = /* @__PURE__ */ new Date(0);
7114
+ const epoch2 = /* @__PURE__ */ new Date(0);
7115
7115
  return {
7116
7116
  isFile: true,
7117
7117
  isDirectory: false,
7118
7118
  isSymbolicLink: false,
7119
7119
  size: bytes.length,
7120
7120
  mode: S_IFREG2 | 420,
7121
- mtime: epoch,
7122
- atime: epoch,
7123
- ctime: epoch,
7121
+ mtime: epoch2,
7122
+ atime: epoch2,
7123
+ ctime: epoch2,
7124
7124
  ino: inodeOf(path),
7125
7125
  nlink: 1,
7126
7126
  uid: 0,
@@ -9476,10 +9476,10 @@ async function copyByBytes(fs, from, to, options) {
9476
9476
 
9477
9477
  // src/fs/view.ts
9478
9478
  function view(target, overrides) {
9479
- const keys = new Set(Object.keys(overrides));
9479
+ const keys2 = new Set(Object.keys(overrides));
9480
9480
  return new Proxy(target, {
9481
9481
  get(t, prop, receiver) {
9482
- if (keys.has(prop)) return overrides[prop];
9482
+ if (keys2.has(prop)) return overrides[prop];
9483
9483
  return Reflect.get(t, prop, receiver);
9484
9484
  },
9485
9485
  set(t, prop, value2) {
@@ -9735,11 +9735,11 @@ var HeldPaths = class extends Map {
9735
9735
  handlesAt(path) {
9736
9736
  return [...this.handles].filter(([, key]) => key === path).map(([id2]) => id2);
9737
9737
  }
9738
- doubt(over, exact = false) {
9738
+ doubt(over, exact2 = false) {
9739
9739
  if (this.bound.size === 0) return;
9740
9740
  for (const [handle, key] of this.handles) {
9741
9741
  if (!this.bound.has(handle) || this.keptHandles.has(handle)) continue;
9742
- if (exact ? key === over : within5(key, over)) this.doubted.set(handle, ++this.doubts);
9742
+ if (exact2 ? key === over : within5(key, over)) this.doubted.set(handle, ++this.doubts);
9743
9743
  }
9744
9744
  }
9745
9745
  boundIno(handle) {
@@ -13594,8 +13594,8 @@ function refused2(engine, session, err, opening2) {
13594
13594
  const code = verifyCode(flags2);
13595
13595
  if (code && flags2 !== -1)
13596
13596
  return new KernelError("EPROTO", `${code}: ${engine.verifyInfo(flags2).trim()}`);
13597
- const record2 = recordRefusal(opening2);
13598
- if (record2) return record2;
13597
+ const record3 = recordRefusal(opening2);
13598
+ if (record3) return record3;
13599
13599
  const text = err instanceof TlsError ? err.message : String(err);
13600
13600
  return new KernelError("EPROTO", `ERR_SSL_HANDSHAKE_FAILURE: ${text}${alertOf(session)}`);
13601
13601
  }
@@ -14488,8 +14488,8 @@ var SCRUB_IDLE_MS = 50;
14488
14488
  var SCRUB_NONBLOCK_MAX = 64 * 1024;
14489
14489
  var encoder5 = new TextEncoder();
14490
14490
  function scrubberOf(secrets, sameLength = false) {
14491
- const usable2 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
14492
- return usable2.length > 0 ? compile(usable2, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
14491
+ const usable3 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
14492
+ return usable3.length > 0 ? compile(usable3, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
14493
14493
  }
14494
14494
  async function writeAll(file, bytes) {
14495
14495
  const write = file.write;
@@ -14710,6 +14710,120 @@ var SessionScrubs = class {
14710
14710
  }
14711
14711
  };
14712
14712
 
14713
+ // src/kernel/sealed-oauth.ts
14714
+ var SEALED_OAUTH_TIMEOUT_MS = 45e3;
14715
+ var SEALED_OAUTH_OPS = /* @__PURE__ */ new Set([
14716
+ "oauthGrant",
14717
+ "oauthStatus",
14718
+ "oauthRefresh",
14719
+ "oauthRemove"
14720
+ ]);
14721
+ var MESSAGES = {
14722
+ EINVAL: "Invalid OAuth request",
14723
+ ENOENT: "OAuth credential not found",
14724
+ EEXIST: "OAuth credential identity conflicts",
14725
+ EACCES: "OAuth operation refused",
14726
+ EAGAIN: "OAuth credential changed; try again",
14727
+ ENOTSUP: "OAuth operation is not supported",
14728
+ ENOTCONN: "Vault is unavailable",
14729
+ ETIMEDOUT: "Vault request timed out",
14730
+ EIO: "Invalid vault reply"
14731
+ };
14732
+ var CAUSES = {
14733
+ "vault-locked": "Vault is locked",
14734
+ "oauth-reauth-required": "OAuth sign-in is required",
14735
+ "oauth-refresh-failed": "OAuth refresh failed"
14736
+ };
14737
+ var COMMON = ["v", "op", "caller"];
14738
+ var IDENTITY = [...COMMON, "name", "provider", "account"];
14739
+ var GRANT = [...IDENTITY, "accessToken", "refreshToken", "tokenType", "expiresAt", "oauth"];
14740
+ var CONFIG = ["tokenUrl", "clientId", "authMethod", "scope", "resource"];
14741
+ var UNSUPPORTED = ["auth", "clientSecret", "fields"];
14742
+ var STATES2 = /* @__PURE__ */ new Set([
14743
+ "ready",
14744
+ "refreshing",
14745
+ "expired",
14746
+ "reauth-required",
14747
+ "removed",
14748
+ "locked"
14749
+ ]);
14750
+ var NAME2 = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
14751
+ var PROVIDER = /^[a-z0-9][a-z0-9-]{0,127}$/;
14752
+ var PLACEHOLDER2 = /^fnox_proxy_[0-9a-f]{24}$/;
14753
+ var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
14754
+ function record(value2) {
14755
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
14756
+ }
14757
+ function keys(value2, allowed) {
14758
+ return Object.keys(value2).every((key) => allowed.includes(key));
14759
+ }
14760
+ function exact(value2, fields) {
14761
+ return Object.keys(value2).length === fields.length && keys(value2, fields);
14762
+ }
14763
+ function nonempty(value2) {
14764
+ return typeof value2 === "string" && value2.length > 0;
14765
+ }
14766
+ function epoch(value2) {
14767
+ return typeof value2 === "number" && Number.isSafeInteger(value2) && value2 >= 0;
14768
+ }
14769
+ function sealedOAuthOperation(op) {
14770
+ return op === "capabilities" || SEALED_OAUTH_OPS.has(op);
14771
+ }
14772
+ function validateSealedOAuth(req) {
14773
+ const config = req.oauth;
14774
+ if (UNSUPPORTED.some((key) => Object.hasOwn(req, key)) || record(config) && UNSUPPORTED.some((key) => Object.hasOwn(config, key)))
14775
+ throw new KernelError("ENOTSUP");
14776
+ const allowed = req.op === "capabilities" ? COMMON : req.op === "oauthGrant" ? GRANT : IDENTITY;
14777
+ if (!keys(req, allowed)) throw new KernelError("EINVAL");
14778
+ if (req.op === "capabilities") return;
14779
+ if (typeof req.name !== "string" || !NAME2.test(req.name) || typeof req.provider !== "string" || !PROVIDER.test(req.provider) || !nonempty(req.account) || req.account.length > 256 || CONTROL.test(req.account))
14780
+ throw new KernelError("EINVAL");
14781
+ if (req.op !== "oauthGrant") return;
14782
+ 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))
14783
+ throw new KernelError("EINVAL");
14784
+ if (config.authMethod !== "none" || typeof req.tokenType === "string" && req.tokenType.toLowerCase() !== "bearer")
14785
+ throw new KernelError("ENOTSUP");
14786
+ }
14787
+ function sealedOAuthError(code = "EIO", cause) {
14788
+ return {
14789
+ v: 1,
14790
+ error: { code, ...cause ? { cause } : {}, message: cause ? CAUSES[cause] : MESSAGES[code] }
14791
+ };
14792
+ }
14793
+ function sealedOAuthReply(op, value2) {
14794
+ try {
14795
+ return checkedReply(op, value2) ?? sealedOAuthError();
14796
+ } catch {
14797
+ return sealedOAuthError();
14798
+ }
14799
+ }
14800
+ function checkedError(value2) {
14801
+ const error = value2.error;
14802
+ if (!exact(value2, ["v", "error"]) || !record(error) || !Object.hasOwn(error, "code") || !Object.hasOwn(error, "message"))
14803
+ return void 0;
14804
+ const { code, cause, message: message3 } = error;
14805
+ 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)))
14806
+ return void 0;
14807
+ return sealedOAuthError(code, cause);
14808
+ }
14809
+ function checkedReply(op, value2) {
14810
+ if (!record(value2) || !Object.hasOwn(value2, "v") || value2.v !== 1) return void 0;
14811
+ if (Object.hasOwn(value2, "error")) return checkedError(value2);
14812
+ if (op === "capabilities") {
14813
+ const caps = value2.capabilities;
14814
+ if (!exact(value2, ["v", "capabilities"]) || !record(caps) || !keys(caps, ["kernelOAuth"]))
14815
+ return void 0;
14816
+ if (Object.hasOwn(caps, "kernelOAuth") && caps.kernelOAuth !== 1) return void 0;
14817
+ return { v: 1, capabilities: Object.hasOwn(caps, "kernelOAuth") ? { kernelOAuth: 1 } : {} };
14818
+ }
14819
+ if (op === "oauthRemove")
14820
+ return exact(value2, ["v", "removed"]) && value2.removed === true ? { v: 1, removed: true } : void 0;
14821
+ const { placeholder, state, expiresAt, refreshable } = value2;
14822
+ 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")
14823
+ return void 0;
14824
+ return { v: 1, placeholder, state, expiresAt, refreshable };
14825
+ }
14826
+
14713
14827
  // src/kernel/sealed-session.ts
14714
14828
  var SESSION_WINDOW_MS = 6e4;
14715
14829
  var SESSION_PLACEHOLDERS = 16;
@@ -14820,8 +14934,8 @@ function sessionValues(reply, asked) {
14820
14934
  const { error, values } = reply;
14821
14935
  if (error !== void 0 || typeof values !== "object" || values === null) return void 0;
14822
14936
  const out = {};
14823
- const keys = Object.keys(values);
14824
- const wanted = asked ?? (keys.length === 1 && SESSION_PLACEHOLDER.test(keys[0]) ? keys : []);
14937
+ const keys2 = Object.keys(values);
14938
+ const wanted = asked ?? (keys2.length === 1 && SESSION_PLACEHOLDER.test(keys2[0]) ? keys2 : []);
14825
14939
  for (const ph of wanted) {
14826
14940
  const value2 = values[ph];
14827
14941
  if (typeof value2 === "string" && value2.length > 0 && !UNSAFE.test(value2)) out[ph] = value2;
@@ -14845,7 +14959,9 @@ var SEALED_OPS = /* @__PURE__ */ new Set([
14845
14959
  "request",
14846
14960
  "status",
14847
14961
  "cancel",
14848
- "materialize"
14962
+ "materialize",
14963
+ "capabilities",
14964
+ ...SEALED_OAUTH_OPS
14849
14965
  ]);
14850
14966
  function tagFields(tags) {
14851
14967
  const [tag] = tags ?? [];
@@ -14880,6 +14996,7 @@ function parseSealed(bytes) {
14880
14996
  throw new KernelError("EINVAL");
14881
14997
  if (req.op === "materialize" && !materializable(req)) throw new KernelError("EINVAL");
14882
14998
  if (req.op === "ask" && !askable(req)) throw new KernelError("EINVAL");
14999
+ if (sealedOAuthOperation(req.op)) validateSealedOAuth(req);
14883
15000
  return req;
14884
15001
  } finally {
14885
15002
  bytes.fill(0);
@@ -14958,7 +15075,7 @@ var SealedChannel = class {
14958
15075
  const ask = fields.op === "ask";
14959
15076
  const timer = setTimeout(
14960
15077
  () => control.abort(ask ? "expired" : "timeout"),
14961
- ask ? this.broker.askTimeoutMs : this.broker.timeoutMs
15078
+ ask ? this.broker.askTimeoutMs : SEALED_OAUTH_OPS.has(fields.op) ? this.broker.oauthTimeoutMs : this.broker.timeoutMs
14962
15079
  );
14963
15080
  const req = { ...fields, v: 1, op: fields.op, caller };
14964
15081
  const asked = ask ? this.asking(fields, caller, requester, control.signal) : void 0;
@@ -14981,24 +15098,26 @@ var SealedChannel = class {
14981
15098
  if (!facts) return errorReply("EIO", "the kernel has no record of this process");
14982
15099
  return this.answer(askRequest(fields, caller, facts), signal);
14983
15100
  }
14984
- stopped(signal) {
15101
+ stopped(signal, oauth = false) {
14985
15102
  if (signal.reason === "timeout")
14986
- return errorReply("ETIMEDOUT", "the vault did not answer within 10 s");
15103
+ return oauth ? sealedOAuthError("ETIMEDOUT") : errorReply("ETIMEDOUT", "the vault did not answer within 10 s");
14987
15104
  return signal.reason === "expired" ? { v: 1, state: "expired" } : void 0;
14988
15105
  }
14989
15106
  async answer(req, signal) {
14990
15107
  const hook = this.broker.hook;
14991
- if (!hook) return errorReply("ENOTCONN", "no vault is connected");
15108
+ const oauth = sealedOAuthOperation(req.op);
15109
+ if (!hook)
15110
+ return oauth ? sealedOAuthError("ENOTCONN") : errorReply("ENOTCONN", "no vault is connected");
14992
15111
  const stopped = new Promise((resolve2) => {
14993
15112
  signal.addEventListener("abort", () => resolve2(void 0), { once: true });
14994
15113
  });
14995
15114
  if (signal.aborted) return void 0;
14996
15115
  const asked = Promise.resolve().then(() => hook(req, { signal })).then(
14997
- (reply2) => isRecord(reply2) ? reply2 : errorReply("EIO", "the vault gave no reply"),
14998
- (err) => errorReply("EIO", lineOf3(err))
15116
+ (reply2) => oauth ? sealedOAuthReply(req.op, reply2) : isRecord(reply2) ? reply2 : errorReply("EIO", "the vault gave no reply"),
15117
+ (err) => oauth ? sealedOAuthError() : errorReply("EIO", lineOf3(err))
14999
15118
  );
15000
15119
  const reply = await Promise.race([asked, stopped]);
15001
- return signal.aborted ? this.stopped(signal) : reply;
15120
+ return signal.aborted ? this.stopped(signal, oauth) : reply;
15002
15121
  }
15003
15122
  drop() {
15004
15123
  this.reply?.fill(0);
@@ -15061,6 +15180,7 @@ var SealedBroker = class {
15061
15180
  tickets = new TicketBook();
15062
15181
  sessions = new SessionScrubs();
15063
15182
  askTimeoutMs = SEALED_ASK_TIMEOUT_MS;
15183
+ oauthTimeoutMs = SEALED_OAUTH_TIMEOUT_MS;
15064
15184
  userOf = async () => void 0;
15065
15185
  factsOf = async () => void 0;
15066
15186
  constructor(hook, users, timeoutMs = SEALED_TIMEOUT_MS, tagOf = () => void 0) {
@@ -15128,10 +15248,60 @@ var SealedBroker = class {
15128
15248
  }
15129
15249
  };
15130
15250
 
15251
+ // src/sudo-timestamp.ts
15252
+ var TIMESTAMPS_MAX = 64;
15253
+ var MINUTE_MS2 = 6e4;
15254
+ function timestampKey(owner, type) {
15255
+ if (type === "global") return "global";
15256
+ const { terminal, parent } = owner;
15257
+ if (type === "tty" && terminal) {
15258
+ return JSON.stringify(["tty", terminal.tty, terminal.sid, terminal.started]);
15259
+ }
15260
+ return parent ? JSON.stringify(["ppid", parent.pid, parent.started]) : void 0;
15261
+ }
15262
+ var Timestamps = class {
15263
+ users = /* @__PURE__ */ new Map();
15264
+ now;
15265
+ constructor(now) {
15266
+ this.now = now;
15267
+ }
15268
+ current(owner, policy) {
15269
+ if (policy.minutes === 0) return false;
15270
+ const key = timestampKey(owner, policy.type);
15271
+ const at = key === void 0 ? void 0 : this.users.get(owner.uid)?.get(key);
15272
+ if (at === void 0) return false;
15273
+ const now = this.now();
15274
+ return at <= now && (policy.minutes < 0 || now - at < policy.minutes * MINUTE_MS2);
15275
+ }
15276
+ stamp(owner, policy) {
15277
+ const key = policy.minutes === 0 ? void 0 : timestampKey(owner, policy.type);
15278
+ if (key === void 0) return false;
15279
+ const now = this.now();
15280
+ const records = this.users.get(owner.uid) ?? /* @__PURE__ */ new Map();
15281
+ this.users.set(owner.uid, records);
15282
+ records.delete(key);
15283
+ records.set(key, now);
15284
+ for (const [old, at] of records) {
15285
+ const stale = policy.minutes > 0 && (at > now || now - at >= policy.minutes * MINUTE_MS2);
15286
+ if (stale || records.size > TIMESTAMPS_MAX) records.delete(old);
15287
+ }
15288
+ return true;
15289
+ }
15290
+ invalidate(owner, type) {
15291
+ const key = timestampKey(owner, type);
15292
+ const records = this.users.get(owner.uid);
15293
+ if (key !== void 0) records?.delete(key);
15294
+ if (records?.size === 0) this.users.delete(owner.uid);
15295
+ }
15296
+ remove(uid) {
15297
+ this.users.delete(uid);
15298
+ }
15299
+ };
15300
+
15131
15301
  // src/sudoers.ts
15132
15302
  var SUDOERS = "/etc/sudoers";
15133
15303
  var SUDOERS_D = "/etc/sudoers.d";
15134
- var NAME2 = /^[a-z_][a-z0-9_.-]*$/;
15304
+ var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
15135
15305
  var WORD = String.raw`(?:'[^']*'|"(?:[^"\\]|\\[\s\S])*"|\\[\s\S]|[^\s'"\\])+`;
15136
15306
  var KERNEL_FORM = new RegExp(
15137
15307
  String.raw`^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()]*)\)\s+)?(?:Cwd\s+(${WORD})\s+)?(?:Stdin\s+(\S+)\s+)?Cmnd(?:\s+(.*))?$`
@@ -15141,14 +15311,16 @@ var RUNAS = /^\(([^()]*)\)\s*/;
15141
15311
  var TAG = /^([A-Z_]+)\s*:\s*/;
15142
15312
  var OPTION = /^([A-Z_]+)=/;
15143
15313
  var PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;
15144
- var CONTROL = /[\p{Cc}\u2028\u2029]/u;
15314
+ var CONTROL2 = /[\p{Cc}\u2028\u2029]/u;
15145
15315
  var CONTROLS = /[\p{Cc}\u2028\u2029]/gu;
15146
15316
  var SHOWN_MAX = 60;
15147
15317
  var KERNEL_SYNTAX = "[NOPASSWD] <user|%group|ALL> [(<target>)] [Cwd <dir>] [Stdin none] Cmnd <command\u2026|*>";
15148
15318
  var SUDOERS_SYNTAX = "<user|%group|ALL> ALL=[(<target>[,<target>\u2026])] [NOPASSWD:] <command\u2026|ALL>";
15149
15319
  var EXPECTED = `expected '${KERNEL_SYNTAX}' or '${SUDOERS_SYNTAX}'`;
15320
+ var TIMESTAMP_TIMEOUT = 5;
15321
+ var TIMESTAMP_TYPE = "tty";
15150
15322
  function hasControl(text) {
15151
- return CONTROL.test(text);
15323
+ return CONTROL2.test(text);
15152
15324
  }
15153
15325
  function withoutControl(text) {
15154
15326
  return text.replace(CONTROLS, (ch) => {
@@ -15286,7 +15458,7 @@ function commandText(cmnd) {
15286
15458
  }
15287
15459
  function validWho(who) {
15288
15460
  if (who === "ALL") return true;
15289
- return NAME2.test(who.startsWith("%") ? who.slice(1) : who);
15461
+ return NAME3.test(who.startsWith("%") ? who.slice(1) : who);
15290
15462
  }
15291
15463
  function whoError(who) {
15292
15464
  if (validWho(who)) return void 0;
@@ -15319,7 +15491,7 @@ function kernelRules(m) {
15319
15491
  const [, nopasswd, who = "", target = "root", dir, stdin, rest2 = ""] = m;
15320
15492
  const bad = whoError(who);
15321
15493
  if (bad !== void 0) return bad;
15322
- if (target !== "ALL" && !NAME2.test(target)) return targetError(target);
15494
+ if (target !== "ALL" && !NAME3.test(target)) return targetError(target);
15323
15495
  const cwd = dir === void 0 ? void 0 : cwdOf(dir);
15324
15496
  if (cwd === "") return `Cwd expects one absolute directory, not ${shown(dir)}`;
15325
15497
  if (stdin !== void 0 && stdin !== "none") return `Stdin takes none, not ${shown(stdin)}`;
@@ -15343,11 +15515,11 @@ function runasOf(list) {
15343
15515
  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`;
15344
15516
  }
15345
15517
  for (const name2 of names2) {
15346
- if (name2 === "ALL" || NAME2.test(name2)) continue;
15518
+ if (name2 === "ALL" || NAME3.test(name2)) continue;
15347
15519
  return `expected user names or ALL to run as, not ${shown(name2)} (no %group, #uid or !user)`;
15348
15520
  }
15349
15521
  for (const group of groups) {
15350
- if (group === "" || group === "ALL" || NAME2.test(group)) continue;
15522
+ if (group === "" || group === "ALL" || NAME3.test(group)) continue;
15351
15523
  return `expected group names or ALL after ':' in (\u2026), not ${shown(group)}`;
15352
15524
  }
15353
15525
  return names2.includes("ALL") ? ["ALL"] : [...new Set(names2)];
@@ -15429,9 +15601,6 @@ function directiveError(text) {
15429
15601
  return /^#-?\d+\s+\S+\s*=/.test(text) ? "a #uid isn't supported: name the user, %group or ALL" : null;
15430
15602
  }
15431
15603
  const first = /^\S+/.exec(text)?.[0] ?? "";
15432
- if (/^Defaults([:@>!]|$)/.test(first)) {
15433
- return "Defaults aren't supported: the kernel's sudo has no settings, so this changes nothing";
15434
- }
15435
15604
  if (/^[A-Z][A-Za-z]*_Alias$/.test(first)) {
15436
15605
  return "aliases aren't supported: name the users, targets and commands in each line";
15437
15606
  }
@@ -15439,7 +15608,7 @@ function directiveError(text) {
15439
15608
  }
15440
15609
  function recordedBare(line, file, rule) {
15441
15610
  if (rule.cmnd === "*" || rule.cmnd.args !== void 0 || !rule.nopasswd) return false;
15442
- if (file !== `${SUDOERS_D}/${rule.who}` || !NAME2.test(rule.who) || !NAME2.test(rule.target)) {
15611
+ if (file !== `${SUDOERS_D}/${rule.who}` || !NAME3.test(rule.who) || !NAME3.test(rule.target)) {
15443
15612
  return false;
15444
15613
  }
15445
15614
  const head = `NOPASSWD ${rule.who} (${rule.target}) `;
@@ -15457,9 +15626,90 @@ function kernelLine(m, line, file) {
15457
15626
  )
15458
15627
  };
15459
15628
  }
15629
+ var DEFAULTS = /^Defaults(?=[:@>!\s]|$)/;
15630
+ var DEFAULTS_ONLY = `Defaults aren't supported, except timestamp_timeout and timestamp_type for everyone, a user or a %group: this line changes nothing`;
15631
+ var PARAM = /^(!*)([A-Za-z_]+)(?:\s*([-+]?=)\s*(.*))?$/s;
15632
+ var MINUTES = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/;
15633
+ var TYPES = ["tty", "ppid", "global"];
15634
+ function defaultsValue(raw2) {
15635
+ const quoted = /^"(.*)"$/s.exec(raw2);
15636
+ return quoted ? quoted[1] : raw2;
15637
+ }
15638
+ function timeoutParam({ text, negated, op, value: value2 }) {
15639
+ if (negated && op === void 0) return { timestampTimeout: 0 };
15640
+ if (negated || op !== "=" || !MINUTES.test(value2) || !Number.isFinite(Number(value2))) {
15641
+ return `timestamp_timeout takes minutes, such as =5 or =2.5 (0 always asks, below 0 never expires), not ${shown(text)}`;
15642
+ }
15643
+ return { timestampTimeout: Number(value2) };
15644
+ }
15645
+ function typeParam({ text, negated, op, value: value2 }) {
15646
+ if (!negated && op === "=" && TYPES.includes(value2)) {
15647
+ return { timestampType: value2 };
15648
+ }
15649
+ if (value2 === "kernel") return "timestamp_type=kernel isn't supported: use tty, ppid or global";
15650
+ return `timestamp_type takes =tty, =ppid or =global, not ${shown(text)}`;
15651
+ }
15652
+ function defaultsParam(param) {
15653
+ const m = PARAM.exec(param);
15654
+ if (!m) return DEFAULTS_ONLY;
15655
+ const [, bangs = "", name2, op, raw2 = ""] = m;
15656
+ const read = { text: param, negated: bangs !== "", op, value: defaultsValue(raw2.trim()) };
15657
+ const settings = name2 === "timestamp_timeout" ? timeoutParam(read) : name2 === "timestamp_type" ? typeParam(read) : DEFAULTS_ONLY;
15658
+ if (typeof settings === "string") return settings;
15659
+ const value2 = settings.timestampTimeout ?? settings.timestampType;
15660
+ return { settings, text: `${name2}=${value2}` };
15661
+ }
15662
+ function defaultsWho(binding) {
15663
+ if (binding === "") return null;
15664
+ if (!binding.startsWith(":")) return DEFAULTS_ONLY;
15665
+ const who = binding.slice(1).split(",");
15666
+ for (const name2 of who) {
15667
+ if (!validWho(name2))
15668
+ return `expected users, %groups or ALL after Defaults:, not ${shown(name2)}`;
15669
+ }
15670
+ return who;
15671
+ }
15672
+ function defaultsLine(text) {
15673
+ const m = /^Defaults(\S*)(?:\s+(.*))?$/s.exec(text);
15674
+ const who = defaultsWho(m?.[1] ?? "");
15675
+ if (typeof who === "string") return { error: who };
15676
+ const list = (m?.[2] ?? "").trim();
15677
+ if (list === "")
15678
+ return {
15679
+ error: "expected timestamp_timeout=<minutes> or timestamp_type=<type> after Defaults"
15680
+ };
15681
+ const settings = {};
15682
+ const params = [];
15683
+ for (const param of list.split(",")) {
15684
+ const read = defaultsParam(param.trim());
15685
+ if (typeof read === "string") return { error: read };
15686
+ Object.assign(settings, read.settings);
15687
+ params.push(read.text);
15688
+ }
15689
+ return { defaults: { who, settings, params } };
15690
+ }
15691
+ function resolveDefaults(entries, applies) {
15692
+ const everyone = entries.filter((e) => e.who === null);
15693
+ const mine = entries.filter((e) => e.who?.some(applies));
15694
+ const matching = [...everyone, ...mine];
15695
+ const settings = Object.assign({}, ...matching.map((e) => e.settings));
15696
+ return {
15697
+ timestampTimeout: settings.timestampTimeout ?? TIMESTAMP_TIMEOUT,
15698
+ timestampType: settings.timestampType ?? TIMESTAMP_TYPE,
15699
+ matching
15700
+ };
15701
+ }
15702
+ function defaultsText(entries) {
15703
+ const all = resolveDefaults(entries, () => false);
15704
+ return [
15705
+ `Defaults timestamp_timeout=${all.timestampTimeout}, timestamp_type=${all.timestampType}`,
15706
+ ...entries.filter((e) => e.who !== null).map((e) => `Defaults:${e.who.join(",")} ${e.params.join(", ")}`)
15707
+ ];
15708
+ }
15460
15709
  function parseLine(line, file) {
15461
15710
  const text = line.trim();
15462
15711
  if (text === "") return null;
15712
+ if (DEFAULTS.test(text)) return defaultsLine(text);
15463
15713
  const directive = directiveError(text);
15464
15714
  if (directive !== void 0) return directive === null ? null : { error: directive };
15465
15715
  const kernel = KERNEL_FORM.exec(text);
@@ -15471,14 +15721,16 @@ function parseLine(line, file) {
15471
15721
  }
15472
15722
  function parsePolicy(text, file) {
15473
15723
  const rules = [];
15724
+ const defaults2 = [];
15474
15725
  const errors = [];
15475
15726
  for (const [n, line] of text.split("\n").entries()) {
15476
15727
  const parsed2 = parseLine(line, file);
15477
15728
  if (parsed2 === null) continue;
15478
15729
  if ("error" in parsed2) errors.push({ line: n + 1, message: parsed2.error });
15730
+ else if ("defaults" in parsed2) defaults2.push(parsed2.defaults);
15479
15731
  else rules.push(...parsed2.rules);
15480
15732
  }
15481
- return { rules, errors };
15733
+ return { rules, defaults: defaults2, errors };
15482
15734
  }
15483
15735
 
15484
15736
  // src/users.ts
@@ -15486,7 +15738,7 @@ var PASSWD = "/etc/passwd";
15486
15738
  var GROUP = "/etc/group";
15487
15739
  var DEFAULT_PASSWD = "root:x:0:0:root:/root:/bin/bash\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n";
15488
15740
  var DEFAULT_GROUP = "root:x:0:\ndialout:x:20:\nsudo:x:27:\nusers:x:100:\nnogroup:x:65534:\n";
15489
- var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
15741
+ var NAME4 = /^[a-z_][a-z0-9_.-]*$/;
15490
15742
  var MAX_NAME = 255;
15491
15743
  var FIRST_UID = 1e3;
15492
15744
  var LAST_UID = 6e4;
@@ -15734,7 +15986,7 @@ var UserDb = class {
15734
15986
  add(options) {
15735
15987
  return this.serial(async () => {
15736
15988
  const { name: name2 } = options;
15737
- if (typeof name2 !== "string" || name2.length > MAX_NAME || !NAME3.test(name2)) {
15989
+ if (typeof name2 !== "string" || name2.length > MAX_NAME || !NAME4.test(name2)) {
15738
15990
  throw fsError("EINVAL", `not a user name: ${String(name2)}`);
15739
15991
  }
15740
15992
  const remember = () => {
@@ -15788,7 +16040,7 @@ var UserDb = class {
15788
16040
  }
15789
16041
  addGroup(name2, gid) {
15790
16042
  return this.serial(async () => {
15791
- if (typeof name2 !== "string" || name2.length > MAX_NAME || !NAME3.test(name2)) {
16043
+ if (typeof name2 !== "string" || name2.length > MAX_NAME || !NAME4.test(name2)) {
15792
16044
  throw fsError("EINVAL", `not a group name: ${String(name2)}`);
15793
16045
  }
15794
16046
  if (typeof gid !== "number" || !Number.isInteger(gid) || gid < 0 || gid > MAX_GID) {
@@ -15863,7 +16115,14 @@ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
15863
16115
  var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
15864
16116
  var SUDO_TIMEOUT_MS = 3e5;
15865
16117
  var TERM = /^[A-Za-z0-9._+-]+$/;
15866
- var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
16118
+ var USAGE = [
16119
+ "usage: sudo -K | -k",
16120
+ " sudo -v [-kn] [-u user]",
16121
+ " sudo -l [-kn]",
16122
+ " sudo [-kn] [-u user] [--] command [args...]"
16123
+ ].join("\n");
16124
+ var DONE = { done: true };
16125
+ var NOT_STAMPED = "sudo: approved, but the approver didn't say a person decided, so no timestamp was recorded";
15867
16126
  var DENIED2 = "sudo: approval denied";
15868
16127
  var DENIAL_MAX = 500;
15869
16128
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
@@ -15937,6 +16196,17 @@ async function answerOf(approve, req, signal) {
15937
16196
  return cut === void 0 ? "deny" : await cut;
15938
16197
  }
15939
16198
  }
16199
+ function approvalOf(answer2) {
16200
+ if (answer2 === "once" || answer2 === "always") return { answer: answer2, person: false };
16201
+ if (typeof answer2 !== "object" || answer2 === null) return void 0;
16202
+ try {
16203
+ const { answer: kind, timestamp } = answer2;
16204
+ if (kind !== "once" && kind !== "always" || timestamp !== true) return void 0;
16205
+ return { answer: kind, person: true };
16206
+ } catch {
16207
+ return void 0;
16208
+ }
16209
+ }
15940
16210
  function recordedCommand(argv) {
15941
16211
  const [path = "", ...args] = argv;
15942
16212
  return commandText({ path, args: args.map((a) => [a]) });
@@ -15946,6 +16216,9 @@ function flags(parsed2, arg, next3) {
15946
16216
  const flag = arg[j];
15947
16217
  if (flag === "n") parsed2.nonInteractive = true;
15948
16218
  else if (flag === "l") parsed2.list = true;
16219
+ else if (flag === "v") parsed2.validate = true;
16220
+ else if (flag === "k") parsed2.reset = true;
16221
+ else if (flag === "K") parsed2.removeAll = true;
15949
16222
  else if (flag !== "u") return `sudo: invalid option -- '${flag}'
15950
16223
  ${USAGE}`;
15951
16224
  else {
@@ -15959,7 +16232,15 @@ ${USAGE}`;
15959
16232
  return 0;
15960
16233
  }
15961
16234
  function parseArgs(args) {
15962
- const parsed2 = { list: false, nonInteractive: false, user: "root", command: [] };
16235
+ const parsed2 = {
16236
+ list: false,
16237
+ nonInteractive: false,
16238
+ validate: false,
16239
+ reset: false,
16240
+ removeAll: false,
16241
+ user: "root",
16242
+ command: []
16243
+ };
15963
16244
  let i = 0;
15964
16245
  for (; i < args.length; i++) {
15965
16246
  const arg = args[i];
@@ -15973,8 +16254,17 @@ function parseArgs(args) {
15973
16254
  i += used;
15974
16255
  }
15975
16256
  parsed2.command = args.slice(i);
15976
- if (parsed2.list ? parsed2.command.length > 0 : parsed2.command.length === 0) return USAGE;
15977
- return parsed2;
16257
+ return usable(parsed2) ? parsed2 : USAGE;
16258
+ }
16259
+ function usable(parsed2) {
16260
+ const command = parsed2.command.length > 0;
16261
+ const modes = [parsed2.list, parsed2.validate, parsed2.removeAll].filter(Boolean).length;
16262
+ if (modes > 1 || parsed2.removeAll && parsed2.reset) return false;
16263
+ if (modes === 1) return !command;
16264
+ return command || parsed2.reset;
16265
+ }
16266
+ function policyOf(settings) {
16267
+ return { type: settings.timestampType, minutes: settings.timestampTimeout };
15978
16268
  }
15979
16269
  function described(rule) {
15980
16270
  const cmnd = rule.cmnd === "*" ? "ALL" : commandText(rule.cmnd);
@@ -15986,9 +16276,11 @@ var Sudo = class {
15986
16276
  o;
15987
16277
  warned = /* @__PURE__ */ new Set();
15988
16278
  pending = /* @__PURE__ */ new Set();
16279
+ timestamps;
15989
16280
  next = 0;
15990
16281
  constructor(options) {
15991
16282
  this.o = options;
16283
+ this.timestamps = new Timestamps(options.now ?? (() => performance.now()));
15992
16284
  }
15993
16285
  warn(message3) {
15994
16286
  if (this.warned.has(message3)) return;
@@ -16075,8 +16367,8 @@ var Sudo = class {
16075
16367
  const code = err.code;
16076
16368
  return skipped(`it can't be read (${typeof code === "string" ? code : "error"})`);
16077
16369
  }
16078
- const { rules, errors } = parsePolicy(text, path);
16079
- return { path, rules, problems: errors, ok: true };
16370
+ const { rules, defaults: defaults2, errors } = parsePolicy(text, path);
16371
+ return { path, rules, defaults: defaults2, problems: errors, ok: true };
16080
16372
  }
16081
16373
  async policy() {
16082
16374
  const files = [await this.policyFile(SUDOERS)];
@@ -16105,7 +16397,10 @@ var Sudo = class {
16105
16397
  }
16106
16398
  }
16107
16399
  if (!detailed && skipped > 0) await caller.report?.(skippedNotice(skipped));
16108
- return files.flatMap((f) => f.rules);
16400
+ return {
16401
+ rules: files.flatMap((f) => f.rules),
16402
+ defaults: files.flatMap((f) => f.defaults ?? [])
16403
+ };
16109
16404
  }
16110
16405
  async check(cred, call) {
16111
16406
  const lines2 = [];
@@ -16118,17 +16413,26 @@ var Sudo = class {
16118
16413
  if (read && problems.length === 0) lines2.push([1, `${path}: parsed OK`]);
16119
16414
  if (problems.length > 0) ok4 = false;
16120
16415
  };
16416
+ const values = (defaults2) => {
16417
+ for (const line of defaultsText(defaults2)) lines2.push([1, line]);
16418
+ };
16121
16419
  if (call.files !== void 0) {
16420
+ const defaults2 = [];
16122
16421
  for (const { name: name2, text } of call.files) {
16123
- report({ path: name2, rules: [], problems: parsePolicy(text).errors, ok: true });
16422
+ const parsed2 = parsePolicy(text);
16423
+ defaults2.push(...parsed2.defaults);
16424
+ report({ path: name2, rules: [], problems: parsed2.errors, ok: true });
16124
16425
  }
16426
+ if (defaults2.length > 0) values(defaults2);
16125
16427
  return { lines: lines2, ok: ok4 };
16126
16428
  }
16127
16429
  if (this.o.mode === "enforce" && cred.euid !== 0) {
16128
16430
  const hint = "check the policy as root (sudo visudo -c), or a file with visudo -c -f <file>";
16129
16431
  return { lines: [[2, `visudo: ${SUDOERS}: Permission denied: ${hint}`]], ok: false };
16130
16432
  }
16131
- for (const file of await this.policy()) report(file);
16433
+ const files = await this.policy();
16434
+ for (const file of files) report(file);
16435
+ values(files.flatMap((f) => f.defaults ?? []));
16132
16436
  return { lines: lines2, ok: ok4 };
16133
16437
  }
16134
16438
  async groupIds() {
@@ -16137,9 +16441,9 @@ var Sudo = class {
16137
16441
  async applies(caller, cred) {
16138
16442
  const gids = await this.groupIds();
16139
16443
  const mine = /* @__PURE__ */ new Set([cred.rgid, cred.egid, ...cred.groups]);
16140
- return (rule) => {
16141
- if (rule.who === "ALL" || rule.who === caller.name) return true;
16142
- const gid = rule.who.startsWith("%") ? gids.get(rule.who.slice(1)) : void 0;
16444
+ return (who) => {
16445
+ if (who === "ALL" || who === caller.name) return true;
16446
+ const gid = who.startsWith("%") ? gids.get(who.slice(1)) : void 0;
16143
16447
  return gid !== void 0 && mine.has(gid);
16144
16448
  };
16145
16449
  }
@@ -16184,18 +16488,40 @@ var Sudo = class {
16184
16488
  const mode = this.o.mode;
16185
16489
  if (parsed2.list) return { list: `sudo: users are '${mode}': commands run as the caller
16186
16490
  ` };
16187
- return { run: parsed2.command };
16491
+ return parsed2.command.length > 0 ? { run: parsed2.command } : DONE;
16188
16492
  }
16189
16493
  const { cred } = caller;
16190
16494
  const policy = await this.policyRules(caller);
16191
16495
  const self = await this.o.users.lookup(cred.ruid);
16192
16496
  if (!self) return failed2("EPERM", `sudo: uid ${cred.ruid} is not in the passwd database`);
16497
+ const owner = {
16498
+ uid: cred.ruid,
16499
+ terminal: caller.terminal,
16500
+ parent: caller.parent
16501
+ };
16502
+ const applies = await this.applies(self, cred);
16503
+ const settings = resolveDefaults(policy.defaults, applies);
16504
+ if (parsed2.removeAll) {
16505
+ this.timestamps.remove(owner.uid);
16506
+ return DONE;
16507
+ }
16508
+ if (parsed2.reset && !parsed2.validate && !parsed2.list && parsed2.command.length === 0) {
16509
+ this.timestamps.invalidate(owner, settings.timestampType);
16510
+ return DONE;
16511
+ }
16193
16512
  const target = await this.lookup(parsed2.user);
16194
16513
  if (!target) return failed2("ENOENT", `sudo: unknown user ${parsed2.user}`);
16195
16514
  const root = cred.euid === 0;
16196
- const rules = root ? [] : policy;
16197
- const applies = await this.applies(self, cred);
16198
- if (parsed2.list) return this.listed(self, root, rules.filter(applies));
16515
+ const rules = root ? [] : policy.rules;
16516
+ const mine = rules.filter((rule) => applies(rule.who));
16517
+ if (parsed2.list) return this.listed(self, root, mine, settings.matching);
16518
+ const session = { caller, self, target, owner, policy: policyOf(settings), parsed: parsed2, signal };
16519
+ if (parsed2.validate) return this.validate(session, root, mine, call.cwd);
16520
+ return this.command(session, root, mine, call);
16521
+ }
16522
+ async command(s, root, mine, call) {
16523
+ const { caller, self, target, parsed: parsed2 } = s;
16524
+ const { cred } = caller;
16199
16525
  const [word = "", ...args] = parsed2.command;
16200
16526
  const cwd = await this.canonical(call.cwd);
16201
16527
  if (cwd === void 0 || !await this.reachable(cred, cwd)) {
@@ -16209,8 +16535,8 @@ var Sudo = class {
16209
16535
  const untrusted2 = await this.untrusted(path, cwd, word);
16210
16536
  if (untrusted2) return untrusted2;
16211
16537
  const argv = [path, ...args];
16212
- const matching = rules.filter(
16213
- (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)
16538
+ const matching = mine.filter(
16539
+ (rule) => (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
16214
16540
  );
16215
16541
  const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
16216
16542
  if (root || matching.some((rule) => rule.nopasswd)) return grant();
@@ -16221,23 +16547,67 @@ var Sudo = class {
16221
16547
  `sudo: ${self.name} is not allowed to run ${quoted} as ${target.name}`
16222
16548
  );
16223
16549
  }
16550
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
16551
+ this.timestamps.stamp(s.owner, s.policy);
16552
+ this.logged(s, cwd, argv);
16553
+ return grant();
16554
+ }
16224
16555
  if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
16225
- if (this.pending.has(cred.ruid)) return failed2("EBUSY", "sudo: another request is pending");
16226
- this.pending.add(cred.ruid);
16227
- try {
16228
- const answer2 = await this.ask(caller, self, target, parsed2.command, path, cwd, signal);
16229
- if (typeof answer2 !== "string") return answer2;
16556
+ return this.pendingFor(cred.ruid, async () => {
16557
+ const answer2 = await this.ask(s, { argv: parsed2.command, path, cwd });
16558
+ if ("error" in answer2) return answer2;
16230
16559
  if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
16231
16560
  return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
16232
16561
  }
16233
16562
  const changed = await this.untrusted(path, cwd, word);
16234
16563
  if (changed) return changed;
16235
- if (answer2 === "once") return grant();
16236
- return await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
16564
+ const result = answer2.answer === "once" ? grant() : await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
16565
+ if (!("error" in result)) this.stamped(s, answer2);
16566
+ return result;
16567
+ });
16568
+ }
16569
+ async validate(s, root, mine, dir) {
16570
+ const { self, parsed: parsed2 } = s;
16571
+ if (root) return DONE;
16572
+ if (mine.length === 0) {
16573
+ return failed2("EPERM", `Sorry, user ${self.name} may not run sudo on ${this.o.hostname}.`);
16574
+ }
16575
+ if (mine.every((rule) => rule.nopasswd)) return DONE;
16576
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
16577
+ this.timestamps.stamp(s.owner, s.policy);
16578
+ return DONE;
16579
+ }
16580
+ if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
16581
+ const cwd = await this.canonical(dir) ?? normalizePath(dir);
16582
+ return this.pendingFor(s.caller.cred.ruid, async () => {
16583
+ const answer2 = await this.ask(s, { argv: [], path: "", cwd, validate: true });
16584
+ if ("error" in answer2) return answer2;
16585
+ if (!answer2.person) await s.caller.report?.(NOT_STAMPED);
16586
+ this.stamped(s, answer2);
16587
+ return DONE;
16588
+ });
16589
+ }
16590
+ async pendingFor(uid, work) {
16591
+ if (this.pending.has(uid)) return failed2("EBUSY", "sudo: another request is pending");
16592
+ this.pending.add(uid);
16593
+ try {
16594
+ return await work();
16237
16595
  } finally {
16238
- this.pending.delete(cred.ruid);
16596
+ this.pending.delete(uid);
16239
16597
  }
16240
16598
  }
16599
+ stamped(s, answer2) {
16600
+ if (answer2.person && !s.parsed.reset) this.timestamps.stamp(s.owner, s.policy);
16601
+ }
16602
+ logged(s, cwd, argv) {
16603
+ const { caller, self, target, owner, policy } = s;
16604
+ const type = policy.type === "tty" && !owner.terminal ? "ppid" : policy.type;
16605
+ const tty = owner.terminal?.tty ?? caller.tty ?? "unknown";
16606
+ const command = argv.map(quoteWord).join(" ");
16607
+ (this.o.warn ?? console.warn)(
16608
+ `sudo: ${self.name} : TTY=${tty} ; PWD=${quoteWord(cwd)} ; USER=${target.name} ; TIMESTAMP=${type} ; COMMAND=${command}`
16609
+ );
16610
+ }
16241
16611
  async always(self, target, cwd, argv, shown2, grant) {
16242
16612
  if (!shown2 || !recordable(cwd, argv)) return { ...grant(), notice: RAN_ONCE };
16243
16613
  if (!await this.remember(self, target, cwd, argv)) {
@@ -16245,7 +16615,7 @@ var Sudo = class {
16245
16615
  }
16246
16616
  return grant();
16247
16617
  }
16248
- listed(self, root, rules) {
16618
+ listed(self, root, rules, defaults2) {
16249
16619
  const host = this.o.hostname;
16250
16620
  if (root) {
16251
16621
  return {
@@ -16258,11 +16628,20 @@ var Sudo = class {
16258
16628
  return failed2("EPERM", `User ${self.name} is not allowed to run sudo on ${host}.`);
16259
16629
  }
16260
16630
  const lines2 = rules.map(described).join("\n");
16261
- return { list: `User ${self.name} may run the following commands on ${host}:
16631
+ const params = defaults2.flatMap((d) => d.params).join(", ");
16632
+ const matching = params === "" ? "" : `Matching Defaults entries for ${self.name} on ${host}:
16633
+ ${params}
16634
+
16635
+ `;
16636
+ return {
16637
+ list: `${matching}User ${self.name} may run the following commands on ${host}:
16262
16638
  ${lines2}
16263
- ` };
16639
+ `
16640
+ };
16264
16641
  }
16265
- async ask(caller, self, target, argv, path, cwd, requester) {
16642
+ async ask(s, asked) {
16643
+ const { caller, self, target, signal: requester } = s;
16644
+ const { argv, path, cwd } = asked;
16266
16645
  const { approve } = this.o;
16267
16646
  if (!approve || requester.aborted) return failed2("EACCES", DENIED2);
16268
16647
  const createdAt = Date.now();
@@ -16281,9 +16660,11 @@ ${lines2}
16281
16660
  ...caller.tty ? { tty: caller.tty } : {},
16282
16661
  createdAt,
16283
16662
  expiresAt: createdAt + timeout,
16284
- always: recordable(cwd, [path, ...argv]),
16663
+ always: asked.validate ? false : recordable(cwd, [path, ...argv]),
16285
16664
  stdin: caller.stdin,
16286
- rootOnly: await this.rootOnly(cwd, argv.slice(1))
16665
+ rootOnly: asked.validate ? {} : await this.rootOnly(cwd, argv.slice(1)),
16666
+ ...asked.validate ? { validate: true } : {},
16667
+ timestamp: true
16287
16668
  };
16288
16669
  const control = new AbortController();
16289
16670
  const timer = setTimeout(() => control.abort("expired"), timeout);
@@ -16293,9 +16674,8 @@ ${lines2}
16293
16674
  try {
16294
16675
  const answer2 = await answerOf(approve, req, control.signal);
16295
16676
  if (answer2 === ABORTED) return ended();
16296
- if (answer2 === "once" || answer2 === "always") {
16297
- return control.signal.aborted ? ended() : answer2;
16298
- }
16677
+ const approved = approvalOf(answer2);
16678
+ if (approved) return control.signal.aborted ? ended() : approved;
16299
16679
  return failed2("EACCES", denialText(answer2));
16300
16680
  } finally {
16301
16681
  clearTimeout(timer);
@@ -16305,7 +16685,7 @@ ${lines2}
16305
16685
  async remember(self, target, cwd, argv) {
16306
16686
  const { fs } = this.o;
16307
16687
  const path = `${SUDOERS_D}/${self.name}`;
16308
- if (!NAME2.test(self.name)) return false;
16688
+ if (!NAME3.test(self.name)) return false;
16309
16689
  try {
16310
16690
  await this.provision();
16311
16691
  if (!await this.trusted(SUDOERS_D)) return false;
@@ -17674,8 +18054,8 @@ function sameCert(a, b) {
17674
18054
  if (!a || !b) return a === b;
17675
18055
  return a.cert.length === b.cert.length && a.cert.every((byte, i) => byte === b.cert[i]);
17676
18056
  }
17677
- function winner(current, expected, record2) {
17678
- return sameCert(current, expected) ? record2 : current;
18057
+ function winner(current, expected, record3) {
18058
+ return sameCert(current, expected) ? record3 : current;
17679
18059
  }
17680
18060
  var DAY = 24 * 60 * 60 * 1e3;
17681
18061
  var CA_LIFETIME = 3650 * DAY;
@@ -17692,14 +18072,14 @@ function validLeafName(host) {
17692
18072
  }
17693
18073
  var RealmCa = class _RealmCa {
17694
18074
  record;
17695
- constructor(record2) {
17696
- this.record = record2;
18075
+ constructor(record3) {
18076
+ this.record = record3;
17697
18077
  }
17698
18078
  static async open(owner, store, now = Date.now()) {
17699
18079
  const stored = await store.get(owner).catch(() => void 0);
17700
18080
  if (stored && stored.notAfter - now > CA_RENEW_BEFORE) return new _RealmCa(stored);
17701
- const record2 = await _RealmCa.create(owner, now);
17702
- return new _RealmCa(await store.swap(owner, stored, record2));
18081
+ const record3 = await _RealmCa.create(owner, now);
18082
+ return new _RealmCa(await store.swap(owner, stored, record3));
17703
18083
  }
17704
18084
  static async create(owner, now) {
17705
18085
  const pair = await crypto.subtle.generateKey(ECDSA, false, ["sign", "verify"]);
@@ -17925,8 +18305,8 @@ function memoryCaStore() {
17925
18305
  const records = /* @__PURE__ */ new Map();
17926
18306
  return {
17927
18307
  get: async (owner) => records.get(owner),
17928
- swap: async (owner, expected, record2) => {
17929
- const kept = winner(records.get(owner), expected, record2);
18308
+ swap: async (owner, expected, record3) => {
18309
+ const kept = winner(records.get(owner), expected, record3);
17930
18310
  records.set(owner, kept);
17931
18311
  return kept;
17932
18312
  }
@@ -19852,7 +20232,7 @@ function replaced(text, values) {
19852
20232
  for (const [ph, value2] of Object.entries(values)) out = out.split(ph).join(binary(value2));
19853
20233
  return out;
19854
20234
  }
19855
- function usable(values) {
20235
+ function usable2(values) {
19856
20236
  const out = {};
19857
20237
  for (const [ph, value2] of Object.entries(values ?? {}))
19858
20238
  if (encoder8.encode(value2).length >= MIN_VALUE) out[ph] = value2;
@@ -19867,7 +20247,7 @@ async function substituteHead(head, ask) {
19867
20247
  if (!request || !found || found.length !== 1) return void 0;
19868
20248
  const at = found[0];
19869
20249
  if (at.placeholders.length > SESSION_PLACEHOLDERS) return void 0;
19870
- const values = usable(
20250
+ const values = usable2(
19871
20251
  await ask(`header:${at.name}`, at.placeholders, {
19872
20252
  method: request[1],
19873
20253
  path: request[2]
@@ -20177,7 +20557,7 @@ async function cleartext(message3, asks) {
20177
20557
  const placeholders = placeholdersIn(password);
20178
20558
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS)
20179
20559
  return { bytes: message3, done: true };
20180
- const values = usable(await asks.cleartext(placeholders));
20560
+ const values = usable2(await asks.cleartext(placeholders));
20181
20561
  if (Object.keys(values).length === 0) return { bytes: message3, done: true };
20182
20562
  return { bytes: frame3(112, `${replaced(password, values)}\0`), values, done: true };
20183
20563
  }
@@ -20242,7 +20622,7 @@ var PgClient = class {
20242
20622
  const x = got.exchange;
20243
20623
  const binding = this.header === END_POINT ? this.server.binding : void 0;
20244
20624
  const serverFinal = `c=${channelOf(this.header, binding)},r=${got.nonce}`;
20245
- const values = usable(
20625
+ const values = usable2(
20246
20626
  await this.asks.scram({
20247
20627
  clientFirstBare: x.clientFirstBare,
20248
20628
  serverFirst: x.serverFirst,
@@ -20291,7 +20671,7 @@ function decoded(b642) {
20291
20671
  async function swapped(text, position, ask) {
20292
20672
  const placeholders = placeholdersIn(text);
20293
20673
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS) return void 0;
20294
- const values = usable(await ask(position, placeholders));
20674
+ const values = usable2(await ask(position, placeholders));
20295
20675
  if (Object.keys(values).length === 0) return void 0;
20296
20676
  return { text: replaced(text, values), values };
20297
20677
  }
@@ -26495,7 +26875,7 @@ function header2(id2, flags2, counts) {
26495
26875
  function opt() {
26496
26876
  return [0, 0, TYPE_OPT, UDP_EDNS >> 8, UDP_EDNS & 255, 0, 0, 0, 0, 0, 0];
26497
26877
  }
26498
- function record(type, rdata) {
26878
+ function record2(type, rdata) {
26499
26879
  const ttl = DNS_STUB_TTL;
26500
26880
  return [
26501
26881
  192,
@@ -26603,14 +26983,14 @@ var DnsStub = class {
26603
26983
  const address = reverseAddress(q.name);
26604
26984
  const name2 = address !== void 0 ? await this.reverse(address) : void 0;
26605
26985
  if (name2 !== void 0)
26606
- return answerReply(query, q, NOERROR, [record(TYPE_PTR, encodeName(name2))], true);
26986
+ return answerReply(query, q, NOERROR, [record2(TYPE_PTR, encodeName(name2))], true);
26607
26987
  return this.forward(query, q);
26608
26988
  }
26609
26989
  if (q.type === TYPE_A || q.type === TYPE_AAAA) {
26610
26990
  const addresses = q.name === "" ? [] : await this.lookup(q.name);
26611
26991
  const answers2 = addresses.flatMap((a) => {
26612
26992
  const data = addressData(a, q.type);
26613
- return data ? [record(q.type, data)] : [];
26993
+ return data ? [record2(q.type, data)] : [];
26614
26994
  });
26615
26995
  if (answers2.length > 0 || local(q.name) || await this.known(q.name)) {
26616
26996
  const rcode = addresses.length > 0 ? NOERROR : NXDOMAIN;
@@ -28938,8 +29318,8 @@ var OpfsFs = class {
28938
29318
  const [entry] = await this.meta.get([target]);
28939
29319
  if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
28940
29320
  const kept = fresh3 ? cached : this.files.get(target);
28941
- const usable2 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
28942
- return [usable2 ? kept : void 0, target, true];
29321
+ const usable3 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
29322
+ return [usable3 ? kept : void 0, target, true];
28943
29323
  }
28944
29324
  async writeAt(handle, target, path, offset, bytes) {
28945
29325
  await this.counting(target);
@@ -29741,8 +30121,8 @@ var OpfsFs = class {
29741
30121
  renamingDirs = /* @__PURE__ */ new Set();
29742
30122
  intents = 0;
29743
30123
  async intend(rename, at = `${RENAMES2}/${crypto.randomUUID()}`) {
29744
- const record2 = { ...rename, at: Date.now() * 1e3 + this.intents++ % 1e3 };
29745
- await this.meta.update(at, () => ({ path: at, rename: record2 }));
30124
+ const record3 = { ...rename, at: Date.now() * 1e3 + this.intents++ % 1e3 };
30125
+ await this.meta.update(at, () => ({ path: at, rename: record3 }));
29746
30126
  return at;
29747
30127
  }
29748
30128
  async rollBack(from, to, intents) {
@@ -31935,6 +32315,7 @@ var SUDO_SOURCE = [
31935
32315
  " await say(1, answer.list);",
31936
32316
  " return 0;",
31937
32317
  " }",
32318
+ " if (answer.done === true) return 0;",
31938
32319
  " if (answer.run !== undefined) {",
31939
32320
  " try {",
31940
32321
  " pid = (await ctx.spawn({ argv: answer.run, pgid: 0 })).pid;",
@@ -32570,6 +32951,7 @@ var Launcher = class {
32570
32951
  );
32571
32952
  }
32572
32953
  this.users = new UserDb(this.fs, this.owners);
32954
+ const { sudoClock } = options;
32573
32955
  this.sudoer = new Sudo({
32574
32956
  fs: this.fs,
32575
32957
  users: this.users,
@@ -32579,7 +32961,8 @@ var Launcher = class {
32579
32961
  pnpmHome: this.pnpmHome,
32580
32962
  backing: (path, cwd) => this.backing(path, cwd),
32581
32963
  approve: options.approve,
32582
- timeoutMs: options.sudoTimeoutMs
32964
+ timeoutMs: options.sudoTimeoutMs,
32965
+ now: sudoClock ? () => sudoClock.now() : void 0
32583
32966
  });
32584
32967
  this.cron = this.cronService(options.cron);
32585
32968
  void this.unlinked.sweep("/");
@@ -33150,8 +33533,8 @@ var Launcher = class {
33150
33533
  const caller = this.jobs.shown(ppid);
33151
33534
  const named = typeof req.argv0 === "string" && !planned.script && !planned.target.fixed;
33152
33535
  const argv = named ? [req.argv0, ...req.argv.slice(1)] : req.argv;
33153
- const exact = booked?.pid === caller && req.file === booked.path && await physicalPathAsync(this.base, req.cwd) === booked.cwd && await argvDigest(argv) === booked.argv;
33154
- if (!exact) throw new SpawnError("EACCES");
33536
+ const exact2 = booked?.pid === caller && req.file === booked.path && await physicalPathAsync(this.base, req.cwd) === booked.cwd && await argvDigest(argv) === booked.argv;
33537
+ if (!exact2) throw new SpawnError("EACCES");
33155
33538
  const { requested: path, requestedCwd: cwd, path: resolved2 } = booked;
33156
33539
  const pid = this.nextPid++;
33157
33540
  const ids = { pid: req.exec ? caller : pid, ppid: caller };
@@ -33985,11 +34368,26 @@ var Launcher = class {
33985
34368
  pid: this.jobs.shown(pid),
33986
34369
  tags: this.tagsOf(pid),
33987
34370
  tty: this.described.get(pid)?.tty,
34371
+ ...this.timestampOwner(pid),
33988
34372
  stdin,
33989
34373
  report
33990
34374
  };
33991
34375
  return this.sudoer.request(caller, call, signal);
33992
34376
  }
34377
+ timestampOwner(pid) {
34378
+ const sid = this.jobs.sidOf(pid);
34379
+ const tty = this.jobs.controllingTerminal(pid)?.name;
34380
+ const leader = sid === void 0 ? void 0 : this.startedOf(sid);
34381
+ const host = this.jobs.shownParent(pid) ?? this.jobs.shown(pid);
34382
+ const started = this.startedOf(host);
34383
+ return {
34384
+ ...tty && sid !== void 0 && leader !== void 0 ? { terminal: { tty, sid, started: leader } } : {},
34385
+ ...started === void 0 ? {} : { parent: { pid: host, started } }
34386
+ };
34387
+ }
34388
+ startedOf(shown2) {
34389
+ return this.running(shown2) ? this.described.get(shown2)?.started : void 0;
34390
+ }
33993
34391
  visudo(pid, call) {
33994
34392
  return this.sudoer.check(this.credOf(pid), call);
33995
34393
  }
@@ -34499,9 +34897,9 @@ var Launcher = class {
34499
34897
  }
34500
34898
  pnpmReady() {
34501
34899
  if (this.pnpmMade !== void 0) return this.pnpmMade;
34502
- const epoch = this.pnpmEpoch;
34900
+ const epoch2 = this.pnpmEpoch;
34503
34901
  const made2 = this.base.mkdir(this.pnpmHome, { recursive: true }).then(() => this.owners ? this.stampLayout() : void 0).catch(() => {
34504
- if (this.pnpmEpoch === epoch) this.pnpmMade = void 0;
34902
+ if (this.pnpmEpoch === epoch2) this.pnpmMade = void 0;
34505
34903
  });
34506
34904
  this.pnpmMade = made2;
34507
34905
  return made2;
@@ -37676,6 +38074,7 @@ async function createNodeKernel(options = {}) {
37676
38074
  ...options.clipboard ? { clipboard: options.clipboard } : {},
37677
38075
  ...options.requests ? { embedder: checkedRequests(options.requests) } : {},
37678
38076
  ...options.approve ? { approve: options.approve } : {},
38077
+ ...options.sudo?.clock ? { sudoClock: options.sudo.clock } : {},
37679
38078
  ...options.wasmMemories ? { wasmMemories: options.wasmMemories } : {},
37680
38079
  cron: { lock: cronLock(root), ...options.cron?.clock ? { clock: options.cron.clock } : {} }
37681
38080
  });