shopstack 0.1.0 → 0.2.1

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.
@@ -0,0 +1,55 @@
1
+ export interface ShopstackProfile {
2
+ accountId: string;
3
+ apiKey: string;
4
+ keyType: "developer" | "user";
5
+ userId?: string;
6
+ }
7
+
8
+ export interface PendingSignup {
9
+ accountType: "developer" | "personal";
10
+ attemptId?: string;
11
+ email: string;
12
+ expiresAt?: string;
13
+ id?: string;
14
+ idempotencyKey?: string;
15
+ pollToken?: string;
16
+ profile: string;
17
+ recoveryKey?: string;
18
+ signupId?: string;
19
+ }
20
+
21
+ export class ConfigStore {
22
+ constructor(path?: string);
23
+ activeProfile(): Promise<ShopstackProfile | undefined>;
24
+ saveProfile(
25
+ name: string,
26
+ profile: ShopstackProfile,
27
+ options?: { activate?: boolean },
28
+ ): Promise<ShopstackProfile>;
29
+ useProfile(name: string): Promise<ShopstackProfile>;
30
+ savePendingSignup(signup: PendingSignup): Promise<PendingSignup>;
31
+ pendingSignup(signupId: string): Promise<PendingSignup | undefined>;
32
+ pendingSignups(): Promise<PendingSignup[]>;
33
+ findPendingSignup(input: {
34
+ accountType: "developer" | "personal";
35
+ email: string;
36
+ }): Promise<PendingSignup | undefined>;
37
+ completePendingSignup(
38
+ attemptId: string,
39
+ profileName: string,
40
+ result: {
41
+ account: { id: string };
42
+ api_key: string;
43
+ key_type: "developer" | "user";
44
+ user?: { id?: string };
45
+ },
46
+ ): Promise<ShopstackProfile>;
47
+ deletePendingSignup(signupId: string): Promise<void>;
48
+ load(): Promise<{
49
+ active: string | null;
50
+ pendingSignups: Record<string, PendingSignup>;
51
+ profiles: Record<string, ShopstackProfile>;
52
+ }>;
53
+ }
54
+
55
+ export function defaultConfigPath(): string;
package/src/config.js ADDED
@@ -0,0 +1,153 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export function defaultConfigPath() {
6
+ return (
7
+ process.env.SHOPSTACK_CONFIG_FILE ??
8
+ join(homedir(), ".config", "shopstack", "config.json")
9
+ );
10
+ }
11
+
12
+ export class ConfigStore {
13
+ constructor(path = defaultConfigPath()) {
14
+ this.path = path;
15
+ }
16
+
17
+ async load() {
18
+ try {
19
+ const parsed = JSON.parse(await readFile(this.path, "utf8"));
20
+ if (
21
+ typeof parsed !== "object" ||
22
+ parsed === null ||
23
+ typeof parsed.profiles !== "object" ||
24
+ parsed.profiles === null
25
+ ) {
26
+ throw new Error("Shopstack configuration is invalid.");
27
+ }
28
+ if (
29
+ parsed.pendingSignups !== undefined &&
30
+ (typeof parsed.pendingSignups !== "object" ||
31
+ parsed.pendingSignups === null ||
32
+ Array.isArray(parsed.pendingSignups))
33
+ ) {
34
+ throw new Error("Shopstack configuration is invalid.");
35
+ }
36
+ parsed.pendingSignups ??= {};
37
+ return parsed;
38
+ } catch (error) {
39
+ if (error?.code === "ENOENT") {
40
+ return { active: null, pendingSignups: {}, profiles: {} };
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async write(config) {
47
+ const directory = dirname(this.path);
48
+ await mkdir(directory, { mode: 0o700, recursive: true });
49
+ await chmod(directory, 0o700);
50
+ const temporary = `${this.path}.${process.pid}.tmp`;
51
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, {
52
+ mode: 0o600,
53
+ });
54
+ await rename(temporary, this.path);
55
+ await chmod(this.path, 0o600);
56
+ }
57
+
58
+ async saveProfile(name, profile, { activate = false } = {}) {
59
+ if (!/^[A-Za-z0-9._-]{1,80}$/u.test(name)) {
60
+ throw new Error("Profile name is invalid.");
61
+ }
62
+ const config = await this.load();
63
+ config.profiles[name] = { ...profile };
64
+ if (activate || config.active === null) config.active = name;
65
+ await this.write(config);
66
+ return config.profiles[name];
67
+ }
68
+
69
+ async useProfile(name) {
70
+ const config = await this.load();
71
+ if (!Object.hasOwn(config.profiles, name)) {
72
+ throw new Error(`Unknown Shopstack profile: ${name}`);
73
+ }
74
+ config.active = name;
75
+ await this.write(config);
76
+ return config.profiles[name];
77
+ }
78
+
79
+ async activeProfile() {
80
+ const config = await this.load();
81
+ const name = config.active;
82
+ return typeof name === "string" ? config.profiles[name] : undefined;
83
+ }
84
+
85
+ async savePendingSignup(signup) {
86
+ const config = await this.load();
87
+ const key = signup.attemptId ?? signup.id;
88
+ if (typeof key !== "string" || key.length === 0) {
89
+ throw new Error("Pending signup identity is invalid.");
90
+ }
91
+ config.pendingSignups[key] = { ...signup };
92
+ await this.write(config);
93
+ return config.pendingSignups[key];
94
+ }
95
+
96
+ async pendingSignup(signupId) {
97
+ const config = await this.load();
98
+ return (
99
+ config.pendingSignups[signupId] ??
100
+ Object.values(config.pendingSignups).find(
101
+ (pending) => pending.id === signupId || pending.signupId === signupId,
102
+ )
103
+ );
104
+ }
105
+
106
+ async pendingSignups() {
107
+ return Object.values((await this.load()).pendingSignups);
108
+ }
109
+
110
+ async findPendingSignup({ accountType, email }) {
111
+ const normalizedEmail = email.trim().toLowerCase();
112
+ return Object.values((await this.load()).pendingSignups).find(
113
+ (pending) =>
114
+ pending.accountType === accountType &&
115
+ pending.email.trim().toLowerCase() === normalizedEmail,
116
+ );
117
+ }
118
+
119
+ async completePendingSignup(attemptId, profileName, result) {
120
+ const config = await this.load();
121
+ const entry = Object.entries(config.pendingSignups).find(
122
+ ([key, pending]) =>
123
+ key === attemptId ||
124
+ pending.id === attemptId ||
125
+ pending.signupId === attemptId,
126
+ );
127
+ if (entry === undefined) {
128
+ throw new Error("Pending signup is unavailable.");
129
+ }
130
+ config.profiles[profileName] = {
131
+ accountId: result.account.id,
132
+ apiKey: result.api_key,
133
+ keyType: result.key_type,
134
+ ...(result.user?.id === undefined ? {} : { userId: result.user.id }),
135
+ };
136
+ config.active = profileName;
137
+ delete config.pendingSignups[entry[0]];
138
+ await this.write(config);
139
+ return config.profiles[profileName];
140
+ }
141
+
142
+ async deletePendingSignup(signupId) {
143
+ const config = await this.load();
144
+ const entry = Object.entries(config.pendingSignups).find(
145
+ ([key, pending]) =>
146
+ key === signupId ||
147
+ pending.id === signupId ||
148
+ pending.signupId === signupId,
149
+ );
150
+ if (entry !== undefined) delete config.pendingSignups[entry[0]];
151
+ await this.write(config);
152
+ }
153
+ }
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const message =
4
- "Wow you're fast! Apply for access at https://shopstack.ai";
5
-
6
- console.log(message);