@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
package/dist/schema/jsonld.js
CHANGED
|
@@ -10,14 +10,7 @@
|
|
|
10
10
|
* so they travel in an `additionalProperty` array, which is the vocabulary's
|
|
11
11
|
* own escape hatch and survives every validator.
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
const PER_YEAR = {
|
|
15
|
-
hour: 2080,
|
|
16
|
-
day: 260,
|
|
17
|
-
week: 52,
|
|
18
|
-
month: 12,
|
|
19
|
-
year: 1,
|
|
20
|
-
};
|
|
13
|
+
import { PER_YEAR } from "./salary.js";
|
|
21
14
|
export function jobPostingJsonLd(job, origin) {
|
|
22
15
|
const url = `${origin}/jobs/${job.slug}`;
|
|
23
16
|
const doc = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonld.js","sourceRoot":"","sources":["../../src/schema/jsonld.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"jsonld.js","sourceRoot":"","sources":["../../src/schema/jsonld.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Salary, SalaryPeriod } from './job.ts';
|
|
2
|
+
/** The same annual estimates used in the JobPosting structured data. */
|
|
3
|
+
export declare const PER_YEAR: Record<SalaryPeriod, number>;
|
|
4
|
+
/** Compare the top of the range, falling back to its only stated endpoint. */
|
|
5
|
+
export declare function annualisedTopSalary(salary: Salary): number;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The same annual estimates used in the JobPosting structured data. */
|
|
2
|
+
export const PER_YEAR = {
|
|
3
|
+
hour: 2080,
|
|
4
|
+
day: 260,
|
|
5
|
+
week: 52,
|
|
6
|
+
month: 12,
|
|
7
|
+
year: 1,
|
|
8
|
+
};
|
|
9
|
+
/** Compare the top of the range, falling back to its only stated endpoint. */
|
|
10
|
+
export function annualisedTopSalary(salary) {
|
|
11
|
+
if (salary.unpaid)
|
|
12
|
+
return 0;
|
|
13
|
+
return (salary.max ?? salary.min ?? 0) * (PER_YEAR[salary.period] ?? 1);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=salary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"salary.js","sourceRoot":"","sources":["../../src/schema/salary.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,MAAM,CAAC,MAAM,QAAQ,GAAiC;IACpD,IAAI,EAAE,IAAI;IACV,GAAG,EAAE,GAAG;IACR,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,CAAC;CACR,CAAC;AAEF,8EAA8E;AAC9E,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,IAAI,MAAM,CAAC,MAAM;QAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1E,CAAC"}
|
package/dist/schema/text.js
CHANGED
|
@@ -99,9 +99,13 @@ export function formatSalary(salary) {
|
|
|
99
99
|
};
|
|
100
100
|
const min = salary.min;
|
|
101
101
|
const max = salary.max;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// A single endpoint is a bound, not a fixed salary. Keep that distinction
|
|
103
|
+
// wherever a listing is shown, including the CLI and federated results.
|
|
104
|
+
if (min === null)
|
|
105
|
+
return `Up to ${money(max)} a ${salary.period}`;
|
|
106
|
+
if (max === null)
|
|
107
|
+
return `From ${money(min)} a ${salary.period}`;
|
|
108
|
+
const range = min !== max ? `${money(min)} - ${money(max)}` : money(min);
|
|
105
109
|
return `${range} a ${salary.period}`;
|
|
106
110
|
}
|
|
107
111
|
/** Each entry: below this many seconds, report in the unit named. */
|
package/dist/schema/text.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/schema/text.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/schema/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,KAAK,CAAC,KAAc,EAAE,GAAW,EAAE,UAAmC,EAAE;IACtF,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAClF,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC;QACzD,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,MAAM,IAAI,GAAG,KAAK;SACf,SAAS,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;SAC9B,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;SACZ,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvB,uEAAuE;IACvE,0EAA0E;IAC1E,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACpC,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,MAAM,CAAC,MAAM,GAAG,CAAC;IAC/B,MAAM,QAAQ,GAAG,iCAAiC,CAAC;IACnD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAClE,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,GAAW,EAAE,IAAI,GAAG,EAAE;IAC9D,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7F,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QAChD,IAAI,OAAO,KAAK,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,SAAS;QAClD,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAClB,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;YAAE,MAAM;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,OAAO,GAA2B;IACtC,GAAG,EAAE,GAAG;IACR,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,QAAQ;CACd,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAM5B;IACC,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IAC5C,IAAI,MAAM,CAAC,GAAG,KAAK,IAAI,IAAI,MAAM,CAAC,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5D,MAAM,KAAK,GAAG,CAAC,MAAc,EAAU,EAAE;QACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC;QAC7F,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,GAAG,IAAI,KAAK,CAAC;YAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,CAAC;QAC/E,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;IACtD,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;IACvB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;IACvB,0EAA0E;IAC1E,wEAAwE;IACxE,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,SAAS,KAAK,CAAC,GAAa,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;IAC5E,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,QAAQ,KAAK,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;IACjE,MAAM,KAAK,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzE,OAAO,GAAG,KAAK,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;AACvC,CAAC;AAED,qEAAqE;AACrE,MAAM,KAAK,GAA6E;IACtF,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC;IACpB,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC;IACtB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC;IACxB,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC;IAC5B,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,6DAA6D;AAC7D,MAAM,UAAU,GAAG,CAAC,GAAkB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACtD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,eAAe,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7D,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,UAAU,CAAC;IAEpC,IAAI,OAAO,GAAG,UAAU,CAAC;IACzB,IAAI,IAAI,GAAG,MAAM,CAAC;IAClB,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACvB,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACnB,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAChB,MAAM;QACR,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IACzD,OAAO,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC;AACzD,CAAC"}
|
package/dist/server/app.d.ts
CHANGED
|
@@ -10,9 +10,10 @@ import { Hono } from 'hono';
|
|
|
10
10
|
import type pg from 'pg';
|
|
11
11
|
import type { Config } from '../config.ts';
|
|
12
12
|
import type { Mailer } from '../core/mail.ts';
|
|
13
|
+
import { type CoinPayClient } from '../core/coinpay.ts';
|
|
13
14
|
import type { AppEnv } from './deps.ts';
|
|
14
15
|
/**
|
|
15
|
-
* The mailer
|
|
16
|
-
* would have been sent without a provider or a network.
|
|
16
|
+
* The mailer and the CoinPay client are built once, here, and injectable so a
|
|
17
|
+
* test can watch what would have been sent without a provider or a network.
|
|
17
18
|
*/
|
|
18
|
-
export declare function createApp(pool: pg.Pool, config: Config, mailer?: Mailer | null): Hono<AppEnv>;
|
|
19
|
+
export declare function createApp(pool: pg.Pool, config: Config, mailer?: Mailer | null, coinpay?: CoinPayClient | null): Hono<AppEnv>;
|
package/dist/server/app.js
CHANGED
|
@@ -10,18 +10,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
|
10
10
|
import { Hono } from 'hono';
|
|
11
11
|
import { apiRoutes } from "./routes/api.js";
|
|
12
12
|
import { pageRoutes } from "./routes/pages.js";
|
|
13
|
+
import { inboxRoutes } from "./routes/inbox.js";
|
|
13
14
|
import { discoveryRoutes } from "./routes/discovery.js";
|
|
14
15
|
import { mcpRoutes } from "./routes/mcp.js";
|
|
15
16
|
import { passkeyRoutes } from "./routes/passkey.js";
|
|
16
17
|
import { apiCors, securityHeaders, withViewer } from "./middleware.js";
|
|
17
18
|
import { Layout } from "../views/layout.js";
|
|
18
19
|
import { createMailer } from "../core/mail.js";
|
|
20
|
+
import { createCoinPay } from "../core/coinpay.js";
|
|
19
21
|
/**
|
|
20
|
-
* The mailer
|
|
21
|
-
* would have been sent without a provider or a network.
|
|
22
|
+
* The mailer and the CoinPay client are built once, here, and injectable so a
|
|
23
|
+
* test can watch what would have been sent without a provider or a network.
|
|
22
24
|
*/
|
|
23
|
-
export function createApp(pool, config, mailer = createMailer(config)) {
|
|
24
|
-
const deps = { pool, config, mailer };
|
|
25
|
+
export function createApp(pool, config, mailer = createMailer(config), coinpay = config.coinpay === null ? null : createCoinPay(config.coinpay)) {
|
|
26
|
+
const deps = { pool, config, mailer, coinpay };
|
|
25
27
|
const app = new Hono();
|
|
26
28
|
app.use('*', withViewer(deps));
|
|
27
29
|
app.use('*', securityHeaders());
|
|
@@ -32,6 +34,7 @@ export function createApp(pool, config, mailer = createMailer(config)) {
|
|
|
32
34
|
app.route('/api/mcp', mcpRoutes(() => app));
|
|
33
35
|
app.route('/auth/passkey', passkeyRoutes());
|
|
34
36
|
app.route('/', discoveryRoutes());
|
|
37
|
+
app.route('/', inboxRoutes());
|
|
35
38
|
app.route('/', pageRoutes());
|
|
36
39
|
app.notFound((c) => {
|
|
37
40
|
// A machine asking for JSON gets JSON. Handing an HTML 404 to a client
|
package/dist/server/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.tsx"],"names":[],"mappings":";AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAoB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.tsx"],"names":[],"mappings":"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"}
|
package/dist/server/deps.d.ts
CHANGED
|
@@ -2,11 +2,14 @@ import type pg from 'pg';
|
|
|
2
2
|
import type { Config } from '../config.ts';
|
|
3
3
|
import type { Viewer } from '../core/auth.ts';
|
|
4
4
|
import type { Mailer } from '../core/mail.ts';
|
|
5
|
+
import type { CoinPayClient } from '../core/coinpay.ts';
|
|
5
6
|
export interface Deps {
|
|
6
7
|
pool: pg.Pool;
|
|
7
8
|
config: Config;
|
|
8
9
|
/** Null when this instance sends no mail, which is a supported state. */
|
|
9
10
|
mailer: Mailer | null;
|
|
11
|
+
/** Null when billing is not configured, which is also a supported state. */
|
|
12
|
+
coinpay: CoinPayClient | null;
|
|
10
13
|
}
|
|
11
14
|
/**
|
|
12
15
|
* Hono's per-request bag.
|
|
@@ -19,5 +22,7 @@ export interface AppEnv {
|
|
|
19
22
|
Variables: {
|
|
20
23
|
viewer: Viewer | null;
|
|
21
24
|
deps: Deps;
|
|
25
|
+
/** Conversations with something unread, for the nav. Pages only. */
|
|
26
|
+
unread: number;
|
|
22
27
|
};
|
|
23
28
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getCookie } from 'hono/cookie';
|
|
6
6
|
import { SESSION_COOKIE, viewerFromToken } from "../core/auth.js";
|
|
7
|
+
import { unreadThreads } from "../core/inbox.js";
|
|
7
8
|
/**
|
|
8
9
|
* Resolve the caller.
|
|
9
10
|
*
|
|
@@ -20,6 +21,7 @@ export function withViewer(deps) {
|
|
|
20
21
|
return async (c, next) => {
|
|
21
22
|
c.set('deps', deps);
|
|
22
23
|
c.set('viewer', null);
|
|
24
|
+
c.set('unread', 0);
|
|
23
25
|
const header = c.req.header('authorization') ?? '';
|
|
24
26
|
const bearer = /^Bearer\s+(.+)$/i.exec(header)?.[1]?.trim();
|
|
25
27
|
if (bearer !== undefined && bearer !== '') {
|
|
@@ -29,7 +31,13 @@ export function withViewer(deps) {
|
|
|
29
31
|
}
|
|
30
32
|
const cookie = getCookie(c, SESSION_COOKIE);
|
|
31
33
|
if (cookie !== undefined && cookie !== '') {
|
|
32
|
-
|
|
34
|
+
const viewer = await viewerFromToken(deps.pool, cookie);
|
|
35
|
+
c.set('viewer', viewer);
|
|
36
|
+
// The nav's unread count, for a person on a page. One indexed query, and
|
|
37
|
+
// only on the surface that has a nav: an API call has nowhere to show it.
|
|
38
|
+
if (viewer !== null && !c.req.path.startsWith('/api/') && !c.req.path.startsWith('/assets/')) {
|
|
39
|
+
c.set('unread', await unreadThreads(deps.pool, viewer.id));
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
42
|
await next();
|
|
35
43
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import { approveDeviceCode, createSession, ensureUser, normaliseEmail, pollDevic
|
|
|
18
18
|
import { createApplication, decideApplication, listApplications, listDraftApplications, recentApplicationCount, submitApplication, validateApplication, } from "../../core/applications.js";
|
|
19
19
|
import { countJobs, createJob, getJobBySlug, getJobBySourceUrl, normaliseInput, searchJobs, setStatus, editJob, updateJobFromImport, } from "../../core/jobs.js";
|
|
20
20
|
import { extractJob, JobImportProblem } from "../../core/import-job.js";
|
|
21
|
-
import { createOrg, getOrgBySlug, isMember, listOrgs, listOrgsForUser } from "../../core/orgs.js";
|
|
21
|
+
import { createOrg, deleteOrg, getOrgBySlug, isMember, listOrgs, listOrgsForUser, updateOrg, } from "../../core/orgs.js";
|
|
22
22
|
import { createResume, deleteResume, getPublicResume, getResume, listPublicResumes, getResumeById, getSharedResume, isVisibility, listResumes, updateResume, } from "../../core/resumes.js";
|
|
23
23
|
import { importDocument, ImportProblem, MAX_UPLOAD_BYTES } from "../../core/import.js";
|
|
24
24
|
import { deliverMagicLink } from "../../core/mail.js";
|
|
@@ -33,6 +33,10 @@ import { APPLICATION_DECISIONS, isApplicationDecision } from "../../schema/job.j
|
|
|
33
33
|
import { jobPostingJsonLd } from "../../schema/jsonld.js";
|
|
34
34
|
import { parseResume } from "../../markup/resume.js";
|
|
35
35
|
import { renderMarkdown } from "../../markup/markdown.js";
|
|
36
|
+
import { counterpartyFrom, getThread, listThreads, markRead, notifyParticipants, sendMessage, startThread, } from "../../core/inbox.js";
|
|
37
|
+
import { applyWebhook, cancelInvoice, getInvoice, listInvoicesFor, listInvoicesIn, requestPayment, sendInvoice, syncInvoice, } from "../../core/invoices.js";
|
|
38
|
+
import { disconnect, finishConnect, getAccount } from "../../core/coinpay.js";
|
|
39
|
+
import { coinpayRedirectUri } from "./pages.js";
|
|
36
40
|
import { descriptorFor } from "./descriptor.js";
|
|
37
41
|
import { openApiDocument } from "./openapi.js";
|
|
38
42
|
function fail(c, status, code, message, fields) {
|
|
@@ -42,6 +46,9 @@ function fail(c, status, code, message, fields) {
|
|
|
42
46
|
function viewerOf(c) {
|
|
43
47
|
return c.get('viewer');
|
|
44
48
|
}
|
|
49
|
+
function isUuid(value) {
|
|
50
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
51
|
+
}
|
|
45
52
|
/** Body from JSON or a form, so curl, a browser and a model all work. */
|
|
46
53
|
async function readBody(c) {
|
|
47
54
|
const type = c.req.header('content-type') ?? '';
|
|
@@ -452,6 +459,68 @@ export function apiRoutes() {
|
|
|
452
459
|
return fail(c, 400, 'invalid', created);
|
|
453
460
|
return c.json({ org: created }, 201);
|
|
454
461
|
});
|
|
462
|
+
/**
|
|
463
|
+
* Change an employer.
|
|
464
|
+
*
|
|
465
|
+
* Absent fields are left alone rather than cleared, so a caller that knows
|
|
466
|
+
* about a name and nothing else cannot blank a website it never read. That
|
|
467
|
+
* is also what makes this safe to call from a script that only ever sets
|
|
468
|
+
* one thing.
|
|
469
|
+
*/
|
|
470
|
+
api.patch('/orgs/:slug', async (c) => {
|
|
471
|
+
const { pool } = c.get('deps');
|
|
472
|
+
const viewer = viewerOf(c);
|
|
473
|
+
if (viewer === null)
|
|
474
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to edit an employer.');
|
|
475
|
+
const org = await getOrgBySlug(pool, c.req.param('slug'));
|
|
476
|
+
if (org === null)
|
|
477
|
+
return fail(c, 404, 'not_found', 'No such employer.');
|
|
478
|
+
if (!(await isMember(pool, viewer.id, org.id))) {
|
|
479
|
+
return fail(c, 403, 'not_a_member', `You are not a member of ${org.name}.`);
|
|
480
|
+
}
|
|
481
|
+
const body = await readBody(c);
|
|
482
|
+
const field = (key) => {
|
|
483
|
+
const value = body[key];
|
|
484
|
+
if (value === undefined)
|
|
485
|
+
return undefined;
|
|
486
|
+
// An explicit null clears the field; a string sets it. Anything else is
|
|
487
|
+
// not an answer, so it is treated as not having been sent.
|
|
488
|
+
if (value === null)
|
|
489
|
+
return null;
|
|
490
|
+
return typeof value === 'string' ? value : undefined;
|
|
491
|
+
};
|
|
492
|
+
const updated = await updateOrg(pool, org.slug, {
|
|
493
|
+
...(field('name') === undefined ? {} : { name: field('name') ?? '' }),
|
|
494
|
+
...(field('website') === undefined ? {} : { website: field('website') }),
|
|
495
|
+
...(field('description') === undefined ? {} : { description: field('description') }),
|
|
496
|
+
...(field('logoUrl') === undefined ? {} : { logoUrl: field('logoUrl') }),
|
|
497
|
+
});
|
|
498
|
+
if (typeof updated === 'string')
|
|
499
|
+
return fail(c, 400, 'invalid', updated);
|
|
500
|
+
return c.json({ org: updated });
|
|
501
|
+
});
|
|
502
|
+
/**
|
|
503
|
+
* Delete an employer that never published anything.
|
|
504
|
+
*
|
|
505
|
+
* The rule lives in the model rather than here, because it is a fact about
|
|
506
|
+
* what deleting an organisation drags with it and not a fact about HTTP.
|
|
507
|
+
*/
|
|
508
|
+
api.delete('/orgs/:slug', async (c) => {
|
|
509
|
+
const { pool } = c.get('deps');
|
|
510
|
+
const viewer = viewerOf(c);
|
|
511
|
+
if (viewer === null)
|
|
512
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to delete an employer.');
|
|
513
|
+
const org = await getOrgBySlug(pool, c.req.param('slug'));
|
|
514
|
+
if (org === null)
|
|
515
|
+
return fail(c, 404, 'not_found', 'No such employer.');
|
|
516
|
+
if (!(await isMember(pool, viewer.id, org.id))) {
|
|
517
|
+
return fail(c, 403, 'not_a_member', `You are not a member of ${org.name}.`);
|
|
518
|
+
}
|
|
519
|
+
const removed = await deleteOrg(pool, org.id);
|
|
520
|
+
if (typeof removed === 'string')
|
|
521
|
+
return fail(c, 409, 'has_listings', removed);
|
|
522
|
+
return c.json({ ok: true, deleted: org.slug });
|
|
523
|
+
});
|
|
455
524
|
// --- me and resumes ---------------------------------------------------
|
|
456
525
|
api.get('/me', async (c) => {
|
|
457
526
|
const { pool } = c.get('deps');
|
|
@@ -892,6 +961,283 @@ export function apiRoutes() {
|
|
|
892
961
|
const result = await federatedSearch(targetsFromDescriptors(instances.map((instance) => instance.descriptor)), query);
|
|
893
962
|
return c.json(result);
|
|
894
963
|
});
|
|
964
|
+
// --- inbox ------------------------------------------------------------
|
|
965
|
+
/**
|
|
966
|
+
* Private conversations. Everything here needs the caller's identity, and
|
|
967
|
+
* nothing here is visible to anyone outside the conversation: a thread id
|
|
968
|
+
* that is not yours is a 404, not a 403, so ids cannot be probed.
|
|
969
|
+
*/
|
|
970
|
+
api.get('/inbox', async (c) => {
|
|
971
|
+
const { pool } = c.get('deps');
|
|
972
|
+
const viewer = viewerOf(c);
|
|
973
|
+
if (viewer === null)
|
|
974
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to read your inbox.');
|
|
975
|
+
const items = await listThreads(pool, viewer.id);
|
|
976
|
+
return c.json({ items, total: items.length, unread: items.filter((t) => t.unread > 0).length });
|
|
977
|
+
});
|
|
978
|
+
/**
|
|
979
|
+
* Start a conversation, or continue the one these two parties already have.
|
|
980
|
+
*
|
|
981
|
+
* `candidate` or `employer` names who it is to, by slug. `as` is an employer
|
|
982
|
+
* slug to write as, for a member writing on the company's behalf. `job` ties
|
|
983
|
+
* it to a listing.
|
|
984
|
+
*/
|
|
985
|
+
api.post('/inbox', async (c) => {
|
|
986
|
+
const { pool, config, mailer } = c.get('deps');
|
|
987
|
+
const viewer = viewerOf(c);
|
|
988
|
+
if (viewer === null)
|
|
989
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to send a message.');
|
|
990
|
+
const body = await readBody(c);
|
|
991
|
+
const to = await counterpartyFrom(pool, {
|
|
992
|
+
candidate: typeof body['candidate'] === 'string' ? body['candidate'] : null,
|
|
993
|
+
employer: typeof body['employer'] === 'string' ? body['employer'] : null,
|
|
994
|
+
});
|
|
995
|
+
if (to === null) {
|
|
996
|
+
return fail(c, 404, 'not_found', 'Name a candidate or an employer by slug. Nobody here matches.');
|
|
997
|
+
}
|
|
998
|
+
const jobSlug = typeof body['job'] === 'string' ? body['job'].trim() : '';
|
|
999
|
+
const job = jobSlug === '' ? null : await getJobBySlug(pool, jobSlug);
|
|
1000
|
+
if (jobSlug !== '' && job === null)
|
|
1001
|
+
return fail(c, 404, 'not_found', `No job with the slug "${jobSlug}".`);
|
|
1002
|
+
const asSlug = typeof body['as'] === 'string' ? body['as'].trim() : '';
|
|
1003
|
+
const asOrg = asSlug === '' ? null : await getOrgBySlug(pool, asSlug);
|
|
1004
|
+
if (asSlug !== '' && asOrg === null)
|
|
1005
|
+
return fail(c, 404, 'not_found', `No employer with the slug "${asSlug}".`);
|
|
1006
|
+
const started = await startThread(pool, viewer.id, to, {
|
|
1007
|
+
subject: body['subject'],
|
|
1008
|
+
body: body['body'],
|
|
1009
|
+
jobId: job?.id ?? null,
|
|
1010
|
+
as: asOrg?.id ?? null,
|
|
1011
|
+
});
|
|
1012
|
+
if (typeof started === 'string') {
|
|
1013
|
+
return fail(c, started.includes('conversations today') ? 429 : 400, 'rejected', started);
|
|
1014
|
+
}
|
|
1015
|
+
await notifyParticipants(pool, {
|
|
1016
|
+
mailer,
|
|
1017
|
+
boardName: config.boardName,
|
|
1018
|
+
publicUrl: config.publicUrl,
|
|
1019
|
+
threadId: started.threadId,
|
|
1020
|
+
senderId: viewer.id,
|
|
1021
|
+
kind: 'text',
|
|
1022
|
+
});
|
|
1023
|
+
return c.json({ ...started, url: `${config.publicUrl}/inbox/${started.threadId}` }, started.created ? 201 : 200);
|
|
1024
|
+
});
|
|
1025
|
+
api.get('/inbox/:id', async (c) => {
|
|
1026
|
+
const { pool, coinpay } = c.get('deps');
|
|
1027
|
+
const viewer = viewerOf(c);
|
|
1028
|
+
if (viewer === null)
|
|
1029
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to read your inbox.');
|
|
1030
|
+
const id = c.req.param('id');
|
|
1031
|
+
if (!isUuid(id))
|
|
1032
|
+
return fail(c, 404, 'not_found', 'No such conversation.');
|
|
1033
|
+
const thread = await getThread(pool, id, viewer.id);
|
|
1034
|
+
if (thread === null)
|
|
1035
|
+
return fail(c, 404, 'not_found', 'No such conversation.');
|
|
1036
|
+
const invoices = await Promise.all((await listInvoicesIn(pool, coinpay, id)).map((invoice) => syncInvoice(pool, coinpay, invoice)));
|
|
1037
|
+
await markRead(pool, id, viewer.id);
|
|
1038
|
+
return c.json({ thread, invoices });
|
|
1039
|
+
});
|
|
1040
|
+
api.post('/inbox/:id/messages', async (c) => {
|
|
1041
|
+
const { pool, config, mailer } = c.get('deps');
|
|
1042
|
+
const viewer = viewerOf(c);
|
|
1043
|
+
if (viewer === null)
|
|
1044
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to send a message.');
|
|
1045
|
+
const id = c.req.param('id');
|
|
1046
|
+
if (!isUuid(id))
|
|
1047
|
+
return fail(c, 404, 'not_found', 'No such conversation.');
|
|
1048
|
+
const body = await readBody(c);
|
|
1049
|
+
const sent = await sendMessage(pool, id, viewer.id, body['body']);
|
|
1050
|
+
if (typeof sent === 'string') {
|
|
1051
|
+
return fail(c, sent.includes('not in that') ? 404 : 400, 'rejected', sent);
|
|
1052
|
+
}
|
|
1053
|
+
await notifyParticipants(pool, {
|
|
1054
|
+
mailer,
|
|
1055
|
+
boardName: config.boardName,
|
|
1056
|
+
publicUrl: config.publicUrl,
|
|
1057
|
+
threadId: id,
|
|
1058
|
+
senderId: viewer.id,
|
|
1059
|
+
kind: 'text',
|
|
1060
|
+
});
|
|
1061
|
+
return c.json({ message: sent }, 201);
|
|
1062
|
+
});
|
|
1063
|
+
// --- invoices and billing ------------------------------------------------
|
|
1064
|
+
/**
|
|
1065
|
+
* Send an invoice into a conversation. The caller is the payee; the money
|
|
1066
|
+
* settles to the wallet on their connected CoinPay account, in `currency`
|
|
1067
|
+
* (a chain they have a wallet on). Omit `currency` when there is one wallet.
|
|
1068
|
+
*/
|
|
1069
|
+
api.post('/inbox/:id/invoices', async (c) => {
|
|
1070
|
+
const { pool, config, mailer, coinpay } = c.get('deps');
|
|
1071
|
+
const viewer = viewerOf(c);
|
|
1072
|
+
if (viewer === null)
|
|
1073
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to send an invoice.');
|
|
1074
|
+
const id = c.req.param('id');
|
|
1075
|
+
if (!isUuid(id))
|
|
1076
|
+
return fail(c, 404, 'not_found', 'No such conversation.');
|
|
1077
|
+
const body = await readBody(c);
|
|
1078
|
+
const invoice = await sendInvoice(pool, coinpay, {
|
|
1079
|
+
threadId: id,
|
|
1080
|
+
payeeId: viewer.id,
|
|
1081
|
+
amount: body['amount'],
|
|
1082
|
+
currency: body['currency'],
|
|
1083
|
+
description: body['description'],
|
|
1084
|
+
});
|
|
1085
|
+
if (typeof invoice === 'string') {
|
|
1086
|
+
return fail(c, invoice.includes('not in that') ? 404 : 400, 'rejected', invoice);
|
|
1087
|
+
}
|
|
1088
|
+
await notifyParticipants(pool, {
|
|
1089
|
+
mailer,
|
|
1090
|
+
boardName: config.boardName,
|
|
1091
|
+
publicUrl: config.publicUrl,
|
|
1092
|
+
threadId: id,
|
|
1093
|
+
senderId: viewer.id,
|
|
1094
|
+
kind: 'invoice',
|
|
1095
|
+
});
|
|
1096
|
+
return c.json({ invoice }, 201);
|
|
1097
|
+
});
|
|
1098
|
+
/** Every invoice the caller sent or can pay, newest first. */
|
|
1099
|
+
api.get('/invoices', async (c) => {
|
|
1100
|
+
const { pool, coinpay } = c.get('deps');
|
|
1101
|
+
const viewer = viewerOf(c);
|
|
1102
|
+
if (viewer === null)
|
|
1103
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to read your invoices.');
|
|
1104
|
+
const items = await listInvoicesFor(pool, coinpay, viewer.id);
|
|
1105
|
+
return c.json({ items, total: items.length });
|
|
1106
|
+
});
|
|
1107
|
+
api.get('/invoices/:id', async (c) => {
|
|
1108
|
+
const { pool, coinpay } = c.get('deps');
|
|
1109
|
+
const viewer = viewerOf(c);
|
|
1110
|
+
if (viewer === null)
|
|
1111
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to read an invoice.');
|
|
1112
|
+
const id = c.req.param('id');
|
|
1113
|
+
if (!isUuid(id))
|
|
1114
|
+
return fail(c, 404, 'not_found', 'No such invoice.');
|
|
1115
|
+
const invoice = await getInvoice(pool, coinpay, id, viewer.id);
|
|
1116
|
+
if (invoice === null)
|
|
1117
|
+
return fail(c, 404, 'not_found', 'No such invoice.');
|
|
1118
|
+
return c.json({ invoice: await syncInvoice(pool, coinpay, invoice) });
|
|
1119
|
+
});
|
|
1120
|
+
/**
|
|
1121
|
+
* Get a live quote to pay an invoice. The response carries `payment.url`,
|
|
1122
|
+
* the CoinPay page to pay on, and `payment.address` plus `amountCrypto` for
|
|
1123
|
+
* a wallet that would rather pay directly.
|
|
1124
|
+
*/
|
|
1125
|
+
api.post('/invoices/:id/pay', async (c) => {
|
|
1126
|
+
const { pool, config, coinpay } = c.get('deps');
|
|
1127
|
+
const viewer = viewerOf(c);
|
|
1128
|
+
if (viewer === null)
|
|
1129
|
+
return fail(c, 401, 'unauthenticated', 'Sign in to pay an invoice.');
|
|
1130
|
+
const id = c.req.param('id');
|
|
1131
|
+
if (!isUuid(id))
|
|
1132
|
+
return fail(c, 404, 'not_found', 'No such invoice.');
|
|
1133
|
+
const result = await requestPayment(pool, coinpay, {
|
|
1134
|
+
invoiceId: id,
|
|
1135
|
+
payerId: viewer.id,
|
|
1136
|
+
publicUrl: config.publicUrl,
|
|
1137
|
+
});
|
|
1138
|
+
if (typeof result === 'string') {
|
|
1139
|
+
return fail(c, result === 'No such invoice.' ? 404 : 400, 'rejected', result);
|
|
1140
|
+
}
|
|
1141
|
+
return c.json({ invoice: result });
|
|
1142
|
+
});
|
|
1143
|
+
api.post('/invoices/:id/cancel', async (c) => {
|
|
1144
|
+
const { pool, coinpay } = c.get('deps');
|
|
1145
|
+
const viewer = viewerOf(c);
|
|
1146
|
+
if (viewer === null)
|
|
1147
|
+
return fail(c, 401, 'unauthenticated', 'Sign in first.');
|
|
1148
|
+
const id = c.req.param('id');
|
|
1149
|
+
if (!isUuid(id))
|
|
1150
|
+
return fail(c, 404, 'not_found', 'No such invoice.');
|
|
1151
|
+
const invoice = await getInvoice(pool, coinpay, id, viewer.id);
|
|
1152
|
+
if (invoice === null)
|
|
1153
|
+
return fail(c, 404, 'not_found', 'No such invoice.');
|
|
1154
|
+
const cancelled = await cancelInvoice(pool, id, viewer.id);
|
|
1155
|
+
if (!cancelled) {
|
|
1156
|
+
return fail(c, 409, 'not_cancellable', 'Only the sender can cancel an invoice, and only while it is unpaid.');
|
|
1157
|
+
}
|
|
1158
|
+
return c.json({ ok: true });
|
|
1159
|
+
});
|
|
1160
|
+
/**
|
|
1161
|
+
* The caller's CoinPay connection: whether this board has billing at all,
|
|
1162
|
+
* and whether this account is connected well enough to be paid.
|
|
1163
|
+
*/
|
|
1164
|
+
api.get('/coinpay', async (c) => {
|
|
1165
|
+
const { pool, config, coinpay } = c.get('deps');
|
|
1166
|
+
const viewer = viewerOf(c);
|
|
1167
|
+
if (viewer === null)
|
|
1168
|
+
return fail(c, 401, 'unauthenticated', 'Sign in first.');
|
|
1169
|
+
if (coinpay === null)
|
|
1170
|
+
return c.json({ configured: false, account: null });
|
|
1171
|
+
const account = await getAccount(pool, viewer.id);
|
|
1172
|
+
return c.json({
|
|
1173
|
+
configured: true,
|
|
1174
|
+
account,
|
|
1175
|
+
// Connecting needs a browser: it is CoinPay's consent screen.
|
|
1176
|
+
connectUrl: `${config.publicUrl}/me/coinpay/connect`,
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
api.delete('/coinpay', async (c) => {
|
|
1180
|
+
const { pool } = c.get('deps');
|
|
1181
|
+
const viewer = viewerOf(c);
|
|
1182
|
+
if (viewer === null)
|
|
1183
|
+
return fail(c, 401, 'unauthenticated', 'Sign in first.');
|
|
1184
|
+
return c.json({ ok: await disconnect(pool, viewer.id) });
|
|
1185
|
+
});
|
|
1186
|
+
/**
|
|
1187
|
+
* Where CoinPay sends the person after consent. Under /api/v1 because that
|
|
1188
|
+
* is the path registered on the OAuth client; it answers a browser, with a
|
|
1189
|
+
* redirect, not JSON.
|
|
1190
|
+
*/
|
|
1191
|
+
api.get('/coinpay/callback', async (c) => {
|
|
1192
|
+
const { pool, config, coinpay } = c.get('deps');
|
|
1193
|
+
const params = new URL(c.req.url).searchParams;
|
|
1194
|
+
const code = params.get('code') ?? '';
|
|
1195
|
+
const state = params.get('state') ?? '';
|
|
1196
|
+
if (params.get('error') !== null) {
|
|
1197
|
+
const said = params.get('error_description') ?? params.get('error') ?? 'refused';
|
|
1198
|
+
return c.redirect(`/me?coinpay=${encodeURIComponent(`CoinPay said: ${said}`)}#billing`, 303);
|
|
1199
|
+
}
|
|
1200
|
+
if (code === '' || state === '')
|
|
1201
|
+
return fail(c, 400, 'bad_request', 'Missing code or state.');
|
|
1202
|
+
if (coinpay === null)
|
|
1203
|
+
return fail(c, 404, 'not_found', 'This board has no payment rail configured.');
|
|
1204
|
+
const done = await finishConnect(pool, coinpay, {
|
|
1205
|
+
state,
|
|
1206
|
+
code,
|
|
1207
|
+
redirectUri: coinpayRedirectUri(config.publicUrl),
|
|
1208
|
+
});
|
|
1209
|
+
if (typeof done === 'string') {
|
|
1210
|
+
return c.redirect(`/me?coinpay=${encodeURIComponent(done)}#billing`, 303);
|
|
1211
|
+
}
|
|
1212
|
+
const back = done.redirect ?? '/me#billing';
|
|
1213
|
+
if (back.startsWith('/me'))
|
|
1214
|
+
return c.redirect('/me?coinpay=connected#billing', 303);
|
|
1215
|
+
return c.redirect(`${back}${back.includes('?') ? '&' : '?'}coinpay=connected`, 303);
|
|
1216
|
+
});
|
|
1217
|
+
/**
|
|
1218
|
+
* CoinPay telling us a payment settled. Verified against the business's
|
|
1219
|
+
* webhook secret; an unsigned or mis-signed body is dropped with a 401 and
|
|
1220
|
+
* the invoice waits for the next page load to ask CoinPay directly.
|
|
1221
|
+
*/
|
|
1222
|
+
api.post('/coinpay/webhook', async (c) => {
|
|
1223
|
+
const { pool, coinpay } = c.get('deps');
|
|
1224
|
+
if (coinpay === null)
|
|
1225
|
+
return fail(c, 404, 'not_found', 'This board has no payment rail configured.');
|
|
1226
|
+
const raw = await c.req.text();
|
|
1227
|
+
if (!coinpay.verifyWebhook(raw, c.req.header('x-coinpay-signature'))) {
|
|
1228
|
+
return fail(c, 401, 'bad_signature', "The signature does not match this board's webhook secret.");
|
|
1229
|
+
}
|
|
1230
|
+
let payload;
|
|
1231
|
+
try {
|
|
1232
|
+
const parsed = JSON.parse(raw);
|
|
1233
|
+
payload = typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
return fail(c, 400, 'bad_request', 'The body is not JSON.');
|
|
1237
|
+
}
|
|
1238
|
+
const outcome = await applyWebhook(pool, payload);
|
|
1239
|
+
return c.json({ received: true, outcome });
|
|
1240
|
+
});
|
|
895
1241
|
// --- this instance ----------------------------------------------------
|
|
896
1242
|
api.get('/openapi.json', (c) => {
|
|
897
1243
|
const { config } = c.get('deps');
|