@profullstack/agenticjobs 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -1
- package/dist/cli/args.js +31 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/index.js +396 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/jobfile.js +38 -2
- package/dist/cli/jobfile.js.map +1 -1
- package/dist/client/client.d.ts +149 -0
- package/dist/client/client.js +79 -12
- package/dist/client/client.js.map +1 -1
- package/dist/client/fanout.js +7 -1
- package/dist/client/fanout.js.map +1 -1
- package/dist/config.d.ts +35 -1
- package/dist/config.js +33 -1
- package/dist/config.js.map +1 -1
- package/dist/core/candidates.js +33 -6
- package/dist/core/candidates.js.map +1 -1
- package/dist/core/capacity-alert.d.ts +84 -0
- package/dist/core/capacity-alert.js +144 -0
- package/dist/core/capacity-alert.js.map +1 -0
- package/dist/core/capacity.d.ts +96 -0
- package/dist/core/capacity.js +174 -0
- package/dist/core/capacity.js.map +1 -0
- package/dist/core/coinpay.d.ts +143 -0
- package/dist/core/coinpay.js +417 -0
- package/dist/core/coinpay.js.map +1 -0
- package/dist/core/import-job.js +18 -7
- package/dist/core/import-job.js.map +1 -1
- package/dist/core/import.js +18 -15
- package/dist/core/import.js.map +1 -1
- package/dist/core/inbox.d.ts +155 -0
- package/dist/core/inbox.js +353 -0
- package/dist/core/inbox.js.map +1 -0
- package/dist/core/invoices.d.ts +111 -0
- package/dist/core/invoices.js +292 -0
- package/dist/core/invoices.js.map +1 -0
- package/dist/core/jobs.js +12 -2
- package/dist/core/jobs.js.map +1 -1
- package/dist/core/mail.d.ts +16 -0
- package/dist/core/mail.js +22 -0
- package/dist/core/mail.js.map +1 -1
- package/dist/core/orgs.d.ts +39 -0
- package/dist/core/orgs.js +69 -0
- package/dist/core/orgs.js.map +1 -1
- package/dist/directory/federate.js +2 -4
- package/dist/directory/federate.js.map +1 -1
- package/dist/directory/fetch.d.ts +1 -1
- package/dist/markup/markdown.js +4 -1
- package/dist/markup/markdown.js.map +1 -1
- package/dist/markup/resume.js +66 -1
- package/dist/markup/resume.js.map +1 -1
- package/dist/mcp/server.js +5 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.js +3 -2
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/mcp/tools.js +151 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/schema/jsonld.js +1 -8
- package/dist/schema/jsonld.js.map +1 -1
- package/dist/schema/salary.d.ts +5 -0
- package/dist/schema/salary.js +15 -0
- package/dist/schema/salary.js.map +1 -0
- package/dist/schema/text.js +7 -3
- package/dist/schema/text.js.map +1 -1
- package/dist/server/app.d.ts +4 -3
- package/dist/server/app.js +7 -4
- package/dist/server/app.js.map +1 -1
- package/dist/server/deps.d.ts +5 -0
- package/dist/server/middleware.js +9 -1
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/routes/api.js +347 -1
- package/dist/server/routes/api.js.map +1 -1
- package/dist/server/routes/discovery.js +75 -0
- package/dist/server/routes/discovery.js.map +1 -1
- package/dist/server/routes/inbox.d.ts +9 -0
- package/dist/server/routes/inbox.js +223 -0
- package/dist/server/routes/inbox.js.map +1 -0
- package/dist/server/routes/openapi.js +141 -1
- package/dist/server/routes/openapi.js.map +1 -1
- package/dist/server/routes/pages.d.ts +22 -0
- package/dist/server/routes/pages.js +91 -7
- package/dist/server/routes/pages.js.map +1 -1
- package/dist/views/candidates.d.ts +8 -0
- package/dist/views/candidates.js +2 -1
- package/dist/views/candidates.js.map +1 -1
- package/dist/views/docs.js +48 -4
- package/dist/views/docs.js.map +1 -1
- package/dist/views/inbox.d.ts +90 -0
- package/dist/views/inbox.js +73 -0
- package/dist/views/inbox.js.map +1 -0
- package/dist/views/jobs.d.ts +9 -0
- package/dist/views/jobs.js +3 -2
- package/dist/views/jobs.js.map +1 -1
- package/dist/views/layout.d.ts +2 -0
- package/dist/views/layout.js +2 -2
- package/dist/views/layout.js.map +1 -1
- package/dist/views/me.d.ts +3 -3
- package/dist/views/me.js +2 -2
- package/dist/views/me.js.map +1 -1
- package/dist/views/updates.d.ts +13 -0
- package/dist/views/updates.js +2 -1
- package/dist/views/updates.js.map +1 -1
- package/docs/openjob.md +5 -0
- package/docs/openresume.md +36 -0
- package/migrations/0014_inbox_and_billing.sql +138 -0
- package/package.json +15 -17
- package/web/public/app.css +146 -3
- package/web/public/sw.js +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm capacity: how many agents a candidate brings, and what they cost.
|
|
3
|
+
*
|
|
4
|
+
* This board's thesis is agents hiring agents, and the question an employer
|
|
5
|
+
* actually has to answer before hiring one is not on any resume yet: is this a
|
|
6
|
+
* single agent, or someone who runs ten in parallel? The difference is the
|
|
7
|
+
* difference between a contractor and a firm, and it changes both the price
|
|
8
|
+
* and the kind of work you would send.
|
|
9
|
+
*
|
|
10
|
+
* It is read out of the resume rather than stored beside it, for the reason
|
|
11
|
+
* `candidates.ts` gives: a profile that restates the document is a second copy
|
|
12
|
+
* to keep in step. The contact block is the right home because it is already
|
|
13
|
+
* where the resume keeps its scalars — `Email`, `Location` — and a reader,
|
|
14
|
+
* an agent and a parser all see the same two lines:
|
|
15
|
+
*
|
|
16
|
+
* # Athena
|
|
17
|
+
* - **Email**: a@b.com
|
|
18
|
+
* - **Agents**: 10
|
|
19
|
+
* - **Rate**: $100/hour/agent
|
|
20
|
+
*
|
|
21
|
+
* Like every other OpenResume convention this one degrades. A resume with
|
|
22
|
+
* neither key is a resume with unstated capacity, not an invalid one, and
|
|
23
|
+
* `parseCapacity` returns null rather than throwing. That matters here more
|
|
24
|
+
* than elsewhere: every resume written before this convention existed has no
|
|
25
|
+
* capacity, and none of them should stop rendering.
|
|
26
|
+
*/
|
|
27
|
+
/** Contact keys that answer "how many agents". */
|
|
28
|
+
const AGENTS_KEYS = /^(agents?|sub-?agents?|swarm(\s*size)?|parallelism|capacity)$/i;
|
|
29
|
+
/** Contact keys that answer "what does it cost". */
|
|
30
|
+
const RATE_KEYS = /^(rate|rates|price|pricing|cost|hourly|hourly\s*rate)$/i;
|
|
31
|
+
/** Currency symbols used when no supported currency code is stated. */
|
|
32
|
+
const SYMBOLS = [
|
|
33
|
+
['$', 'USD'],
|
|
34
|
+
['€', 'EUR'],
|
|
35
|
+
['£', 'GBP'],
|
|
36
|
+
['¥', 'JPY'],
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Agents, from a value a person actually typed.
|
|
40
|
+
*
|
|
41
|
+
* "10", "10 agents", "up to 10" and "single" all appear in the wild, so the
|
|
42
|
+
* first integer wins and the words that mean one are handled by name. A count
|
|
43
|
+
* of zero is treated as unstated: nobody is offering zero agents, so it is far
|
|
44
|
+
* more likely to be a placeholder than a claim.
|
|
45
|
+
*/
|
|
46
|
+
export function parseAgentCount(value) {
|
|
47
|
+
const text = value.trim();
|
|
48
|
+
if (text === '')
|
|
49
|
+
return null;
|
|
50
|
+
if (/^(a\s+)?(single|solo|one|just\s+me|1\s*\(single[^)]*\))$/i.test(text))
|
|
51
|
+
return 1;
|
|
52
|
+
const match = /\d+/.exec(text.replace(/,/g, ''));
|
|
53
|
+
if (match === null)
|
|
54
|
+
return null;
|
|
55
|
+
const count = Number.parseInt(match[0], 10);
|
|
56
|
+
if (!Number.isFinite(count) || count <= 0)
|
|
57
|
+
return null;
|
|
58
|
+
// A four-digit swarm is far more likely to be a price that landed in the
|
|
59
|
+
// wrong field than a real fleet, and listing it would put a nonsense number
|
|
60
|
+
// at the top of a card.
|
|
61
|
+
return count > 1000 ? null : count;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A price, and whether it is per agent.
|
|
65
|
+
*
|
|
66
|
+
* The per-agent question is the one that matters and the one people express
|
|
67
|
+
* loosely: "$100/hour/agent", "$100 per agent per hour", "$100/hr each". Any
|
|
68
|
+
* of those means one agent costs 100. Without such a marker the figure is read
|
|
69
|
+
* as the price of the whole swarm, because "$1000/hour" from someone running
|
|
70
|
+
* ten agents is a swarm price — reading it as per-agent would report a rate
|
|
71
|
+
* ten times too high, which is the expensive direction to be wrong in.
|
|
72
|
+
*/
|
|
73
|
+
export function parseRate(value) {
|
|
74
|
+
const text = value.trim();
|
|
75
|
+
if (text === '')
|
|
76
|
+
return null;
|
|
77
|
+
// An explicit code qualifies an ambiguous symbol, e.g. "CAD $100".
|
|
78
|
+
const code = /\b(usd|eur|gbp|jpy|cad|aud|chf|sek|nzd)\b/i.exec(text);
|
|
79
|
+
let currency = code?.[1]?.toUpperCase() ?? '';
|
|
80
|
+
if (currency === '') {
|
|
81
|
+
for (const [symbol, symbolCode] of SYMBOLS) {
|
|
82
|
+
if (text.includes(symbol)) {
|
|
83
|
+
currency = symbolCode;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Strip any currency code before looking for digits, or "USD 100" would be
|
|
89
|
+
// fine but a stray code containing digits would not.
|
|
90
|
+
const numeric = text.replace(/\b[a-z]{3}\b/gi, ' ').replace(/,/g, '');
|
|
91
|
+
const match = /(?:\d+(?:\.\d+)?|\.\d+)/.exec(numeric);
|
|
92
|
+
if (match === null)
|
|
93
|
+
return null;
|
|
94
|
+
const amount = Number.parseFloat(match[0]);
|
|
95
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
96
|
+
return null;
|
|
97
|
+
const perAgent = /(\/|\bper\s+)agent\b|\beach\b|\ban?\s+agent\b|\bper\s+bot\b/i.test(text);
|
|
98
|
+
return { amount, currency: currency === '' ? 'USD' : currency, perAgent };
|
|
99
|
+
}
|
|
100
|
+
/** Round money to cents, so a division never reports 33.333333333. */
|
|
101
|
+
function money(value) {
|
|
102
|
+
return Math.round(value * 100) / 100;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Capacity for a resume, or null when it does not state any.
|
|
106
|
+
*
|
|
107
|
+
* A count with no price is still capacity worth showing — "10 agents, price on
|
|
108
|
+
* request" is a real listing — so a missing rate is not fatal. A price with no
|
|
109
|
+
* count is not: without knowing how many agents it buys, a swarm price and a
|
|
110
|
+
* single-agent price are indistinguishable, and guessing turns an employer's
|
|
111
|
+
* budget into a surprise. That one comes back null.
|
|
112
|
+
*/
|
|
113
|
+
export function parseCapacity(contact) {
|
|
114
|
+
const agentsEntry = contact.find((item) => AGENTS_KEYS.test(item.key.trim()));
|
|
115
|
+
if (agentsEntry === undefined)
|
|
116
|
+
return null;
|
|
117
|
+
const agents = parseAgentCount(agentsEntry.value);
|
|
118
|
+
if (agents === null)
|
|
119
|
+
return null;
|
|
120
|
+
const rateEntry = contact.find((item) => RATE_KEYS.test(item.key.trim()));
|
|
121
|
+
const rate = rateEntry === undefined ? null : parseRate(rateEntry.value);
|
|
122
|
+
if (rate === null) {
|
|
123
|
+
return {
|
|
124
|
+
agents,
|
|
125
|
+
ratePerAgent: null,
|
|
126
|
+
totalPerHour: null,
|
|
127
|
+
currency: 'USD',
|
|
128
|
+
ratePerAgentStated: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (rate.perAgent) {
|
|
132
|
+
return {
|
|
133
|
+
agents,
|
|
134
|
+
ratePerAgent: money(rate.amount),
|
|
135
|
+
totalPerHour: money(rate.amount * agents),
|
|
136
|
+
currency: rate.currency,
|
|
137
|
+
ratePerAgentStated: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
agents,
|
|
142
|
+
ratePerAgent: money(rate.amount / agents),
|
|
143
|
+
totalPerHour: money(rate.amount),
|
|
144
|
+
currency: rate.currency,
|
|
145
|
+
ratePerAgentStated: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** `1234.5` as `1,234.50`, and `1000` as `1,000`. */
|
|
149
|
+
function amount(value) {
|
|
150
|
+
const whole = Number.isInteger(value);
|
|
151
|
+
return value.toLocaleString('en-US', {
|
|
152
|
+
minimumFractionDigits: whole ? 0 : 2,
|
|
153
|
+
maximumFractionDigits: 2,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* One line an employer can read, e.g. "10 agents · $100/hr each · $1,000/hr total".
|
|
158
|
+
*
|
|
159
|
+
* The total is the number being shopped for and the per-agent rate is how it
|
|
160
|
+
* is justified, so both are shown. A single agent gets neither a multiplication
|
|
161
|
+
* nor the word "total", because "1 agent · $100/hr · $100/hr total" reads like
|
|
162
|
+
* a bug.
|
|
163
|
+
*/
|
|
164
|
+
export function formatCapacity(capacity) {
|
|
165
|
+
const agents = capacity.agents === 1 ? '1 agent' : `${capacity.agents} agents`;
|
|
166
|
+
if (capacity.totalPerHour === null)
|
|
167
|
+
return `${agents} · rate on request`;
|
|
168
|
+
const unit = capacity.currency === 'USD' ? '$' : `${capacity.currency} `;
|
|
169
|
+
if (capacity.agents === 1)
|
|
170
|
+
return `${agents} · ${unit}${amount(capacity.totalPerHour)}/hr`;
|
|
171
|
+
const each = capacity.ratePerAgent === null ? '' : ` · ${unit}${amount(capacity.ratePerAgent)}/hr each`;
|
|
172
|
+
return `${agents}${each} · ${unit}${amount(capacity.totalPerHour)}/hr total`;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=capacity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capacity.js","sourceRoot":"","sources":["../../src/core/capacity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoinPay: connecting a person's account, and minting the payments that settle
|
|
3
|
+
* an invoice to their wallet.
|
|
4
|
+
*
|
|
5
|
+
* Two credentials do two jobs, and it matters which is which:
|
|
6
|
+
*
|
|
7
|
+
* - A person connects their own CoinPay account over OAuth. The board asks for
|
|
8
|
+
* `wallet:read` and uses the token for exactly one call, `userinfo`, which
|
|
9
|
+
* returns the wallets they can be paid to. That is all the token can do;
|
|
10
|
+
* CoinPay's OAuth has no scope for creating anything.
|
|
11
|
+
* - The board has a business key of its own. That is what creates a payment,
|
|
12
|
+
* with the payee's address as `merchant_wallet_address`, and CoinPay records
|
|
13
|
+
* who authorised the third-party payout. The board never holds funds.
|
|
14
|
+
*
|
|
15
|
+
* `wallet:read` is checked on the scope the token came back with, not on the
|
|
16
|
+
* scope that was asked for. CoinPay narrows a grant to what the client was
|
|
17
|
+
* registered for without an error, and a board that trusts its own request
|
|
18
|
+
* ends up showing "Connected" beside an account it cannot read a wallet from.
|
|
19
|
+
*
|
|
20
|
+
* Everything here goes through one `fetch`, injected, so the suite can stand
|
|
21
|
+
* in for CoinPay without a network.
|
|
22
|
+
*/
|
|
23
|
+
import type pg from 'pg';
|
|
24
|
+
import type { CoinPayConfig } from '../config.ts';
|
|
25
|
+
export declare const REQUIRED_SCOPE = "wallet:read";
|
|
26
|
+
export declare const SCOPES: string[];
|
|
27
|
+
export interface Wallet {
|
|
28
|
+
address: string;
|
|
29
|
+
/** CoinPay's spelling: BTC, ETH, SOL, USDC_POL ... */
|
|
30
|
+
chain: string;
|
|
31
|
+
label: string | null;
|
|
32
|
+
}
|
|
33
|
+
export interface TokenSet {
|
|
34
|
+
accessToken: string;
|
|
35
|
+
refreshToken: string | null;
|
|
36
|
+
scope: string[];
|
|
37
|
+
expiresAt: string;
|
|
38
|
+
}
|
|
39
|
+
export interface UserInfo {
|
|
40
|
+
sub: string;
|
|
41
|
+
email: string | null;
|
|
42
|
+
name: string | null;
|
|
43
|
+
wallets: Wallet[];
|
|
44
|
+
}
|
|
45
|
+
export interface Payment {
|
|
46
|
+
id: string;
|
|
47
|
+
address: string | null;
|
|
48
|
+
amountCrypto: string | null;
|
|
49
|
+
currency: string | null;
|
|
50
|
+
status: string;
|
|
51
|
+
expiresAt: string | null;
|
|
52
|
+
txHash: string | null;
|
|
53
|
+
}
|
|
54
|
+
export declare class CoinPayProblem extends Error {
|
|
55
|
+
}
|
|
56
|
+
export interface CoinPayClient {
|
|
57
|
+
readonly config: CoinPayConfig;
|
|
58
|
+
authorizeUrl(input: {
|
|
59
|
+
state: string;
|
|
60
|
+
codeChallenge: string;
|
|
61
|
+
redirectUri: string;
|
|
62
|
+
}): string;
|
|
63
|
+
exchangeCode(input: {
|
|
64
|
+
code: string;
|
|
65
|
+
redirectUri: string;
|
|
66
|
+
codeVerifier: string;
|
|
67
|
+
}): Promise<TokenSet>;
|
|
68
|
+
refresh(refreshToken: string): Promise<TokenSet>;
|
|
69
|
+
userinfo(accessToken: string): Promise<UserInfo>;
|
|
70
|
+
createPayment(input: {
|
|
71
|
+
amountUsd: string;
|
|
72
|
+
chain: string;
|
|
73
|
+
payeeAddress: string;
|
|
74
|
+
description: string;
|
|
75
|
+
redirectUrl: string;
|
|
76
|
+
idempotencyKey: string;
|
|
77
|
+
metadata: Record<string, string>;
|
|
78
|
+
}): Promise<Payment>;
|
|
79
|
+
getPayment(id: string): Promise<Payment | null>;
|
|
80
|
+
/** Where a payer goes to pay. */
|
|
81
|
+
payUrl(paymentId: string): string;
|
|
82
|
+
/** True when a webhook body was signed with this board's secret. */
|
|
83
|
+
verifyWebhook(rawBody: string, signature: string | undefined, now?: number): boolean;
|
|
84
|
+
}
|
|
85
|
+
type Fetch = typeof fetch;
|
|
86
|
+
export declare function pkcePair(): {
|
|
87
|
+
verifier: string;
|
|
88
|
+
challenge: string;
|
|
89
|
+
};
|
|
90
|
+
/** The wallets claim, as userinfo returns it: `{ address, chain, label }`. */
|
|
91
|
+
export declare function normaliseWallets(value: unknown): Wallet[];
|
|
92
|
+
export declare function hasScope(scope: string[] | string, wanted?: string): boolean;
|
|
93
|
+
export declare function createCoinPay(config: CoinPayConfig, fetchImpl?: Fetch): CoinPayClient;
|
|
94
|
+
export interface Account {
|
|
95
|
+
userId: string;
|
|
96
|
+
sub: string;
|
|
97
|
+
email: string | null;
|
|
98
|
+
name: string | null;
|
|
99
|
+
scope: string[];
|
|
100
|
+
wallets: Wallet[];
|
|
101
|
+
connectedAt: string;
|
|
102
|
+
updatedAt: string;
|
|
103
|
+
/** False when the token cannot read wallets: the person has to reconnect. */
|
|
104
|
+
usable: boolean;
|
|
105
|
+
}
|
|
106
|
+
export declare function getAccount(pool: pg.Pool, userId: string): Promise<Account | null>;
|
|
107
|
+
/**
|
|
108
|
+
* Begin connecting. Returns the URL to send the person to.
|
|
109
|
+
*
|
|
110
|
+
* The state and the PKCE verifier live in the database, not a cookie, so the
|
|
111
|
+
* callback can be completed from any session of the same person and a state
|
|
112
|
+
* is used up by being deleted.
|
|
113
|
+
*/
|
|
114
|
+
export declare function beginConnect(pool: pg.Pool, coinpay: CoinPayClient, userId: string, redirectUri: string, next: string | null): Promise<string>;
|
|
115
|
+
/**
|
|
116
|
+
* Finish connecting. Returns where to send the person, or a sentence.
|
|
117
|
+
*
|
|
118
|
+
* The account is attached to whoever *started* the flow, read off the state,
|
|
119
|
+
* not to whoever is signed in when the callback lands: those are the same
|
|
120
|
+
* person in every honest case and a different one in the case this protects
|
|
121
|
+
* against.
|
|
122
|
+
*/
|
|
123
|
+
export declare function finishConnect(pool: pg.Pool, coinpay: CoinPayClient, input: {
|
|
124
|
+
state: string;
|
|
125
|
+
code: string;
|
|
126
|
+
redirectUri: string;
|
|
127
|
+
}): Promise<{
|
|
128
|
+
userId: string;
|
|
129
|
+
redirect: string | null;
|
|
130
|
+
account: Account;
|
|
131
|
+
} | string>;
|
|
132
|
+
/**
|
|
133
|
+
* Re-read the wallets from CoinPay. Returns the account as it now stands, or
|
|
134
|
+
* a sentence when the connection no longer works.
|
|
135
|
+
*/
|
|
136
|
+
export declare function refreshWallets(pool: pg.Pool, coinpay: CoinPayClient, userId: string): Promise<Account | string>;
|
|
137
|
+
/**
|
|
138
|
+
* Forget the connection. CoinPay publishes no revocation endpoint, so deleting
|
|
139
|
+
* the row is the whole of what can be done; invoices already sent keep the
|
|
140
|
+
* address they were sent with and are unaffected.
|
|
141
|
+
*/
|
|
142
|
+
export declare function disconnect(pool: pg.Pool, userId: string): Promise<boolean>;
|
|
143
|
+
export {};
|