@the-focus-ai/artifacts 1.0.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.
Files changed (42) hide show
  1. package/README.md +83 -0
  2. package/dist/src/auth-clerk.d.ts +21 -0
  3. package/dist/src/auth-clerk.d.ts.map +1 -0
  4. package/dist/src/auth-clerk.js +97 -0
  5. package/dist/src/auth-clerk.js.map +1 -0
  6. package/dist/src/auth.d.ts +68 -0
  7. package/dist/src/auth.d.ts.map +1 -0
  8. package/dist/src/auth.js +141 -0
  9. package/dist/src/auth.js.map +1 -0
  10. package/dist/src/cli.d.ts +23 -0
  11. package/dist/src/cli.d.ts.map +1 -0
  12. package/dist/src/cli.js +186 -0
  13. package/dist/src/cli.js.map +1 -0
  14. package/dist/src/http.d.ts +6 -0
  15. package/dist/src/http.d.ts.map +1 -0
  16. package/dist/src/http.js +62 -0
  17. package/dist/src/http.js.map +1 -0
  18. package/dist/src/index.d.ts +7 -0
  19. package/dist/src/index.d.ts.map +1 -0
  20. package/dist/src/index.js +7 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/local-config.d.ts +44 -0
  23. package/dist/src/local-config.d.ts.map +1 -0
  24. package/dist/src/local-config.js +123 -0
  25. package/dist/src/local-config.js.map +1 -0
  26. package/dist/src/login-flow.d.ts +12 -0
  27. package/dist/src/login-flow.d.ts.map +1 -0
  28. package/dist/src/login-flow.js +124 -0
  29. package/dist/src/login-flow.js.map +1 -0
  30. package/dist/src/publication.d.ts +142 -0
  31. package/dist/src/publication.d.ts.map +1 -0
  32. package/dist/src/publication.js +703 -0
  33. package/dist/src/publication.js.map +1 -0
  34. package/dist/src/storage/artifact-content.d.ts +50 -0
  35. package/dist/src/storage/artifact-content.d.ts.map +1 -0
  36. package/dist/src/storage/artifact-content.js +117 -0
  37. package/dist/src/storage/artifact-content.js.map +1 -0
  38. package/dist/src/storage/publication-metadata.d.ts +67 -0
  39. package/dist/src/storage/publication-metadata.d.ts.map +1 -0
  40. package/dist/src/storage/publication-metadata.js +201 -0
  41. package/dist/src/storage/publication-metadata.js.map +1 -0
  42. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # artifacts.thefocus.ai
