@hot-updater/plugin-api-keys 1.0.0-rc.20

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/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Sungyu Kang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ADDITIONAL DISCLAIMER
26
+
27
+ The authors of this software shall not be held responsible for any legal issues,
28
+ regulatory violations, policy violations, or disputes arising from the use,
29
+ distribution, or integration of this software.
30
+
31
+ Users are solely responsible for ensuring compliance with all applicable laws,
32
+ regulations, and third-party policies (including but not limited to app store
33
+ policies).
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # @hot-updater/plugin-api-keys
2
+
3
+ Hot Updater's API keys plugin. It protects the server's client routes with API
4
+ keys, stores their SHA-256 digests, and adds `hot-updater api-key` commands to
5
+ the CLI.
6
+
7
+ You don't install it on its own. `@hot-updater/server` depends on it and
8
+ re-exports its `./server` entry:
9
+
10
+ ```ts
11
+ import { apiKeys } from "@hot-updater/server/plugins/api-keys";
12
+ ```
13
+
14
+ See [API key authentication](https://hot-updater.dev/docs/custom/api-key-authentication).
@@ -0,0 +1,11 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_apiKeys = require("./server/apiKeys.cjs");
3
+ const require_cli = require("./server/cli.cjs");
4
+ exports.apiKeysCli = require_cli.apiKeysCli;
5
+ exports.authenticateApiKey = require_apiKeys.authenticateApiKey;
6
+ exports.createApiKey = require_apiKeys.createApiKey;
7
+ exports.createApiKeyManagement = require_apiKeys.createApiKeyManagement;
8
+ exports.hashApiKey = require_apiKeys.hashApiKey;
9
+ exports.normalizeApiKeyHeaderName = require_apiKeys.normalizeApiKeyHeaderName;
10
+ exports.provisionApiKey = require_apiKeys.provisionApiKey;
11
+ exports.registerApiKey = require_apiKeys.registerApiKey;
@@ -0,0 +1,3 @@
1
+ import { authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey } from "./server/apiKeys.cjs";
2
+ import { ApiKeysCliApi, apiKeysCli } from "./server/cli.cjs";
3
+ export { type ApiKeysCliApi, apiKeysCli, authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey };
@@ -0,0 +1,3 @@
1
+ import { authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey } from "./server/apiKeys.mjs";
2
+ import { ApiKeysCliApi, apiKeysCli } from "./server/cli.mjs";
3
+ export { type ApiKeysCliApi, apiKeysCli, authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey };
@@ -0,0 +1,3 @@
1
+ import { authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey } from "./server/apiKeys.mjs";
2
+ import { apiKeysCli } from "./server/cli.mjs";
3
+ export { apiKeysCli, authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey };
@@ -0,0 +1,139 @@
1
+ //#region src/server/apiKeys.ts
2
+ const API_KEY_HEADER_NAME = "x-api-key";
3
+ const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
4
+ const bytesToBase64Url = (bytes) => {
5
+ let binary = "";
6
+ for (const byte of bytes) binary += String.fromCharCode(byte);
7
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
8
+ };
9
+ const isApiKey = (value) => {
10
+ if (!BASE64URL_32_PATTERN.test(value)) return false;
11
+ try {
12
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
13
+ const binary = atob(`${base64}=`);
14
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
15
+ return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
16
+ } catch {
17
+ return false;
18
+ }
19
+ };
20
+ const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
21
+ if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
22
+ try {
23
+ const headers = new Headers();
24
+ headers.set(value, "");
25
+ return headers.keys().next().value;
26
+ } catch {
27
+ throw new TypeError("apiKeys({ headerName }) must name a valid header.");
28
+ }
29
+ };
30
+ const hashApiKey = async (apiKey) => {
31
+ if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
32
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
33
+ return bytesToBase64Url(new Uint8Array(digest));
34
+ };
35
+ const apiKeyId = () => {
36
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
37
+ crypto.getRandomValues(bytes);
38
+ return `api-${bytesToBase64Url(bytes)}`;
39
+ };
40
+ const normalizeApiKeyName = (name) => {
41
+ const normalized = name.trim();
42
+ if (normalized.length === 0 || normalized.length > 64 || /[\p{Cc}\p{Cf}]/u.test(normalized)) throw new TypeError("API key names must contain 1-64 visible characters.");
43
+ return normalized;
44
+ };
45
+ const toApiKeyMetadata = ({ hash: _hash, ...metadata }) => Object.freeze(metadata);
46
+ const registerApiKey = async (input) => {
47
+ const hash = await hashApiKey(input.apiKey);
48
+ const createdAtMs = input.createdAtMs ?? Date.now();
49
+ if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) throw new TypeError("API key creation time must be non-negative.");
50
+ const record = Object.freeze({
51
+ created_at_ms: createdAtMs,
52
+ hash,
53
+ id: apiKeyId(),
54
+ name: normalizeApiKeyName(input.name),
55
+ prefix: input.apiKey.slice(0, 6),
56
+ revoked_at_ms: null,
57
+ role: "client"
58
+ });
59
+ if (await input.apiKeys.create(record) === "existing") {
60
+ const existing = await input.apiKeys.findByHash(hash);
61
+ if (existing === null || existing.revoked_at_ms !== null) throw new Error("The API key has been revoked.");
62
+ return Object.freeze({
63
+ apiKey: input.apiKey,
64
+ record: toApiKeyMetadata(existing)
65
+ });
66
+ }
67
+ return Object.freeze({
68
+ apiKey: input.apiKey,
69
+ record: toApiKeyMetadata(record)
70
+ });
71
+ };
72
+ const createApiKey = (input) => {
73
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
74
+ crypto.getRandomValues(bytes);
75
+ return registerApiKey({
76
+ apiKey: bytesToBase64Url(bytes),
77
+ apiKeys: input.apiKeys,
78
+ name: input.name
79
+ });
80
+ };
81
+ /**
82
+ * Provisions the client API key used by managed init.
83
+ *
84
+ * A saved plaintext key is registered idempotently; otherwise a new key is
85
+ * created. Callers must persist or display the returned plaintext because the
86
+ * database stores only its hash.
87
+ */
88
+ const provisionApiKey = (input) => {
89
+ const existingApiKey = input.existingApiKey?.trim();
90
+ return existingApiKey ? registerApiKey({
91
+ apiKey: existingApiKey,
92
+ apiKeys: input.apiKeys,
93
+ name: input.name
94
+ }) : createApiKey({
95
+ apiKeys: input.apiKeys,
96
+ name: input.name
97
+ });
98
+ };
99
+ const authenticateApiKey = async (input) => {
100
+ const apiKey = input.request.headers.get(normalizeApiKeyHeaderName(input.headerName));
101
+ if (apiKey === null || !isApiKey(apiKey)) return false;
102
+ const hash = await hashApiKey(apiKey);
103
+ const record = await input.apiKeys.findByHash(hash);
104
+ return record !== null && record.hash === hash && record.role === "client" && record.revoked_at_ms === null;
105
+ };
106
+ const createApiKeyManagement = (input) => Object.freeze({
107
+ async create({ name }) {
108
+ const created = await createApiKey({
109
+ apiKeys: input.apiKeys,
110
+ name
111
+ });
112
+ return Object.freeze({
113
+ apiKey: created.apiKey,
114
+ record: created.record
115
+ });
116
+ },
117
+ async list() {
118
+ return Object.freeze((await input.apiKeys.list()).map(toApiKeyMetadata));
119
+ },
120
+ async revoke({ id }) {
121
+ const record = await input.apiKeys.revoke({
122
+ id,
123
+ revokedAtMs: Date.now()
124
+ });
125
+ return record === null ? null : toApiKeyMetadata(record);
126
+ }
127
+ });
128
+ //#endregion
129
+ exports.API_KEY_HEADER_NAME = API_KEY_HEADER_NAME;
130
+ exports.apiKeyId = apiKeyId;
131
+ exports.authenticateApiKey = authenticateApiKey;
132
+ exports.createApiKey = createApiKey;
133
+ exports.createApiKeyManagement = createApiKeyManagement;
134
+ exports.hashApiKey = hashApiKey;
135
+ exports.isApiKey = isApiKey;
136
+ exports.normalizeApiKeyHeaderName = normalizeApiKeyHeaderName;
137
+ exports.normalizeApiKeyName = normalizeApiKeyName;
138
+ exports.provisionApiKey = provisionApiKey;
139
+ exports.registerApiKey = registerApiKey;
@@ -0,0 +1,59 @@
1
+ import { ApiKeyModel, ApiKeyRow } from "./model.cjs";
2
+ //#region src/server/apiKeys.d.ts
3
+ declare const API_KEY_HEADER_NAME = "x-api-key";
4
+ declare const normalizeApiKeyHeaderName: (value?: unknown) => string;
5
+ declare const hashApiKey: (apiKey: string) => Promise<string>;
6
+ type ApiKeyMetadata = Omit<ApiKeyRow, "hash">;
7
+ interface CreatedApiKey {
8
+ /** Plaintext credential. It cannot be recovered after this call returns. */
9
+ readonly apiKey: string;
10
+ /** Non-secret metadata. The persisted SHA-256 hash is never exposed. */
11
+ readonly record: ApiKeyMetadata;
12
+ }
13
+ interface ApiKeyManagementAPI {
14
+ /** Creates an API key and returns its plaintext exactly once. */
15
+ create(input: {
16
+ readonly name: string;
17
+ }): Promise<{
18
+ readonly apiKey: string;
19
+ readonly record: ApiKeyMetadata;
20
+ }>;
21
+ /** Lists non-secret API key metadata, including revoked keys. */
22
+ list(): Promise<readonly ApiKeyMetadata[]>;
23
+ /** Revokes an API key immediately and returns only non-secret metadata. */
24
+ revoke(input: {
25
+ readonly id: string;
26
+ }): Promise<ApiKeyMetadata | null>;
27
+ }
28
+ declare const registerApiKey: (input: {
29
+ readonly apiKey: string;
30
+ readonly apiKeys: ApiKeyModel;
31
+ readonly createdAtMs?: number;
32
+ readonly name: string;
33
+ }) => Promise<CreatedApiKey>;
34
+ declare const createApiKey: (input: {
35
+ readonly apiKeys: ApiKeyModel;
36
+ readonly name: string;
37
+ }) => Promise<CreatedApiKey>;
38
+ /**
39
+ * Provisions the client API key used by managed init.
40
+ *
41
+ * A saved plaintext key is registered idempotently; otherwise a new key is
42
+ * created. Callers must persist or display the returned plaintext because the
43
+ * database stores only its hash.
44
+ */
45
+ declare const provisionApiKey: (input: {
46
+ readonly apiKeys: ApiKeyModel;
47
+ readonly existingApiKey?: string;
48
+ readonly name: string;
49
+ }) => Promise<CreatedApiKey>;
50
+ declare const authenticateApiKey: (input: {
51
+ readonly apiKeys: ApiKeyModel;
52
+ readonly headerName?: string;
53
+ readonly request: Request;
54
+ }) => Promise<boolean>;
55
+ declare const createApiKeyManagement: (input: {
56
+ readonly apiKeys: ApiKeyModel;
57
+ }) => ApiKeyManagementAPI;
58
+ //#endregion
59
+ export { API_KEY_HEADER_NAME, ApiKeyManagementAPI, ApiKeyMetadata, CreatedApiKey, authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey };
@@ -0,0 +1,59 @@
1
+ import { ApiKeyModel, ApiKeyRow } from "./model.mjs";
2
+ //#region src/server/apiKeys.d.ts
3
+ declare const API_KEY_HEADER_NAME = "x-api-key";
4
+ declare const normalizeApiKeyHeaderName: (value?: unknown) => string;
5
+ declare const hashApiKey: (apiKey: string) => Promise<string>;
6
+ type ApiKeyMetadata = Omit<ApiKeyRow, "hash">;
7
+ interface CreatedApiKey {
8
+ /** Plaintext credential. It cannot be recovered after this call returns. */
9
+ readonly apiKey: string;
10
+ /** Non-secret metadata. The persisted SHA-256 hash is never exposed. */
11
+ readonly record: ApiKeyMetadata;
12
+ }
13
+ interface ApiKeyManagementAPI {
14
+ /** Creates an API key and returns its plaintext exactly once. */
15
+ create(input: {
16
+ readonly name: string;
17
+ }): Promise<{
18
+ readonly apiKey: string;
19
+ readonly record: ApiKeyMetadata;
20
+ }>;
21
+ /** Lists non-secret API key metadata, including revoked keys. */
22
+ list(): Promise<readonly ApiKeyMetadata[]>;
23
+ /** Revokes an API key immediately and returns only non-secret metadata. */
24
+ revoke(input: {
25
+ readonly id: string;
26
+ }): Promise<ApiKeyMetadata | null>;
27
+ }
28
+ declare const registerApiKey: (input: {
29
+ readonly apiKey: string;
30
+ readonly apiKeys: ApiKeyModel;
31
+ readonly createdAtMs?: number;
32
+ readonly name: string;
33
+ }) => Promise<CreatedApiKey>;
34
+ declare const createApiKey: (input: {
35
+ readonly apiKeys: ApiKeyModel;
36
+ readonly name: string;
37
+ }) => Promise<CreatedApiKey>;
38
+ /**
39
+ * Provisions the client API key used by managed init.
40
+ *
41
+ * A saved plaintext key is registered idempotently; otherwise a new key is
42
+ * created. Callers must persist or display the returned plaintext because the
43
+ * database stores only its hash.
44
+ */
45
+ declare const provisionApiKey: (input: {
46
+ readonly apiKeys: ApiKeyModel;
47
+ readonly existingApiKey?: string;
48
+ readonly name: string;
49
+ }) => Promise<CreatedApiKey>;
50
+ declare const authenticateApiKey: (input: {
51
+ readonly apiKeys: ApiKeyModel;
52
+ readonly headerName?: string;
53
+ readonly request: Request;
54
+ }) => Promise<boolean>;
55
+ declare const createApiKeyManagement: (input: {
56
+ readonly apiKeys: ApiKeyModel;
57
+ }) => ApiKeyManagementAPI;
58
+ //#endregion
59
+ export { API_KEY_HEADER_NAME, ApiKeyManagementAPI, ApiKeyMetadata, CreatedApiKey, authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, normalizeApiKeyHeaderName, provisionApiKey, registerApiKey };
@@ -0,0 +1,129 @@
1
+ //#region src/server/apiKeys.ts
2
+ const API_KEY_HEADER_NAME = "x-api-key";
3
+ const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
4
+ const bytesToBase64Url = (bytes) => {
5
+ let binary = "";
6
+ for (const byte of bytes) binary += String.fromCharCode(byte);
7
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
8
+ };
9
+ const isApiKey = (value) => {
10
+ if (!BASE64URL_32_PATTERN.test(value)) return false;
11
+ try {
12
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
13
+ const binary = atob(`${base64}=`);
14
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
15
+ return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
16
+ } catch {
17
+ return false;
18
+ }
19
+ };
20
+ const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
21
+ if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
22
+ try {
23
+ const headers = new Headers();
24
+ headers.set(value, "");
25
+ return headers.keys().next().value;
26
+ } catch {
27
+ throw new TypeError("apiKeys({ headerName }) must name a valid header.");
28
+ }
29
+ };
30
+ const hashApiKey = async (apiKey) => {
31
+ if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
32
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
33
+ return bytesToBase64Url(new Uint8Array(digest));
34
+ };
35
+ const apiKeyId = () => {
36
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
37
+ crypto.getRandomValues(bytes);
38
+ return `api-${bytesToBase64Url(bytes)}`;
39
+ };
40
+ const normalizeApiKeyName = (name) => {
41
+ const normalized = name.trim();
42
+ if (normalized.length === 0 || normalized.length > 64 || /[\p{Cc}\p{Cf}]/u.test(normalized)) throw new TypeError("API key names must contain 1-64 visible characters.");
43
+ return normalized;
44
+ };
45
+ const toApiKeyMetadata = ({ hash: _hash, ...metadata }) => Object.freeze(metadata);
46
+ const registerApiKey = async (input) => {
47
+ const hash = await hashApiKey(input.apiKey);
48
+ const createdAtMs = input.createdAtMs ?? Date.now();
49
+ if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) throw new TypeError("API key creation time must be non-negative.");
50
+ const record = Object.freeze({
51
+ created_at_ms: createdAtMs,
52
+ hash,
53
+ id: apiKeyId(),
54
+ name: normalizeApiKeyName(input.name),
55
+ prefix: input.apiKey.slice(0, 6),
56
+ revoked_at_ms: null,
57
+ role: "client"
58
+ });
59
+ if (await input.apiKeys.create(record) === "existing") {
60
+ const existing = await input.apiKeys.findByHash(hash);
61
+ if (existing === null || existing.revoked_at_ms !== null) throw new Error("The API key has been revoked.");
62
+ return Object.freeze({
63
+ apiKey: input.apiKey,
64
+ record: toApiKeyMetadata(existing)
65
+ });
66
+ }
67
+ return Object.freeze({
68
+ apiKey: input.apiKey,
69
+ record: toApiKeyMetadata(record)
70
+ });
71
+ };
72
+ const createApiKey = (input) => {
73
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
74
+ crypto.getRandomValues(bytes);
75
+ return registerApiKey({
76
+ apiKey: bytesToBase64Url(bytes),
77
+ apiKeys: input.apiKeys,
78
+ name: input.name
79
+ });
80
+ };
81
+ /**
82
+ * Provisions the client API key used by managed init.
83
+ *
84
+ * A saved plaintext key is registered idempotently; otherwise a new key is
85
+ * created. Callers must persist or display the returned plaintext because the
86
+ * database stores only its hash.
87
+ */
88
+ const provisionApiKey = (input) => {
89
+ const existingApiKey = input.existingApiKey?.trim();
90
+ return existingApiKey ? registerApiKey({
91
+ apiKey: existingApiKey,
92
+ apiKeys: input.apiKeys,
93
+ name: input.name
94
+ }) : createApiKey({
95
+ apiKeys: input.apiKeys,
96
+ name: input.name
97
+ });
98
+ };
99
+ const authenticateApiKey = async (input) => {
100
+ const apiKey = input.request.headers.get(normalizeApiKeyHeaderName(input.headerName));
101
+ if (apiKey === null || !isApiKey(apiKey)) return false;
102
+ const hash = await hashApiKey(apiKey);
103
+ const record = await input.apiKeys.findByHash(hash);
104
+ return record !== null && record.hash === hash && record.role === "client" && record.revoked_at_ms === null;
105
+ };
106
+ const createApiKeyManagement = (input) => Object.freeze({
107
+ async create({ name }) {
108
+ const created = await createApiKey({
109
+ apiKeys: input.apiKeys,
110
+ name
111
+ });
112
+ return Object.freeze({
113
+ apiKey: created.apiKey,
114
+ record: created.record
115
+ });
116
+ },
117
+ async list() {
118
+ return Object.freeze((await input.apiKeys.list()).map(toApiKeyMetadata));
119
+ },
120
+ async revoke({ id }) {
121
+ const record = await input.apiKeys.revoke({
122
+ id,
123
+ revokedAtMs: Date.now()
124
+ });
125
+ return record === null ? null : toApiKeyMetadata(record);
126
+ }
127
+ });
128
+ //#endregion
129
+ export { API_KEY_HEADER_NAME, apiKeyId, authenticateApiKey, createApiKey, createApiKeyManagement, hashApiKey, isApiKey, normalizeApiKeyHeaderName, normalizeApiKeyName, provisionApiKey, registerApiKey };
@@ -0,0 +1,115 @@
1
+ //#region src/server/cli.ts
2
+ /** The environment variable init stores the app's API key in. */
3
+ const API_KEY_ENV = "HOT_UPDATER_API_KEY";
4
+ /** A new API key: 32 random bytes, base64url, as the plugin creates them. */
5
+ const generateApiKey = () => {
6
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
7
+ return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
8
+ };
9
+ const formatList = (ui, records) => records.length === 0 ? ui.muted("(no API keys)") : ui.table([
10
+ {
11
+ key: "id",
12
+ label: "ID",
13
+ format: ui.id
14
+ },
15
+ {
16
+ key: "name",
17
+ label: "Name"
18
+ },
19
+ {
20
+ key: "prefix",
21
+ label: "Prefix",
22
+ format: ui.muted
23
+ },
24
+ {
25
+ key: "status",
26
+ label: "Status",
27
+ format: (value) => value.trim() === "active" ? ui.success(value) : ui.danger(value)
28
+ },
29
+ {
30
+ key: "created",
31
+ label: "Created",
32
+ format: ui.muted
33
+ }
34
+ ], records.map((record) => ({
35
+ created: new Date(record.created_at_ms).toISOString(),
36
+ id: record.id,
37
+ name: record.name,
38
+ prefix: record.prefix,
39
+ status: record.revoked_at_ms === null ? "active" : "revoked"
40
+ })));
41
+ /**
42
+ * `hot-updater api-key create|list|revoke`, and the API key that init
43
+ * provisions for an app, sent in `headerName`.
44
+ */
45
+ const apiKeysCli = (headerName) => ({
46
+ commands: [{
47
+ name: "api-key",
48
+ description: "Manage API keys",
49
+ commands: [
50
+ {
51
+ name: "create",
52
+ description: "Create an API key",
53
+ options: [{
54
+ flags: "--name <name>",
55
+ description: "name used to identify the API key",
56
+ required: true
57
+ }],
58
+ async run({ api, options, ui }) {
59
+ const created = await api.create({ name: String(options.name) });
60
+ ui.message(ui.block("API key created", [
61
+ ui.kv("Name", created.record.name),
62
+ ui.kv("ID", ui.id(created.record.id)),
63
+ ui.kv("API key", ui.warning(created.apiKey))
64
+ ]));
65
+ ui.warn("Save this API key now. It will not be shown again.");
66
+ }
67
+ },
68
+ {
69
+ name: "list",
70
+ description: "List API keys",
71
+ options: [{
72
+ flags: "--json",
73
+ description: "output API key metadata as JSON"
74
+ }],
75
+ async run({ api, options, ui }) {
76
+ const records = [...await api.list()].sort((left, right) => right.created_at_ms - left.created_at_ms);
77
+ if (options.json) ui.print(JSON.stringify(records, null, 2));
78
+ else ui.message(formatList(ui, records));
79
+ }
80
+ },
81
+ {
82
+ name: "revoke",
83
+ description: "Revoke an API key",
84
+ arguments: [{
85
+ name: "id",
86
+ description: "API key id"
87
+ }],
88
+ options: [{
89
+ flags: "-y, --yes",
90
+ description: "skip confirmation prompt"
91
+ }],
92
+ async run({ api, args, options, ui }) {
93
+ const id = String(args.id);
94
+ if (!options.yes) await ui.confirm(`Revoke API key ${id}?`);
95
+ const revoked = await api.revoke({ id });
96
+ if (revoked === null) throw new Error(`API key "${id}" was not found.`);
97
+ ui.message(ui.block("API key revoked", [ui.kv("Name", revoked.name), ui.kv("ID", ui.id(revoked.id))]));
98
+ }
99
+ }
100
+ ]
101
+ }],
102
+ clientCredential: {
103
+ label: "API key",
104
+ header: headerName,
105
+ env: API_KEY_ENV,
106
+ generate: generateApiKey,
107
+ provision: async (api, { existing, name }) => (await api.provision({
108
+ ...existing === void 0 ? {} : { existingApiKey: existing },
109
+ name
110
+ })).apiKey
111
+ }
112
+ });
113
+ //#endregion
114
+ exports.API_KEY_ENV = API_KEY_ENV;
115
+ exports.apiKeysCli = apiKeysCli;
@@ -0,0 +1,17 @@
1
+ import { ApiKeyManagementAPI, CreatedApiKey } from "./apiKeys.cjs";
2
+ import { PluginCli } from "@hot-updater/plugin-core/server-plugin";
3
+ //#region src/server/cli.d.ts
4
+ /** The part of the apiKeys() API the CLI uses. */
5
+ interface ApiKeysCliApi extends ApiKeyManagementAPI {
6
+ provision(input: {
7
+ readonly existingApiKey?: string;
8
+ readonly name: string;
9
+ }): Promise<CreatedApiKey>;
10
+ }
11
+ /**
12
+ * `hot-updater api-key create|list|revoke`, and the API key that init
13
+ * provisions for an app, sent in `headerName`.
14
+ */
15
+ declare const apiKeysCli: (headerName: string) => PluginCli<ApiKeysCliApi>;
16
+ //#endregion
17
+ export { ApiKeysCliApi, apiKeysCli };
@@ -0,0 +1,17 @@
1
+ import { ApiKeyManagementAPI, CreatedApiKey } from "./apiKeys.mjs";
2
+ import { PluginCli } from "@hot-updater/plugin-core/server-plugin";
3
+ //#region src/server/cli.d.ts
4
+ /** The part of the apiKeys() API the CLI uses. */
5
+ interface ApiKeysCliApi extends ApiKeyManagementAPI {
6
+ provision(input: {
7
+ readonly existingApiKey?: string;
8
+ readonly name: string;
9
+ }): Promise<CreatedApiKey>;
10
+ }
11
+ /**
12
+ * `hot-updater api-key create|list|revoke`, and the API key that init
13
+ * provisions for an app, sent in `headerName`.
14
+ */
15
+ declare const apiKeysCli: (headerName: string) => PluginCli<ApiKeysCliApi>;
16
+ //#endregion
17
+ export { ApiKeysCliApi, apiKeysCli };