toolroll 0.9.57 → 0.9.59
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/CHANGELOG.md +15 -0
- package/dist/action-ledger.js +10 -5
- package/dist/actor.d.ts +0 -10
- package/dist/actor.js +1 -18
- package/dist/api-tokens.d.ts +33 -2
- package/dist/api-tokens.js +39 -11
- package/dist/assignment-delivery.js +1 -1
- package/dist/assignment-status.js +1 -1
- package/dist/assignment.js +3 -2
- package/dist/browser/workspace.js +1 -1
- package/dist/builder.js +4 -2
- package/dist/chat-channel.d.ts +1 -0
- package/dist/chat-channel.js +3 -0
- package/dist/chat-delivery.js +2 -0
- package/dist/cli-http.d.ts +7 -0
- package/dist/cli-http.js +30 -4
- package/dist/cli.js +2 -1
- package/dist/contract-changes.d.ts +52 -0
- package/dist/contract-changes.js +61 -0
- package/dist/coordinator.d.ts +2 -1
- package/dist/coordinator.js +22 -31
- package/dist/decision.d.ts +4 -0
- package/dist/decision.js +8 -0
- package/dist/deploy-lock.d.ts +8 -0
- package/dist/deploy-lock.js +45 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/export.js +2 -2
- package/dist/lead-cli.js +3 -0
- package/dist/lead-doors.js +3 -0
- package/dist/lead-tools.js +2 -1
- package/dist/mcp-http.js +5 -5
- package/dist/needs-you.js +3 -1
- package/dist/operate.js +23 -11
- package/dist/password-bearer.d.ts +27 -0
- package/dist/password-bearer.js +59 -0
- package/dist/planner-source.d.ts +23 -53
- package/dist/planner-source.js +43 -68
- package/dist/release-gate.d.ts +2 -3
- package/dist/release-gate.js +44 -16
- package/dist/remote-command.js +0 -2
- package/dist/remote-exec.d.ts +19 -0
- package/dist/remote-exec.js +9 -8
- package/dist/request-budget.d.ts +1 -1
- package/dist/scope.js +16 -1
- package/dist/serve.js +15 -0
- package/dist/server/edge-refusal.d.ts +2 -0
- package/dist/server/edge-refusal.js +8 -6
- package/dist/server/guards.js +5 -4
- package/dist/server/http.d.ts +2 -0
- package/dist/server/remote.js +6 -13
- package/dist/server/render-chat.js +10 -0
- package/dist/server/render-tasks.js +19 -9
- package/dist/server/route-table.js +0 -3
- package/dist/server/tasks.js +3 -1
- package/dist/session-cli.d.ts +38 -9
- package/dist/session-cli.js +141 -154
- package/dist/session-contract.js +2 -2
- package/dist/session-remote.d.ts +19 -0
- package/dist/session-remote.js +86 -0
- package/dist/store.d.ts +72 -12
- package/dist/store.js +367 -105
- package/dist/task-status.js +3 -1
- package/dist/team-cli.d.ts +3 -0
- package/dist/team-cli.js +23 -3
- package/dist/telegram-lead.js +6 -0
- package/dist/telegram.js +9 -7
- package/dist/toolroll-update.js +10 -1
- package/package.json +1 -1
- package/dist/session-http.d.ts +0 -24
- package/dist/session-http.js +0 -170
- package/dist/session-server.d.ts +0 -17
- package/dist/session-server.js +0 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.59 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
- **Safer deploys.** A deploy that fails before the swap releases its own
|
|
6
|
+
lock. A stranded lock clears with one guided command.
|
|
7
|
+
- **Planning restarts on a new scope.** Changing a task's scope restarts its
|
|
8
|
+
planning. Cancelling a task closes its open questions.
|
|
9
|
+
|
|
10
|
+
## 0.9.58 — 2026-10-09
|
|
11
|
+
|
|
12
|
+
- **One token kind.** Personal, lead and coordinator tokens now live in one
|
|
13
|
+
table with one check. Existing tokens keep working.
|
|
14
|
+
- **Sessions join the CLI API.** The sessions API is now part of the CLI API.
|
|
15
|
+
- **Passwords as tokens warn.** Sending your password as a token still works
|
|
16
|
+
this release but warns. Create a token with `toolroll tokens create`.
|
|
17
|
+
|
|
3
18
|
## 0.9.57 — 2026-10-09
|
|
4
19
|
|
|
5
20
|
- **Truthful status.** Finished tasks read Complete everywhere. With checks
|
package/dist/action-ledger.js
CHANGED
|
@@ -36,6 +36,11 @@ export function installLedgerTriggers(db) {
|
|
|
36
36
|
const task = (ref) => `(SELECT external_id FROM task_ref WHERE id = ${ref})`;
|
|
37
37
|
const runRef = `(SELECT task_ref FROM run WHERE id = NEW.run)`;
|
|
38
38
|
const event = (at, actor, project, taskId, runId, action, outcome) => `INSERT INTO action_ledger(at,actor,repo,task_id,run_id,action,outcome,source) VALUES (${at},${actor},${project},${taskId},${runId},'${action}',${outcome},'work');`;
|
|
39
|
+
// v119: closing a question without an answer is its own event. Replace only the old trigger's future behavior;
|
|
40
|
+
// every ledger entry already recorded, and its seal, stays unchanged.
|
|
41
|
+
const answeredTrigger = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = 'ledger_decision_answered'").get();
|
|
42
|
+
if (answeredTrigger !== undefined && !String(answeredTrigger["sql"]).includes("superseded_reason"))
|
|
43
|
+
db.exec("DROP TRIGGER ledger_decision_answered");
|
|
39
44
|
db.exec(`
|
|
40
45
|
CREATE TRIGGER IF NOT EXISTS ledger_run_started AFTER INSERT ON run BEGIN
|
|
41
46
|
${event("NEW.started_at", "NEW.runner", repo("NEW.task_ref"), task("NEW.task_ref"), "NEW.id", "run started", "NEW.role")}
|
|
@@ -48,9 +53,13 @@ CREATE TRIGGER IF NOT EXISTS ledger_decision_opened AFTER INSERT ON decision BEG
|
|
|
48
53
|
${event("NEW.created_at", "'system'", repo(runRef), task(runRef), "NEW.run", "decision opened", "NEW.state")}
|
|
49
54
|
END;
|
|
50
55
|
CREATE TRIGGER IF NOT EXISTS ledger_decision_answered AFTER UPDATE OF answered_at ON decision
|
|
51
|
-
WHEN NEW.answered_at IS NOT NULL AND OLD.answered_at IS NULL BEGIN
|
|
56
|
+
WHEN NEW.answered_at IS NOT NULL AND OLD.answered_at IS NULL AND NEW.superseded_reason IS NULL BEGIN
|
|
52
57
|
${event("NEW.answered_at", "COALESCE(NEW.answered_by,'system')", repo(runRef), task(runRef), "NEW.run", "decision answered", "NEW.state")}
|
|
53
58
|
END;
|
|
59
|
+
CREATE TRIGGER IF NOT EXISTS ledger_decision_closed AFTER UPDATE OF answered_at ON decision
|
|
60
|
+
WHEN NEW.answered_at IS NOT NULL AND OLD.answered_at IS NULL AND NEW.superseded_reason IS NOT NULL BEGIN
|
|
61
|
+
${event("NEW.answered_at", "COALESCE(NEW.answered_by,'system')", repo(runRef), task(runRef), "NEW.run", "decision closed", "NEW.superseded_reason")}
|
|
62
|
+
END;
|
|
54
63
|
CREATE TRIGGER IF NOT EXISTS ledger_scope_approved AFTER UPDATE OF approved_at, approved_digest ON task_scope
|
|
55
64
|
WHEN NEW.approved_at IS NOT NULL AND (OLD.approved_at IS NOT NEW.approved_at OR OLD.approved_digest IS NOT NEW.approved_digest) BEGIN
|
|
56
65
|
${event("NEW.approved_at", "COALESCE(NEW.approved_by,'system')", "(SELECT repo FROM task_ref WHERE backend = 'built-in' AND external_id = NEW.task_id)", "NEW.task_id", "NULL", "scope approved", "'approved'")}
|
|
@@ -63,10 +72,6 @@ CREATE TRIGGER IF NOT EXISTS ledger_task_placed AFTER UPDATE OF repo ON task_ref
|
|
|
63
72
|
WHEN OLD.repo IS NOT NEW.repo BEGIN
|
|
64
73
|
${event("strftime('%Y-%m-%dT%H:%M:%fZ','now')", "'system'", "NEW.repo", "NEW.external_id", "NULL", "task placed", "'recorded'")}
|
|
65
74
|
END;
|
|
66
|
-
CREATE TRIGGER IF NOT EXISTS ledger_coordinator_minted AFTER INSERT ON coordinator_credential BEGIN
|
|
67
|
-
INSERT INTO action_ledger(at,actor,repo,task_id,run_id,action,outcome,source,detail) VALUES (NEW.created_at, NEW.created_by, NULL, NULL, NULL,
|
|
68
|
-
'coordinator minted: ' || NEW.name, 'minted', 'access', 'projects: ' || NEW.repos);
|
|
69
|
-
END;
|
|
70
75
|
CREATE TRIGGER IF NOT EXISTS ledger_task_registered AFTER INSERT ON task_ref BEGIN
|
|
71
76
|
${event("strftime('%Y-%m-%dT%H:%M:%fZ','now')", "'system'", "NEW.repo", "NEW.external_id", "NULL", "task registered", "'recorded'")}
|
|
72
77
|
END;
|
package/dist/actor.d.ts
CHANGED
|
@@ -9,13 +9,3 @@ export declare function currentActor(): Actor | null;
|
|
|
9
9
|
export declare function claimActor(account: string): void;
|
|
10
10
|
/** How the ledger names an actor. */
|
|
11
11
|
export declare function actorLabel(actor: Actor): string;
|
|
12
|
-
export declare function mintLeadToken(): {
|
|
13
|
-
id: string;
|
|
14
|
-
token: string;
|
|
15
|
-
hash: string;
|
|
16
|
-
};
|
|
17
|
-
export declare function parseLeadToken(presented: string): {
|
|
18
|
-
id: string;
|
|
19
|
-
secret: string;
|
|
20
|
-
} | null;
|
|
21
|
-
export declare function leadSecretMatches(secret: string, keptHash: string): boolean;
|
package/dist/actor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Who is acting right now, for pings that follow responsibility.
|
|
3
3
|
*
|
|
4
4
|
* A lead is an agent acting for one person under a lead token (`toolroll lead
|
|
5
|
-
* token`): it signs in as that person, and its acts are recorded as "lead for
|
|
5
|
+
* token`, a `credential` row of kind 'lead', api-tokens.ts): it signs in as that person, and its acts are recorded as "lead for
|
|
6
6
|
* <owner>". A person acting for themselves is recorded by name. Work whose
|
|
7
7
|
* actor is the lead pings nobody, and nobody is pinged about their own act.
|
|
8
8
|
*
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* long-running service: a worker's facts are nobody's act.
|
|
11
11
|
*/
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
14
13
|
/** The slot is mutable so a command can name its person after it has verified their password. */
|
|
15
14
|
const slot = new AsyncLocalStorage();
|
|
16
15
|
/** Run `fn` with this actor (or an empty slot a verified sign-in may fill). */
|
|
@@ -30,19 +29,3 @@ export function claimActor(account) {
|
|
|
30
29
|
export function actorLabel(actor) {
|
|
31
30
|
return actor.lead ? `lead for ${actor.account}` : actor.account;
|
|
32
31
|
}
|
|
33
|
-
// ---- the lead token ---------------------------------------------------------
|
|
34
|
-
/** `lt_<id>_<secret>`: shown once, only its hash kept. */
|
|
35
|
-
const SHAPE = /^lt_([a-f0-9]{12})_([A-Za-z0-9_-]{43})$/;
|
|
36
|
-
export function mintLeadToken() {
|
|
37
|
-
const id = randomBytes(6).toString("hex"), secret = randomBytes(32).toString("base64url");
|
|
38
|
-
return { id, token: `lt_${id}_${secret}`, hash: hashLeadSecret(secret) };
|
|
39
|
-
}
|
|
40
|
-
const hashLeadSecret = (secret) => createHash("sha256").update(secret, "utf8").digest("hex");
|
|
41
|
-
export function parseLeadToken(presented) {
|
|
42
|
-
const match = SHAPE.exec(presented.trim());
|
|
43
|
-
return match === null ? null : { id: match[1], secret: match[2] };
|
|
44
|
-
}
|
|
45
|
-
export function leadSecretMatches(secret, keptHash) {
|
|
46
|
-
const a = Buffer.from(hashLeadSecret(secret), "hex"), b = Buffer.from(keptHash, "hex");
|
|
47
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
48
|
-
}
|
package/dist/api-tokens.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type TokenAccess = "read" | "act";
|
|
2
2
|
export type TokenPurpose = "api" | "mcp";
|
|
3
|
+
/** Who a credential stands for: a person (their API token), a person's lead, or a coordinator. */
|
|
4
|
+
export type CredentialKind = "person" | "lead" | "coordinator";
|
|
3
5
|
export declare const MCP_ROTATION_REFUSAL = "Reconnect the MCP client to renew this token. You can still revoke it here.";
|
|
4
6
|
/** Missing purpose is an old ordinary token; an unrecognised stored purpose is never treated as ordinary. */
|
|
5
7
|
export declare const tokenPurpose: (value: unknown) => TokenPurpose;
|
|
@@ -7,18 +9,47 @@ export declare const tokenRotationProblem: (token: {
|
|
|
7
9
|
purpose: TokenPurpose;
|
|
8
10
|
}) => string | null;
|
|
9
11
|
export declare const TOKEN_DAYS: readonly [30, 90, 365];
|
|
10
|
-
|
|
12
|
+
/** What a presented secret says it is, before any check. A coordinator's secret has no prefix and is hashed whole. */
|
|
13
|
+
export type PresentedCredential = {
|
|
14
|
+
kind: "person" | "lead";
|
|
15
|
+
id: string;
|
|
16
|
+
secret: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "coordinator";
|
|
19
|
+
secret: string;
|
|
20
|
+
};
|
|
21
|
+
/** A new credential of `kind`: its id, the secret shown once, and the only thing kept, its hash. */
|
|
22
|
+
export declare function mintCredential(kind: CredentialKind): {
|
|
11
23
|
id: string;
|
|
12
24
|
secret: string;
|
|
13
25
|
token: string;
|
|
14
26
|
hash: string;
|
|
15
27
|
};
|
|
16
28
|
export declare const hashSecret: (secret: string) => string;
|
|
17
|
-
/**
|
|
29
|
+
/** Read a presented credential's kind and parts; null for nothing at all. A lead token may carry surrounding space. */
|
|
30
|
+
export declare function parseCredential(presented: string): PresentedCredential | null;
|
|
31
|
+
export declare const mintApiToken: () => {
|
|
32
|
+
id: string;
|
|
33
|
+
secret: string;
|
|
34
|
+
token: string;
|
|
35
|
+
hash: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const mintLeadToken: () => {
|
|
38
|
+
id: string;
|
|
39
|
+
secret: string;
|
|
40
|
+
token: string;
|
|
41
|
+
hash: string;
|
|
42
|
+
};
|
|
43
|
+
/** The id and secret of something shaped like an API token, or null. */
|
|
18
44
|
export declare function parseApiToken(presented: string): {
|
|
19
45
|
id: string;
|
|
20
46
|
secret: string;
|
|
21
47
|
} | null;
|
|
48
|
+
/** The id and secret of something shaped like a lead token, or null. */
|
|
49
|
+
export declare function parseLeadToken(presented: string): {
|
|
50
|
+
id: string;
|
|
51
|
+
secret: string;
|
|
52
|
+
} | null;
|
|
22
53
|
/** Whether the secret is the one whose hash was kept, in constant time. */
|
|
23
54
|
export declare function secretMatches(secret: string, keptHash: string): boolean;
|
|
24
55
|
/** v111: how long a rotated token keeps working beside its replacement, by default and at most. */
|
package/dist/api-tokens.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* request
|
|
4
|
-
* person may), always expires (a year at most), is shown once, and only its
|
|
5
|
-
*
|
|
2
|
+
* Machine credentials (D7, v118): one codec and one table (`credential`) for every kind. A person's API token is
|
|
3
|
+
* what scripts and CI use instead of a password on each request: it belongs to one person, reads or acts (never
|
|
4
|
+
* more than the person may), always expires (a year at most), is shown once, and only its hash is kept.
|
|
5
|
+
* `Authorization: Bearer so_<id>_<secret>`. A lead token (`lt_<id>_<secret>`) is an agent acting for its owner; a
|
|
6
|
+
* coordinator's secret (no prefix, hashed whole) files proposals over MCP. Each kind keeps its own policy.
|
|
6
7
|
*
|
|
7
8
|
* A token never passes a step-up: approvals and other password ceremonies
|
|
8
9
|
* stay a person's act in the console.
|
|
@@ -13,16 +14,43 @@ export const MCP_ROTATION_REFUSAL = "Reconnect the MCP client to renew this toke
|
|
|
13
14
|
export const tokenPurpose = (value) => value === undefined || value === "api" ? "api" : "mcp";
|
|
14
15
|
export const tokenRotationProblem = (token) => token.purpose === "api" ? null : MCP_ROTATION_REFUSAL;
|
|
15
16
|
export const TOKEN_DAYS = [30, 90, 365];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/** The prefixed shapes: so_ (a person's API token) and lt_ (a lead token), each `<prefix>_<12 hex>_<43 base64url>`. */
|
|
18
|
+
const PRESENTED = /^(so|lt)_([a-f0-9]{12})_([A-Za-z0-9_-]{43})$/;
|
|
19
|
+
const PREFIX = { person: "so", lead: "lt" };
|
|
20
|
+
const CID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
21
|
+
/** A new credential of `kind`: its id, the secret shown once, and the only thing kept, its hash. */
|
|
22
|
+
export function mintCredential(kind) {
|
|
23
|
+
const secret = randomBytes(32).toString("base64url");
|
|
24
|
+
if (kind === "coordinator") {
|
|
25
|
+
let id = "";
|
|
26
|
+
for (const byte of randomBytes(12))
|
|
27
|
+
id += CID_ALPHABET[byte % CID_ALPHABET.length];
|
|
28
|
+
return { id, secret, token: secret, hash: hashSecret(secret) };
|
|
29
|
+
}
|
|
30
|
+
const id = randomBytes(6).toString("hex");
|
|
31
|
+
return { id, secret, token: `${PREFIX[kind]}_${id}_${secret}`, hash: hashSecret(secret) };
|
|
20
32
|
}
|
|
21
33
|
export const hashSecret = (secret) => createHash("sha256").update(secret, "utf8").digest("hex");
|
|
22
|
-
/**
|
|
34
|
+
/** Read a presented credential's kind and parts; null for nothing at all. A lead token may carry surrounding space. */
|
|
35
|
+
export function parseCredential(presented) {
|
|
36
|
+
if (presented === "")
|
|
37
|
+
return null;
|
|
38
|
+
const match = PRESENTED.exec(presented) ?? (/^lt_/.test(presented.trim()) ? PRESENTED.exec(presented.trim()) : null);
|
|
39
|
+
if (match !== null)
|
|
40
|
+
return { kind: match[1] === "so" ? "person" : "lead", id: match[2], secret: match[3] };
|
|
41
|
+
return { kind: "coordinator", secret: presented };
|
|
42
|
+
}
|
|
43
|
+
export const mintApiToken = () => mintCredential("person");
|
|
44
|
+
export const mintLeadToken = () => mintCredential("lead");
|
|
45
|
+
/** The id and secret of something shaped like an API token, or null. */
|
|
23
46
|
export function parseApiToken(presented) {
|
|
24
|
-
const
|
|
25
|
-
return
|
|
47
|
+
const parsed = parseCredential(presented);
|
|
48
|
+
return parsed?.kind === "person" && PRESENTED.test(presented) ? { id: parsed.id, secret: parsed.secret } : null;
|
|
49
|
+
}
|
|
50
|
+
/** The id and secret of something shaped like a lead token, or null. */
|
|
51
|
+
export function parseLeadToken(presented) {
|
|
52
|
+
const parsed = parseCredential(presented);
|
|
53
|
+
return parsed?.kind === "lead" ? { id: parsed.id, secret: parsed.secret } : null;
|
|
26
54
|
}
|
|
27
55
|
/** Whether the secret is the one whose hash was kept, in constant time. */
|
|
28
56
|
export function secretMatches(secret, keptHash) {
|
|
@@ -14,7 +14,7 @@ const ownerSql = `COALESCE((SELECT 'lead:'||o.lead FROM team_task_owner o WHERE
|
|
|
14
14
|
function admittedRepos(store, owner) {
|
|
15
15
|
if (owner.kind !== "coordinator" || typeof owner.id !== "string" || owner.id.length === 0)
|
|
16
16
|
return null;
|
|
17
|
-
const row = store.handle.prepare("SELECT repos,revoked_at FROM
|
|
17
|
+
const row = store.handle.prepare("SELECT projects_json AS repos,revoked_at FROM credential WHERE id = ? AND kind = 'coordinator'").get(owner.id);
|
|
18
18
|
if (!row || row["revoked_at"] !== null)
|
|
19
19
|
return null;
|
|
20
20
|
try {
|
|
@@ -92,7 +92,7 @@ export function noteAssignmentStatus(store, snapshot, now) {
|
|
|
92
92
|
const claimed = store.handle.prepare("SELECT actor FROM action_ledger WHERE task_id=? AND action='assignment claimed' AND source='work' ORDER BY id DESC LIMIT 1").get(snapshot.rootId);
|
|
93
93
|
if ((claimed === undefined ? ref.coordinatorCid : String(claimed["actor"]).replace(/^coordinator:/, "")) !== owner.id)
|
|
94
94
|
return null;
|
|
95
|
-
const credential = store.handle.prepare("SELECT repos FROM
|
|
95
|
+
const credential = store.handle.prepare("SELECT projects_json AS repos FROM credential WHERE id=? AND kind='coordinator' AND revoked_at IS NULL").get(owner.id);
|
|
96
96
|
let repos;
|
|
97
97
|
try {
|
|
98
98
|
repos = JSON.parse(String(credential?.["repos"] ?? "null"));
|
package/dist/assignment.js
CHANGED
|
@@ -73,7 +73,8 @@ function ownerOf(store, rootId, repo) {
|
|
|
73
73
|
const id = claim === undefined ? origin : String(claim["actor"]).replace(/^coordinator:/, "");
|
|
74
74
|
if (!id)
|
|
75
75
|
return null;
|
|
76
|
-
|
|
76
|
+
// A ledger actor names a coordinator by the id it had when it acted (v118 keeps that as public_id).
|
|
77
|
+
const credential = store.handle.prepare("SELECT name,projects_json AS repos,revoked_at FROM credential WHERE kind = 'coordinator' AND (id = ? OR public_id = ?) ORDER BY id = ? DESC LIMIT 1").get(id, id, id);
|
|
77
78
|
let repos = [];
|
|
78
79
|
try {
|
|
79
80
|
repos = JSON.parse(String(credential?.["repos"] ?? "[]"));
|
|
@@ -379,7 +380,7 @@ function readinessOf(store, receipt, repo, review) {
|
|
|
379
380
|
function admittedOwner(store, taskId, owner, now, root) {
|
|
380
381
|
if (owner.kind !== "coordinator")
|
|
381
382
|
return null; // A stable lead is identity, not a bearer credential.
|
|
382
|
-
const row = store.handle.prepare("SELECT repos, revoked_at FROM
|
|
383
|
+
const row = store.handle.prepare("SELECT projects_json AS repos, revoked_at FROM credential WHERE id = ? AND kind = 'coordinator'").get(owner.id);
|
|
383
384
|
if (row === undefined || row["revoked_at"] !== null)
|
|
384
385
|
return null;
|
|
385
386
|
let repos;
|