2
+
3
+ CLI-first Artifact publishing for TheFocus.AI.
4
+
5
+ The CLI is packaged as `@the-focus-ai/artifacts` and exposes the `artifacts` executable:
6
+
7
+ ```bash
8
+ npx @the-focus-ai/artifacts publish ./dist
9
+ ```
10
+
11
+ ## Development
12
+
13
+ This repo uses mise and pnpm:
14
+
15
+ ```bash
16
+ mise trust
17
+ mise install
18
+ mise run setup
19
+ mise run install
20
+ mise run lint
21
+ mise run test
22
+ ```
23
+
24
+ See [docs/development.md](docs/development.md) for the Neon/Postgres and Vercel Blob environment contract, migration setup, and local fake adapter notes.
25
+
26
+ See [docs/deploy.md](docs/deploy.md) for Vercel project, custom domain, pull-request Preview deployment, and environment configuration. See [docs/smoke-test.md](docs/smoke-test.md) for the integrated smoke flow and [docs/release.md](docs/release.md) for npm release steps.
27
+
28
+ ## Publishing Artifacts
29
+
30
+ Install/run the CLI through npm or the repo-local script:
31
+
32
+ ```bash
33
+ npx @the-focus-ai/artifacts publish ./artifact.html
34
+ pnpm artifacts publish ./artifact.html
35
+ ```
36
+
37
+ Log in once with a browser, or store a pre-issued Publisher Token non-interactively:
38
+
39
+ ```bash
40
+ pnpm artifacts login
41
+ pnpm artifacts login --token tfai_pub_...
42
+ pnpm artifacts whoami
43
+ ```
44
+
45
+ The publishing path accepts one local HTML file or one local directory and returns a canonical unlisted Publication URL:
46
+
47
+ ```bash
48
+ THEFOCUS_ARTIFACTS_TOKEN=tfai_pub_... \
49
+ fnox exec -- pnpm artifacts publish ./artifact.html
50
+ ```
51
+
52
+ Publisher Tokens are issued only to verified emails ending exactly in `@thefocus.ai`, are stored hashed server-side, and can be stored locally with `artifacts login --token <token>` or supplied non-interactively with `THEFOCUS_ARTIFACTS_TOKEN` (which overrides local config). Local CLI token state is stored under `~/.config/thefocus-artifacts/` with restricted file permissions where supported; `artifacts whoami` validates the active token, `artifacts list` shows the current Publisher's active and removed Publications, and `artifacts logout` removes local token state.
53
+
54
+ Remove a Publication with an interactive confirmation, or pass `--yes` for non-interactive scripted Removal:
55
+
56
+ ```bash
57
+ THEFOCUS_ARTIFACTS_TOKEN=tfai_pub_... \
58
+ fnox exec -- pnpm artifacts remove https://artifacts.thefocus.ai/a/Ab3xY9kQ --yes
59
+ ```
60
+
61
+ Removal marks the Publication as removed, clears matching Local Source state when present, deletes the active Artifact contents from Blob storage, and makes the Publication URL return 404.
62
+
63
+ Directory Artifacts require a root `index.html` by default and preserve nested Artifact Paths:
64
+
65
+ ```bash
66
+ THEFOCUS_ARTIFACTS_TOKEN=tfai_pub_... \
67
+ fnox exec -- pnpm artifacts publish ./dist
68
+ ```
69
+
70
+ Use `--entry-page path/to/page.html` to choose a different HTML Entry Page inside a directory Artifact. Use `--new` to force a fresh Publication during the Revision Window, or `--update <Publication URL>` to intentionally update an older active Publication URL.
71
+
72
+ Other CLI management commands:
73
+
74
+ ```bash
75
+ pnpm artifacts list
76
+ pnpm artifacts remove https://artifacts.thefocus.ai/a/Ab3xY9kQ
77
+ pnpm artifacts remove https://artifacts.thefocus.ai/a/Ab3xY9kQ --yes
78
+ pnpm artifacts logout
79
+ ```
80
+
81
+ Packaging applies built-in safety rules before upload: obvious secret, dependency, cache, and hidden paths are excluded by default, except `.well-known/`; `.gitignore` is not read in v1; symlinks are rejected; and preflight fails before upload if any file exceeds 25 MB, total Artifact size exceeds 100 MB, or the Artifact has more than 1,000 files. Exclusion output is concise by default; add `--verbose` to print excluded paths.
82
+
83
+ `ARTIFACTS_PUBLIC_BASE_URL` defaults to `https://artifacts.thefocus.ai`; set it only when publishing against a Vercel Preview or another host that serves this app. Published Artifacts are served by the Vercel rewrite from `/a/{opaque}` and nested `/a/{opaque}/{path}` URLs to the API functions with `Cache-Control: no-store` and `X-Robots-Tag: noindex, nofollow` headers.
@@ -0,0 +1,21 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export interface ClerkVerification {
3
+ email: string;
4
+ userId: string;
5
+ }
6
+ export type ClerkAuthResult = {
7
+ kind: "authenticated";
8
+ verification: ClerkVerification;
9
+ } | {
10
+ kind: "redirect";
11
+ headers: Headers;
12
+ } | {
13
+ kind: "unauthenticated";
14
+ message: string;
15
+ };
16
+ export interface ClerkVerifier {
17
+ authenticateRequest(request: IncomingMessage): Promise<ClerkAuthResult>;
18
+ }
19
+ export declare function buildClerkSignInUrl(returnUrl: string, signInDomain?: string): string;
20
+ export declare function createServerClerkVerifier(secretKey?: string, publishableKey?: string): ClerkVerifier;
21
+ //# sourceMappingURL=auth-clerk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-clerk.d.ts","sourceRoot":"","sources":["../../src/auth-clerk.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAuB,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,YAAY,EAAE,iBAAiB,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,mBAAmB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACzE;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,SACsB,GACjC,MAAM,CAIR;AAED,wBAAgB,yBAAyB,CACvC,SAAS,SAAkC,EAC3C,cAAc,SAAmD,GAChE,aAAa,CAiDf"}
@@ -0,0 +1,97 @@
1
+ import { createClerkClient } from "@clerk/backend";
2
+ export function buildClerkSignInUrl(returnUrl, signInDomain = process.env["CLERK_SIGN_IN_DOMAIN"] ??
3
+ "possible-shrew-37.accounts.dev") {
4
+ const signInUrl = new URL(`https://${signInDomain}/sign-in`);
5
+ signInUrl.searchParams.set("redirect_url", returnUrl);
6
+ return signInUrl.toString();
7
+ }
8
+ export function createServerClerkVerifier(secretKey = requiredEnv("CLERK_SECRET_KEY"), publishableKey = requiredEnv("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")) {
9
+ const clerkClient = createClerkClient({ secretKey, publishableKey });
10
+ return {
11
+ async authenticateRequest(request) {
12
+ const webRequest = toWebRequest(request);
13
+ const requestState = await clerkClient.authenticateRequest(webRequest, {
14
+ authorizedParties: authorizedPartiesFor(request),
15
+ });
16
+ if (requestState.status === "handshake") {
17
+ return { kind: "redirect", headers: requestState.headers };
18
+ }
19
+ if (!requestState.isAuthenticated) {
20
+ return {
21
+ kind: "unauthenticated",
22
+ message: requestState.message ||
23
+ requestState.reason ||
24
+ "Could not verify your Clerk session.",
25
+ };
26
+ }
27
+ const auth = requestState.toAuth();
28
+ if (!auth.userId) {
29
+ return {
30
+ kind: "unauthenticated",
31
+ message: "Clerk authenticated the request but did not return a user.",
32
+ };
33
+ }
34
+ const email = await primaryEmailForUser(clerkClient, auth.userId);
35
+ if (!email) {
36
+ return {
37
+ kind: "unauthenticated",
38
+ message: "Clerk authenticated the request but no email was available.",
39
+ };
40
+ }
41
+ return {
42
+ kind: "authenticated",
43
+ verification: { email, userId: auth.userId },
44
+ };
45
+ },
46
+ };
47
+ }
48
+ async function primaryEmailForUser(clerkClient, userId) {
49
+ const user = await clerkClient.users.getUser(userId);
50
+ const primaryEmail = user.emailAddresses.find((email) => email.id === user.primaryEmailAddressId) ?? user.emailAddresses[0];
51
+ return primaryEmail?.emailAddress ?? null;
52
+ }
53
+ function toWebRequest(request) {
54
+ return new Request(requestToUrl(request), {
55
+ method: request.method ?? "GET",
56
+ headers: toWebHeaders(request.headers),
57
+ });
58
+ }
59
+ function toWebHeaders(headers) {
60
+ const webHeaders = new Headers();
61
+ for (const [name, value] of Object.entries(headers)) {
62
+ if (value === undefined)
63
+ continue;
64
+ if (Array.isArray(value)) {
65
+ for (const item of value)
66
+ webHeaders.append(name, item);
67
+ continue;
68
+ }
69
+ webHeaders.set(name, value);
70
+ }
71
+ return webHeaders;
72
+ }
73
+ function authorizedPartiesFor(request) {
74
+ const configured = process.env["CLERK_AUTHORIZED_PARTIES"];
75
+ if (configured) {
76
+ return configured
77
+ .split(",")
78
+ .map((origin) => origin.trim())
79
+ .filter(Boolean);
80
+ }
81
+ return [requestToUrl(request).origin];
82
+ }
83
+ function requestToUrl(request) {
84
+ const host = firstHeader(request.headers.host) ?? "localhost";
85
+ const protocol = firstHeader(request.headers["x-forwarded-proto"]) ?? "https";
86
+ return new URL(request.url ?? "/", `${protocol}://${host}`);
87
+ }
88
+ function firstHeader(value) {
89
+ return Array.isArray(value) ? value[0] : value;
90
+ }
91
+ function requiredEnv(name) {
92
+ const value = process.env[name];
93
+ if (!value)
94
+ throw new Error(`${name} is required`);
95
+ return value;
96
+ }
97
+ //# sourceMappingURL=auth-clerk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-clerk.js","sourceRoot":"","sources":["../../src/auth-clerk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { ClockOptions, SqlClient } from "./storage/publication-metadata.js";
2
+ export interface PublisherTokenRecord {
3
+ tokenHash: string;
4
+ publisherEmail: string;
5
+ createdAt: Date;
6
+ lastUsedAt: Date | null;
7
+ }
8
+ export interface PublisherTokenStore {
9
+ create(input: {
10
+ tokenHash: string;
11
+ publisherEmail: string;
12
+ createdAt: Date;
13
+ }): Promise<PublisherTokenRecord>;
14
+ getByTokenHash(tokenHash: string): Promise<PublisherTokenRecord | null>;
15
+ markUsed(tokenHash: string, usedAt: Date): Promise<void>;
16
+ }
17
+ export interface IssuePublisherTokenResult {
18
+ token: string;
19
+ tokenHash: string;
20
+ publisherEmail: string;
21
+ }
22
+ export declare class InvalidPublisherEmailError extends Error {
23
+ constructor(email: string);
24
+ }
25
+ export declare class InvalidPublisherTokenError extends Error {
26
+ constructor();
27
+ }
28
+ export declare class InMemoryPublisherTokenStore implements PublisherTokenStore {
29
+ private readonly rows;
30
+ private readonly now;
31
+ constructor(options?: ClockOptions);
32
+ create(input: {
33
+ tokenHash: string;
34
+ publisherEmail: string;
35
+ createdAt?: Date;
36
+ }): Promise<PublisherTokenRecord>;
37
+ getByTokenHash(tokenHash: string): Promise<PublisherTokenRecord | null>;
38
+ markUsed(tokenHash: string, usedAt: Date): Promise<void>;
39
+ }
40
+ export declare class PostgresPublisherTokenStore implements PublisherTokenStore {
41
+ private readonly sql;
42
+ private readonly now;
43
+ constructor(sql: SqlClient, options?: ClockOptions);
44
+ create(input: {
45
+ tokenHash: string;
46
+ publisherEmail: string;
47
+ createdAt?: Date;
48
+ }): Promise<PublisherTokenRecord>;
49
+ getByTokenHash(tokenHash: string): Promise<PublisherTokenRecord | null>;
50
+ markUsed(tokenHash: string, usedAt: Date): Promise<void>;
51
+ }
52
+ export declare function issuePublisherToken(input: {
53
+ email: string;
54
+ store: PublisherTokenStore;
55
+ now?: () => Date;
56
+ tokenFactory?: () => string;
57
+ }): Promise<IssuePublisherTokenResult>;
58
+ export declare function authenticatePublisherToken(input: {
59
+ token: string | null | undefined;
60
+ store: PublisherTokenStore;
61
+ now?: () => Date;
62
+ }): Promise<string>;
63
+ export declare function normalizePublisherEmail(email: string): string;
64
+ export declare function isTheFocusPublisherEmail(email: string): boolean;
65
+ export declare function createPublisherToken(): string;
66
+ export declare function hashPublisherToken(token: string): string;
67
+ export declare function createNeonPublisherTokenStore(databaseUrl?: string): PostgresPublisherTokenStore;
68
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { neon } from "@neondatabase/serverless";
3
+ export class InvalidPublisherEmailError extends Error {
4
+ constructor(email) {
5
+ super(`Publishing is limited to verified email addresses ending exactly in @thefocus.ai: ${email}`);
6
+ this.name = "InvalidPublisherEmailError";
7
+ }
8
+ }
9
+ export class InvalidPublisherTokenError extends Error {
10
+ constructor() {
11
+ super("A valid Publisher Token is required");
12
+ this.name = "InvalidPublisherTokenError";
13
+ }
14
+ }
15
+ export class InMemoryPublisherTokenStore {
16
+ rows = new Map();
17
+ now;
18
+ constructor(options = {}) {
19
+ this.now = options.now ?? (() => new Date());
20
+ }
21
+ async create(input) {
22
+ const row = {
23
+ tokenHash: input.tokenHash,
24
+ publisherEmail: input.publisherEmail,
25
+ createdAt: input.createdAt ?? this.now(),
26
+ lastUsedAt: null,
27
+ };
28
+ this.rows.set(row.tokenHash, row);
29
+ return clonePublisherTokenRecord(row);
30
+ }
31
+ async getByTokenHash(tokenHash) {
32
+ const row = this.rows.get(tokenHash);
33
+ return row ? clonePublisherTokenRecord(row) : null;
34
+ }
35
+ async markUsed(tokenHash, usedAt) {
36
+ const row = this.rows.get(tokenHash);
37
+ if (!row)
38
+ return;
39
+ this.rows.set(tokenHash, { ...row, lastUsedAt: usedAt });
40
+ }
41
+ }
42
+ export class PostgresPublisherTokenStore {
43
+ sql;
44
+ now;
45
+ constructor(sql, options = {}) {
46
+ this.sql = sql;
47
+ this.now = options.now ?? (() => new Date());
48
+ }
49
+ async create(input) {
50
+ const createdAt = input.createdAt ?? this.now();
51
+ const result = await this.sql.query(`
52
+ insert into publisher_tokens (token_hash, publisher_email, created_at)
53
+ values ($1, $2, $3)
54
+ returning *
55
+ `, [input.tokenHash, input.publisherEmail, createdAt]);
56
+ return mapPublisherTokenRow(result.rows[0]);
57
+ }
58
+ async getByTokenHash(tokenHash) {
59
+ const result = await this.sql.query("select * from publisher_tokens where token_hash = $1", [tokenHash]);
60
+ return result.rows[0] ? mapPublisherTokenRow(result.rows[0]) : null;
61
+ }
62
+ async markUsed(tokenHash, usedAt) {
63
+ await this.sql.query("update publisher_tokens set last_used_at = $2 where token_hash = $1", [tokenHash, usedAt]);
64
+ }
65
+ }
66
+ export async function issuePublisherToken(input) {
67
+ const publisherEmail = normalizePublisherEmail(input.email);
68
+ const token = input.tokenFactory?.() ?? createPublisherToken();
69
+ const tokenHash = hashPublisherToken(token);
70
+ await input.store.create({
71
+ tokenHash,
72
+ publisherEmail,
73
+ createdAt: input.now?.() ?? new Date(),
74
+ });
75
+ return { token, tokenHash, publisherEmail };
76
+ }
77
+ export async function authenticatePublisherToken(input) {
78
+ if (!input.token)
79
+ throw new InvalidPublisherTokenError();
80
+ const tokenHash = hashPublisherToken(input.token);
81
+ const record = await input.store.getByTokenHash(tokenHash);
82
+ if (!record || !constantTimeEqual(record.tokenHash, tokenHash)) {
83
+ throw new InvalidPublisherTokenError();
84
+ }
85
+ await input.store.markUsed(tokenHash, input.now?.() ?? new Date());
86
+ return record.publisherEmail;
87
+ }
88
+ export function normalizePublisherEmail(email) {
89
+ const normalized = email.trim().toLowerCase();
90
+ if (!isTheFocusPublisherEmail(normalized)) {
91
+ throw new InvalidPublisherEmailError(email);
92
+ }
93
+ return normalized;
94
+ }
95
+ export function isTheFocusPublisherEmail(email) {
96
+ return /^[^@\s]+@thefocus\.ai$/.test(email.trim().toLowerCase());
97
+ }
98
+ export function createPublisherToken() {
99
+ return `tfai_pub_${randomBytes(32).toString("base64url")}`;
100
+ }
101
+ export function hashPublisherToken(token) {
102
+ return createHash("sha256").update(token).digest("hex");
103
+ }
104
+ export function createNeonPublisherTokenStore(databaseUrl = requiredEnv("DATABASE_URL")) {
105
+ const sql = neon(databaseUrl);
106
+ return new PostgresPublisherTokenStore({
107
+ async query(text, params = []) {
108
+ const rows = (await sql.query(text, params));
109
+ return { rows };
110
+ },
111
+ });
112
+ }
113
+ function mapPublisherTokenRow(row) {
114
+ if (!row)
115
+ throw new Error("Expected Publisher Token row");
116
+ return {
117
+ tokenHash: row.token_hash,
118
+ publisherEmail: row.publisher_email,
119
+ createdAt: new Date(row.created_at),
120
+ lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : null,
121
+ };
122
+ }
123
+ function clonePublisherTokenRecord(row) {
124
+ return {
125
+ ...row,
126
+ createdAt: new Date(row.createdAt),
127
+ lastUsedAt: row.lastUsedAt ? new Date(row.lastUsedAt) : null,
128
+ };
129
+ }
130
+ function constantTimeEqual(a, b) {
131
+ const left = Buffer.from(a);
132
+ const right = Buffer.from(b);
133
+ return left.length === right.length && timingSafeEqual(left, right);
134
+ }
135
+ function requiredEnv(name) {
136
+ const value = process.env[name];
137
+ if (!value)
138
+ throw new Error(`${name} is required`);
139
+ return value;
140
+ }
141
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AA8BhD,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IACnD,YAAY,KAAa;QACvB,KAAK,CACH,qFAAqF,KAAK,EAAE,CAC7F,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF;AAED,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IACnD;QACE,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAC7C,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF;AAED,MAAM,OAAO,2BAA2B;IACrB,IAAI,GAAG,IAAI,GAAG,EAAgC,CAAC;IAC/C,GAAG,CAAa;IAEjC,YAAY,UAAwB,EAAE;QACpC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAIZ;QACC,MAAM,GAAG,GAAyB;YAChC,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE;YACxC,UAAU,EAAE,IAAI;SACjB,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAClC,OAAO,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,SAAiB;QAEjB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,OAAO,GAAG,CAAC,CAAC,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,MAAY;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,GAAG,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3D,CAAC;CACF;AAED,MAAM,OAAO,2BAA2B;IAInB;IAHF,GAAG,CAAa;IAEjC,YACmB,GAAc,EAC/B,UAAwB,EAAE;QADT,QAAG,GAAH,GAAG,CAAW;QAG/B,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAIZ;QACC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CACjC;;;;OAIC,EACD,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,SAAS,CAAC,CACnD,CAAC;QACF,OAAO,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,SAAiB;QAEjB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CACjC,sDAAsD,EACtD,CAAC,SAAS,CAAC,CACZ,CAAC;QACF,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,MAAY;QAC5C,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAClB,qEAAqE,EACrE,CAAC,SAAS,EAAE,MAAM,CAAC,CACpB,CAAC;IACJ,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAKzC;IACC,MAAM,cAAc,GAAG,uBAAuB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,EAAE,EAAE,IAAI,oBAAoB,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC;QACvB,SAAS;QACT,cAAc;QACd,SAAS,EAAE,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE;KACvC,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,KAIhD;IACC,IAAI,CAAC,KAAK,CAAC,KAAK;QAAE,MAAM,IAAI,0BAA0B,EAAE,CAAC;IACzD,MAAM,SAAS,GAAG,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IAC3D,IAAI,CAAC,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,0BAA0B,EAAE,CAAC;IACzC,CAAC;IACD,MAAM,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;IACnE,OAAO,MAAM,CAAC,cAAc,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAI,CAAC,wBAAwB,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,0BAA0B,CAAC,KAAK,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAAa;IACpD,OAAO,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,YAAY,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,6BAA6B,CAC3C,WAAW,GAAG,WAAW,CAAC,cAAc,CAAC;IAEzC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IAC9B,OAAO,IAAI,2BAA2B,CAAC;QACrC,KAAK,CAAC,KAAK,CACT,IAAY,EACZ,SAAoB,EAAE;YAEtB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAQ,CAAC;YACpD,OAAO,EAAE,IAAI,EAAE,CAAC;QAClB,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AASD,SAAS,oBAAoB,CAAC,GAAkC;IAC9D,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC1D,OAAO;QACL,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,cAAc,EAAE,GAAG,CAAC,eAAe;QACnC,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;QACnC,UAAU,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI;KACjE,CAAC;AACJ,CAAC;AAED,SAAS,yBAAyB,CAChC,GAAyB;IAEzB,OAAO;QACL,GAAG,GAAG;QACN,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;QAClC,UAAU,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI;KAC7D,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAS,EAAE,CAAS;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,OAAO,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAC;IACnD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { type PublisherTokenStore } from "./auth.js";
3
+ import type { BrowserLoginResult } from "./login-flow.js";
4
+ import { type RemovePublicationResult } from "./publication.js";
5
+ import { type PublicationMetadataStore } from "./storage/publication-metadata.js";
6
+ export interface CliDependencies {
7
+ env?: NodeJS.ProcessEnv;
8
+ stdout?: Pick<NodeJS.WriteStream, "write">;
9
+ stderr?: Pick<NodeJS.WriteStream, "write">;
10
+ tokenStore?: PublisherTokenStore;
11
+ metadataStore?: PublicationMetadataStore;
12
+ configDir?: string;
13
+ openBrowser?: (url: string) => Promise<void>;
14
+ stdin?: NodeJS.ReadStream;
15
+ removePublication?: (publicationUrl: string) => Promise<RemovePublicationResult>;
16
+ confirmRemoval?: (publicationUrl: string) => Promise<boolean>;
17
+ loginFlow?: (deps: {
18
+ baseUrl: string;
19
+ openBrowser?: (url: string) => Promise<void>;
20
+ }) => Promise<BrowserLoginResult>;
21
+ }
22
+ export declare function runCli(argv: string[], dependencies?: CliDependencies): Promise<number>;
23
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,WAAW,CAAC;AAOnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,OAAO,EAML,KAAK,uBAAuB,EAC7B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,mCAAmC,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,iBAAiB,CAAC,EAAE,CAClB,cAAc,EAAE,MAAM,KACnB,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACtC,cAAc,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC9C,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACnC;AAED,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,MAAM,CAAC,CAuIjB"}
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { authenticatePublisherToken, createNeonPublisherTokenStore, } from "./auth.js";
5
+ import { clearLocalPublisherConfig, defaultConfigDir, resolvePublisherToken, writeLocalPublisherConfig, } from "./local-config.js";
6
+ import { loginWithBrowserFlow } from "./login-flow.js";
7
+ import { absolutePublicationUrl, publishArtifactFromEnvironment, publishArtifactSummary, removePublicationFromEnvironment, removePublicationSummary, } from "./publication.js";
8
+ import { createNeonPublicationMetadataStore, } from "./storage/publication-metadata.js";
9
+ export async function runCli(argv, dependencies = {}) {
10
+ const [command, firstArg, ...flags] = argv;
11
+ const options = parseFlags(command === "publish" ? flags : argv.slice(1));
12
+ const env = dependencies.env ?? process.env;
13
+ const stdout = dependencies.stdout ?? process.stdout;
14
+ const stderr = dependencies.stderr ?? process.stderr;
15
+ const configDir = dependencies.configDir ?? defaultConfigDir(env);
16
+ try {
17
+ if (!command ||
18
+ command === "help" ||
19
+ command === "--help" ||
20
+ command === "-h") {
21
+ printUsage(stdout);
22
+ return 0;
23
+ }
24
+ if (command === "publish" && firstArg) {
25
+ const result = await publishArtifactFromEnvironment(firstArg, {
26
+ publicBaseUrl: options["base-url"],
27
+ entryPage: options["entry-page"],
28
+ env,
29
+ configDir,
30
+ forceNew: Object.hasOwn(options, "new"),
31
+ updatePublicationUrl: options.update,
32
+ });
33
+ stdout.write(`${publishArtifactSummary(result, { verbose: options.verbose === "true" })}\n`);
34
+ return 0;
35
+ }
36
+ if (command === "remove" && firstArg) {
37
+ if (!Object.hasOwn(options, "yes")) {
38
+ const confirm = dependencies.confirmRemoval ??
39
+ ((publicationUrl) => confirmRemoval(publicationUrl, dependencies.stdin ?? process.stdin, stdout));
40
+ const confirmed = await confirm(firstArg);
41
+ if (!confirmed) {
42
+ stdout.write("Removal cancelled.\n");
43
+ return 1;
44
+ }
45
+ }
46
+ const remove = dependencies.removePublication ??
47
+ ((publicationUrl) => removePublicationFromEnvironment(publicationUrl, {
48
+ env,
49
+ configDir,
50
+ }));
51
+ const result = await remove(firstArg);
52
+ stdout.write(`${removePublicationSummary(result)}\n`);
53
+ return 0;
54
+ }
55
+ if (command === "login") {
56
+ if (options.token) {
57
+ await writeLocalPublisherConfig({ token: options.token }, configDir);
58
+ stdout.write(`Publisher Token stored in ${configDir}\n`);
59
+ return 0;
60
+ }
61
+ const baseUrl = options["base-url"] ?? "https://artifacts.thefocus.ai";
62
+ const loginUrl = new URL("/login", baseUrl);
63
+ stdout.write("Opening browser to complete login...\n");
64
+ stdout.write(`If the browser does not open, visit: ${loginUrl.toString()}\n`);
65
+ try {
66
+ const flow = dependencies.loginFlow ?? loginWithBrowserFlow;
67
+ const result = await flow({
68
+ baseUrl,
69
+ openBrowser: dependencies.openBrowser,
70
+ });
71
+ await writeLocalPublisherConfig({ token: result.token }, configDir);
72
+ stdout.write(`Publisher Token stored in ${configDir}\n`);
73
+ return 0;
74
+ }
75
+ catch (error) {
76
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
77
+ return 1;
78
+ }
79
+ }
80
+ if (command === "logout") {
81
+ await clearLocalPublisherConfig(configDir);
82
+ stdout.write("Removed local Publisher Token state.\n");
83
+ return 0;
84
+ }
85
+ if (command === "whoami") {
86
+ const token = await resolvePublisherToken({ env, configDir });
87
+ if (!token.token)
88
+ throw new Error("Not logged in");
89
+ const email = await authenticatePublisherToken({
90
+ token: token.token,
91
+ store: dependencies.tokenStore ?? createNeonPublisherTokenStore(),
92
+ });
93
+ stdout.write(`${email}\n`);
94
+ return 0;
95
+ }
96
+ if (command === "list") {
97
+ const token = await resolvePublisherToken({ env, configDir });
98
+ if (!token.token)
99
+ throw new Error("A valid Publisher Token is required");
100
+ const publisherEmail = await authenticatePublisherToken({
101
+ token: token.token,
102
+ store: dependencies.tokenStore ?? createNeonPublisherTokenStore(),
103
+ });
104
+ const publications = await (dependencies.metadataStore ?? createNeonPublicationMetadataStore()).listByPublisherEmail(publisherEmail);
105
+ stdout.write(`${formatPublicationList(publications, {
106
+ publicBaseUrl: options["base-url"] ??
107
+ env.ARTIFACTS_PUBLIC_BASE_URL ??
108
+ "https://artifacts.thefocus.ai",
109
+ })}\n`);
110
+ return 0;
111
+ }
112
+ printUsage(stderr);
113
+ return 1;
114
+ }
115
+ catch (error) {
116
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
117
+ return 1;
118
+ }
119
+ }
120
+ function parseFlags(flags) {
121
+ const parsed = {};
122
+ for (let index = 0; index < flags.length; index += 1) {
123
+ const flag = flags[index];
124
+ if (!flag?.startsWith("--"))
125
+ continue;
126
+ const [name, inlineValue] = flag.slice(2).split("=", 2);
127
+ if (inlineValue !== undefined) {
128
+ parsed[name] = inlineValue;
129
+ continue;
130
+ }
131
+ const next = flags[index + 1];
132
+ if (!next || next.startsWith("--")) {
133
+ parsed[name] = "true";
134
+ continue;
135
+ }
136
+ parsed[name] = next;
137
+ index += 1;
138
+ }
139
+ return parsed;
140
+ }
141
+ function formatPublicationList(publications, options) {
142
+ const lines = ["LAST UPDATED STATUS PUBLICATION URL"];
143
+ if (publications.length === 0) {
144
+ lines.push("No Publications found.");
145
+ return lines.join("\n");
146
+ }
147
+ for (const publication of publications) {
148
+ lines.push(`${publication.updatedAt.toISOString()} ${publication.status.padEnd(8)} ${absolutePublicationUrl(options.publicBaseUrl, publication.publicationUrlPath)}`);
149
+ }
150
+ return lines.join("\n");
151
+ }
152
+ async function confirmRemoval(publicationUrl, stdin, stdout) {
153
+ if (!stdin.isTTY) {
154
+ throw new Error("Removal requires --yes in non-interactive terminals.");
155
+ }
156
+ const readline = createInterface({
157
+ input: stdin,
158
+ output: stdout,
159
+ });
160
+ try {
161
+ const answer = await readline.question(`Remove ${publicationUrl}? Removal cannot be undone. Type yes to continue: `);
162
+ return answer.trim().toLowerCase() === "yes";
163
+ }
164
+ finally {
165
+ readline.close();
166
+ stdout.write("");
167
+ }
168
+ }
169
+ function printUsage(output) {
170
+ output.write("Usage: artifacts <login|logout|whoami|publish|remove|list>\n" +
171
+ " artifacts login [--base-url https://artifacts.thefocus.ai]\n" +
172
+ " artifacts publish <file.html|directory> [--entry-page index.html] [--base-url https://artifacts.thefocus.ai] [--new] [--update <Publication URL>] [--verbose]\n" +
173
+ " artifacts remove <Publication URL> [--yes]\n" +
174
+ " artifacts list [--base-url https://artifacts.thefocus.ai]\n" +
175
+ " artifacts whoami\n" +
176
+ " artifacts logout\n");
177
+ }
178
+ const isDirectRun = process.argv[1]
179
+ ? fileURLToPath(import.meta.url) === process.argv[1]
180
+ : false;
181
+ if (isDirectRun) {
182
+ runCli(process.argv.slice(2)).then((exitCode) => {
183
+ process.exitCode = exitCode;
184
+ });
185
+ }
186
+ //# sourceMappingURL=cli.js.map