-
Notifications
You must be signed in to change notification settings - Fork 327
/
Copy pathkeyless-node.ts
193 lines (158 loc) · 5.27 KB
/
keyless-node.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import type { AccountlessApplication } from '@clerk/backend';
import { createClerkClientWithOptions } from './createClerkClient';
import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './fs/utils';
/**
* The Clerk-specific directory name.
*/
const CLERK_HIDDEN = '.clerk';
/**
* The Clerk-specific lock file that is used to mitigate multiple key creation.
* This is automatically cleaned up.
*/
const CLERK_LOCK = 'clerk.lock';
/**
* The `.clerk/` directory is NOT safe to be committed as it may include sensitive information about a Clerk instance.
* It may include an instance's secret key and the secret token for claiming that instance.
*/
function updateGitignore() {
const { existsSync, writeFileSync, readFileSync, appendFileSync } = nodeFsOrThrow();
const path = nodePathOrThrow();
const cwd = nodeCwdOrThrow();
const gitignorePath = path.join(cwd(), '.gitignore');
if (!existsSync(gitignorePath)) {
writeFileSync(gitignorePath, '');
}
// Check if `.clerk/` entry exists in .gitignore
const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
const COMMENT = `# clerk configuration (can include secrets)`;
if (!gitignoreContent.includes(CLERK_HIDDEN + '/')) {
appendFileSync(gitignorePath, `\n${COMMENT}\n/${CLERK_HIDDEN}/\n`);
}
}
const generatePath = (...slugs: string[]) => {
const path = nodePathOrThrow();
const cwd = nodeCwdOrThrow();
return path.join(cwd(), CLERK_HIDDEN, ...slugs);
};
const _TEMP_DIR_NAME = '.tmp';
const getKeylessConfigurationPath = () => generatePath(_TEMP_DIR_NAME, 'keyless.json');
const getKeylessReadMePath = () => generatePath(_TEMP_DIR_NAME, 'README.md');
let isCreatingFile = false;
export function safeParseClerkFile(): AccountlessApplication | undefined {
const { readFileSync } = nodeFsOrThrow();
try {
const CONFIG_PATH = getKeylessConfigurationPath();
let fileAsString;
try {
fileAsString = readFileSync(CONFIG_PATH, { encoding: 'utf-8' }) || '{}';
} catch {
fileAsString = '{}';
}
return JSON.parse(fileAsString) as AccountlessApplication;
} catch {
return undefined;
}
}
/**
* Using both an in-memory and file system lock seems to be the most effective solution.
*/
const lockFileWriting = () => {
const { writeFileSync } = nodeFsOrThrow();
isCreatingFile = true;
writeFileSync(
CLERK_LOCK,
// In the rare case, the file persists give the developer enough context.
'This file can be deleted. Please delete this file and refresh your application',
{
encoding: 'utf8',
mode: '0777',
flag: 'w',
},
);
};
const unlockFileWriting = () => {
const { rmSync } = nodeFsOrThrow();
try {
rmSync(CLERK_LOCK, { force: true, recursive: true });
} catch {
// Simply ignore if the removal of the directory/file fails
}
isCreatingFile = false;
};
const isFileWritingLocked = () => {
const { existsSync } = nodeFsOrThrow();
return isCreatingFile || existsSync(CLERK_LOCK);
};
async function createOrReadKeyless(): Promise<AccountlessApplication | null> {
const { writeFileSync, mkdirSync } = nodeFsOrThrow();
/**
* If another request is already in the process of acquiring keys return early.
* Using both an in-memory and file system lock seems to be the most effective solution.
*/
if (isFileWritingLocked()) {
return null;
}
lockFileWriting();
const CONFIG_PATH = getKeylessConfigurationPath();
const README_PATH = getKeylessReadMePath();
mkdirSync(generatePath(_TEMP_DIR_NAME), { recursive: true });
updateGitignore();
/**
* When the configuration file exists, always read the keys from the file
*/
const envVarsMap = safeParseClerkFile();
if (envVarsMap?.publishableKey && envVarsMap?.secretKey) {
unlockFileWriting();
return envVarsMap;
}
/**
* At this step, it is safe to create new keys and store them.
*/
const client = createClerkClientWithOptions({});
const accountlessApplication = await client.__experimental_accountlessApplications
.createAccountlessApplication()
.catch(() => null);
if (accountlessApplication) {
writeFileSync(CONFIG_PATH, JSON.stringify(accountlessApplication), {
encoding: 'utf8',
mode: '0777',
flag: 'w',
});
// TODO-KEYLESS: Add link to official documentation.
const README_NOTIFICATION = `
## DO NOT COMMIT
This directory is auto-generated from \`@clerk/nextjs\` because you are running in Keyless mode. Avoid committing the \`.clerk/\` directory as it includes the secret key of the unclaimed instance.
`;
writeFileSync(README_PATH, README_NOTIFICATION, {
encoding: 'utf8',
mode: '0777',
flag: 'w',
});
}
/**
* Clean up locks.
*/
unlockFileWriting();
return accountlessApplication;
}
function removeKeyless() {
const { rmSync } = nodeFsOrThrow();
/**
* If another request is already in the process of acquiring keys return early.
* Using both an in-memory and file system lock seems to be the most effective solution.
*/
if (isFileWritingLocked()) {
return undefined;
}
lockFileWriting();
try {
rmSync(generatePath(), { force: true, recursive: true });
} catch {
// Simply ignore if the removal of the directory/file fails
}
/**
* Clean up locks.
*/
unlockFileWriting();
}
export { createOrReadKeyless, removeKeyless };