storybooker 0.19.4 → 0.22.0-canary.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 +40 -18
- package/dist/adapters/_internal/queue.d.mts +127 -0
- package/dist/aws-dynamodb.d.mts +22 -0
- package/dist/aws-dynamodb.mjs +118 -0
- package/dist/aws-dynamodb.mjs.map +1 -0
- package/dist/aws-s3.d.mts +20 -0
- package/dist/aws-s3.mjs +96 -0
- package/dist/aws-s3.mjs.map +1 -0
- package/dist/azure-blob-storage.d.mts +20 -0
- package/dist/azure-blob-storage.mjs +126 -0
- package/dist/azure-blob-storage.mjs.map +1 -0
- package/dist/azure-cosmos-db.d.mts +23 -0
- package/dist/azure-cosmos-db.mjs +87 -0
- package/dist/azure-cosmos-db.mjs.map +1 -0
- package/dist/azure-data-tables.d.mts +23 -0
- package/dist/azure-data-tables.mjs +127 -0
- package/dist/azure-data-tables.mjs.map +1 -0
- package/dist/azure-easy-auth.d.mts +50 -0
- package/dist/azure-easy-auth.mjs +88 -0
- package/dist/azure-easy-auth.mjs.map +1 -0
- package/dist/azure-functions.d.mts +62 -0
- package/dist/azure-functions.mjs +147 -0
- package/dist/azure-functions.mjs.map +1 -0
- package/dist/fs.d.mts +37 -0
- package/dist/fs.mjs +240 -0
- package/dist/fs.mjs.map +1 -0
- package/dist/gcp-big-table.d.mts +23 -0
- package/dist/gcp-big-table.mjs +92 -0
- package/dist/gcp-big-table.mjs.map +1 -0
- package/dist/gcp-firestore.d.mts +22 -0
- package/dist/gcp-firestore.mjs +87 -0
- package/dist/gcp-firestore.mjs.map +1 -0
- package/dist/gcp-storage.d.mts +20 -0
- package/dist/gcp-storage.mjs +96 -0
- package/dist/gcp-storage.mjs.map +1 -0
- package/dist/handlers/handle-process-zip.mjs +90 -0
- package/dist/handlers/handle-process-zip.mjs.map +1 -0
- package/dist/handlers/handle-purge.d.mts +12 -0
- package/dist/handlers/handle-purge.mjs +36 -0
- package/dist/handlers/handle-purge.mjs.map +1 -0
- package/dist/handlers/handle-serve-storybook.mjs +94 -0
- package/dist/handlers/handle-serve-storybook.mjs.map +1 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +62 -0
- package/dist/index.mjs.map +1 -0
- package/dist/models/builds-model.mjs +248 -0
- package/dist/models/builds-model.mjs.map +1 -0
- package/dist/models/builds-schema.d.mts +171 -0
- package/dist/models/builds-schema.mjs +67 -0
- package/dist/models/builds-schema.mjs.map +1 -0
- package/dist/models/projects-model.mjs +122 -0
- package/dist/models/projects-model.mjs.map +1 -0
- package/dist/models/projects-schema.d.mts +70 -0
- package/dist/models/projects-schema.mjs +37 -0
- package/dist/models/projects-schema.mjs.map +1 -0
- package/dist/models/tags-model.mjs +110 -0
- package/dist/models/tags-model.mjs.map +1 -0
- package/dist/models/tags-schema.d.mts +76 -0
- package/dist/models/tags-schema.mjs +34 -0
- package/dist/models/tags-schema.mjs.map +1 -0
- package/dist/models/~model.mjs +43 -0
- package/dist/models/~model.mjs.map +1 -0
- package/dist/models/~shared-schema.d.mts +1 -0
- package/dist/models/~shared-schema.mjs +20 -0
- package/dist/models/~shared-schema.mjs.map +1 -0
- package/dist/mysql.d.mts +39 -0
- package/dist/mysql.mjs +151 -0
- package/dist/mysql.mjs.map +1 -0
- package/dist/redis.d.mts +33 -0
- package/dist/redis.mjs +118 -0
- package/dist/redis.mjs.map +1 -0
- package/dist/routers/account-router.mjs +91 -0
- package/dist/routers/account-router.mjs.map +1 -0
- package/dist/routers/builds-router.mjs +347 -0
- package/dist/routers/builds-router.mjs.map +1 -0
- package/dist/routers/projects-router.mjs +236 -0
- package/dist/routers/projects-router.mjs.map +1 -0
- package/dist/routers/root-router.mjs +108 -0
- package/dist/routers/root-router.mjs.map +1 -0
- package/dist/routers/tags-router.mjs +269 -0
- package/dist/routers/tags-router.mjs.map +1 -0
- package/dist/routers/tasks-router.mjs +71 -0
- package/dist/routers/tasks-router.mjs.map +1 -0
- package/dist/urls.d.mts +47 -0
- package/dist/urls.mjs +208 -0
- package/dist/urls.mjs.map +1 -0
- package/dist/utils/adapter-utils.d.mts +14 -0
- package/dist/utils/adapter-utils.mjs +14 -0
- package/dist/utils/adapter-utils.mjs.map +1 -0
- package/dist/utils/auth.mjs +25 -0
- package/dist/utils/auth.mjs.map +1 -0
- package/dist/utils/error.d.mts +21 -0
- package/dist/utils/error.mjs +109 -0
- package/dist/utils/error.mjs.map +1 -0
- package/dist/utils/file-utils.mjs +16 -0
- package/dist/utils/file-utils.mjs.map +1 -0
- package/dist/utils/openapi-utils.mjs +45 -0
- package/dist/utils/openapi-utils.mjs.map +1 -0
- package/dist/utils/request.mjs +35 -0
- package/dist/utils/request.mjs.map +1 -0
- package/dist/utils/response.mjs +24 -0
- package/dist/utils/response.mjs.map +1 -0
- package/dist/utils/store.mjs +54 -0
- package/dist/utils/store.mjs.map +1 -0
- package/dist/utils/ui-utils.mjs +38 -0
- package/dist/utils/ui-utils.mjs.map +1 -0
- package/dist/utils/url-utils.d.mts +10 -0
- package/dist/utils/url-utils.mjs +54 -0
- package/dist/utils/url-utils.mjs.map +1 -0
- package/dist/~internal/adapter/auth.d.mts +123 -0
- package/dist/~internal/adapter/auth.mjs +20 -0
- package/dist/~internal/adapter/auth.mjs.map +1 -0
- package/dist/~internal/adapter/database.d.mts +240 -0
- package/dist/~internal/adapter/database.mjs +63 -0
- package/dist/~internal/adapter/database.mjs.map +1 -0
- package/dist/~internal/adapter/logger.d.mts +34 -0
- package/dist/~internal/adapter/logger.mjs +13 -0
- package/dist/~internal/adapter/logger.mjs.map +1 -0
- package/dist/~internal/adapter/storage.d.mts +208 -0
- package/dist/~internal/adapter/storage.mjs +63 -0
- package/dist/~internal/adapter/storage.mjs.map +1 -0
- package/dist/~internal/adapter/ui.d.mts +109 -0
- package/dist/~internal/adapter/ui.mjs +1 -0
- package/dist/~internal/adapter.d.mts +8 -0
- package/dist/~internal/adapter.mjs +6 -0
- package/dist/~internal/constants.d.mts +24 -0
- package/dist/~internal/constants.mjs +32 -0
- package/dist/~internal/constants.mjs.map +1 -0
- package/dist/~internal/mimes.d.mts +449 -0
- package/dist/~internal/mimes.mjs +454 -0
- package/dist/~internal/mimes.mjs.map +1 -0
- package/dist/~internal/router.d.mts +1651 -0
- package/dist/~internal/router.mjs +39 -0
- package/dist/~internal/router.mjs.map +1 -0
- package/dist/~internal/types.d.mts +77 -0
- package/dist/~internal/types.mjs +1 -0
- package/dist/~internal/utils.d.mts +4 -0
- package/dist/~internal/utils.mjs +5 -0
- package/openapi.json +3162 -0
- package/package.json +148 -27
- package/src/adapters/_internal/auth.ts +135 -0
- package/src/adapters/_internal/database.ts +241 -0
- package/src/adapters/_internal/index.ts +8 -0
- package/src/adapters/_internal/logger.ts +41 -0
- package/src/adapters/_internal/queue.ts +151 -0
- package/src/adapters/_internal/storage.ts +197 -0
- package/src/adapters/_internal/ui.ts +103 -0
- package/src/adapters/aws-dynamodb.ts +201 -0
- package/src/adapters/aws-s3.ts +160 -0
- package/src/adapters/azure-blob-storage.ts +223 -0
- package/src/adapters/azure-cosmos-db.ts +158 -0
- package/src/adapters/azure-data-tables.ts +223 -0
- package/src/adapters/azure-easy-auth.ts +174 -0
- package/src/adapters/azure-functions.ts +242 -0
- package/src/adapters/fs.ts +398 -0
- package/src/adapters/gcp-big-table.ts +157 -0
- package/src/adapters/gcp-firestore.ts +146 -0
- package/src/adapters/gcp-storage.ts +141 -0
- package/src/adapters/mysql.ts +296 -0
- package/src/adapters/redis.ts +242 -0
- package/src/handlers/handle-process-zip.ts +117 -0
- package/src/handlers/handle-purge.ts +65 -0
- package/src/handlers/handle-serve-storybook.ts +101 -0
- package/src/index.ts +81 -16
- package/src/mocks/mock-auth-service.ts +51 -0
- package/src/mocks/mock-store.ts +26 -0
- package/src/models/builds-model.ts +373 -0
- package/src/models/builds-schema.ts +84 -0
- package/src/models/projects-model.ts +177 -0
- package/src/models/projects-schema.ts +69 -0
- package/src/models/tags-model.ts +138 -0
- package/src/models/tags-schema.ts +45 -0
- package/src/models/~model.ts +79 -0
- package/src/models/~shared-schema.ts +14 -0
- package/src/routers/_app-router.ts +57 -0
- package/src/routers/account-router.ts +136 -0
- package/src/routers/builds-router.ts +464 -0
- package/src/routers/projects-router.ts +309 -0
- package/src/routers/root-router.ts +127 -0
- package/src/routers/tags-router.ts +339 -0
- package/src/routers/tasks-router.ts +75 -0
- package/src/types.ts +107 -0
- package/src/urls.ts +327 -0
- package/src/utils/adapter-utils.ts +26 -0
- package/src/utils/auth.test.ts +71 -0
- package/src/utils/auth.ts +39 -0
- package/src/utils/constants.ts +31 -0
- package/src/utils/date-utils.ts +10 -0
- package/src/utils/error.test.ts +86 -0
- package/src/utils/error.ts +140 -0
- package/src/utils/file-utils.test.ts +65 -0
- package/src/utils/file-utils.ts +43 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/mime-utils.ts +457 -0
- package/src/utils/openapi-utils.ts +49 -0
- package/src/utils/request.ts +97 -0
- package/src/utils/response.ts +20 -0
- package/src/utils/store.ts +85 -0
- package/src/utils/story-utils.ts +42 -0
- package/src/utils/text-utils.ts +10 -0
- package/src/utils/ui-utils.ts +57 -0
- package/src/utils/url-utils.ts +113 -0
- package/dist/index.js +0 -554
- package/src/commands/create.ts +0 -263
- package/src/commands/purge.ts +0 -70
- package/src/commands/test.ts +0 -42
- package/src/service-schema.d.ts +0 -2023
- package/src/utils/auth-utils.ts +0 -31
- package/src/utils/pkg-utils.ts +0 -37
- package/src/utils/sb-build.ts +0 -55
- package/src/utils/sb-test.ts +0 -115
- package/src/utils/schema-utils.ts +0 -123
- package/src/utils/stream-utils.ts +0 -72
- package/src/utils/types.ts +0 -4
- package/src/utils/zip.ts +0 -77
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { TableClient, TableEntityResult, TableServiceClient } from "@azure/data-tables";
|
|
2
|
+
import {
|
|
3
|
+
DatabaseAdapterErrors,
|
|
4
|
+
type DatabaseAdapter,
|
|
5
|
+
type DatabaseAdapterOptions,
|
|
6
|
+
type DatabaseDocumentListOptions,
|
|
7
|
+
type StoryBookerDatabaseDocument,
|
|
8
|
+
} from "./_internal/database.ts";
|
|
9
|
+
|
|
10
|
+
export type TableClientGenerator = (tableName: string) => TableClient;
|
|
11
|
+
|
|
12
|
+
export class AzureDataTablesDatabaseService implements DatabaseAdapter {
|
|
13
|
+
#serviceClient: TableServiceClient;
|
|
14
|
+
#tableClientGenerator: TableClientGenerator;
|
|
15
|
+
|
|
16
|
+
constructor(serviceClient: TableServiceClient, tableClientGenerator: TableClientGenerator) {
|
|
17
|
+
this.#serviceClient = serviceClient;
|
|
18
|
+
this.#tableClientGenerator = tableClientGenerator;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
metadata: DatabaseAdapter["metadata"] = { name: "Azure Tables" };
|
|
22
|
+
|
|
23
|
+
listCollections: DatabaseAdapter["listCollections"] = async (options) => {
|
|
24
|
+
const collections: string[] = [];
|
|
25
|
+
for await (const table of this.#serviceClient.listTables({
|
|
26
|
+
abortSignal: options.abortSignal,
|
|
27
|
+
})) {
|
|
28
|
+
if (table.name) {
|
|
29
|
+
collections.push(table.name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return collections;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
createCollection: DatabaseAdapter["createCollection"] = async (collectionId, options) => {
|
|
37
|
+
try {
|
|
38
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
39
|
+
await this.#serviceClient.createTable(tableName, {
|
|
40
|
+
abortSignal: options.abortSignal,
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
hasCollection: DatabaseAdapter["hasCollection"] = async (collectionId, options) => {
|
|
48
|
+
try {
|
|
49
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
50
|
+
const iterator = this.#serviceClient.listTables({
|
|
51
|
+
abortSignal: options.abortSignal,
|
|
52
|
+
queryOptions: { filter: `TableName eq '${tableName}'` },
|
|
53
|
+
});
|
|
54
|
+
for await (const table of iterator) {
|
|
55
|
+
if (table.name === collectionId) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
deleteCollection: DatabaseAdapter["deleteCollection"] = async (collectionId, options) => {
|
|
67
|
+
try {
|
|
68
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
69
|
+
await this.#serviceClient.deleteTable(tableName, {
|
|
70
|
+
abortSignal: options.abortSignal,
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
listDocuments: DatabaseAdapter["listDocuments"] = async <
|
|
78
|
+
Document extends StoryBookerDatabaseDocument,
|
|
79
|
+
>(
|
|
80
|
+
collectionId: string,
|
|
81
|
+
listOptions: DatabaseDocumentListOptions<Document>,
|
|
82
|
+
options: DatabaseAdapterOptions,
|
|
83
|
+
): Promise<Document[]> => {
|
|
84
|
+
const { filter, limit, select, sort } = listOptions ?? {};
|
|
85
|
+
|
|
86
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
87
|
+
const tableClient = this.#tableClientGenerator(tableName);
|
|
88
|
+
|
|
89
|
+
const pageIterator = tableClient
|
|
90
|
+
.listEntities({
|
|
91
|
+
abortSignal: options.abortSignal,
|
|
92
|
+
queryOptions: {
|
|
93
|
+
filter: typeof filter === "string" ? filter : undefined,
|
|
94
|
+
select,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
.byPage({ maxPageSize: limit });
|
|
98
|
+
|
|
99
|
+
const items: Document[] = [];
|
|
100
|
+
for await (const page of pageIterator) {
|
|
101
|
+
for (const entity of page) {
|
|
102
|
+
const item = entityToItem<Document>(entity);
|
|
103
|
+
if (filter && typeof filter === "function") {
|
|
104
|
+
if (filter(item)) {
|
|
105
|
+
items.push(item);
|
|
106
|
+
} else {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
items.push(item);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (sort && typeof sort === "function") {
|
|
116
|
+
items.sort(sort);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return items;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
getDocument: DatabaseAdapter["getDocument"] = async <
|
|
123
|
+
Document extends StoryBookerDatabaseDocument,
|
|
124
|
+
>(
|
|
125
|
+
collectionId: string,
|
|
126
|
+
documentId: string,
|
|
127
|
+
options: DatabaseAdapterOptions,
|
|
128
|
+
): Promise<Document> => {
|
|
129
|
+
try {
|
|
130
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
131
|
+
const tableClient = this.#tableClientGenerator(tableName);
|
|
132
|
+
const entity = await tableClient.getEntity(collectionId, documentId, {
|
|
133
|
+
abortSignal: options.abortSignal,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return entityToItem<Document>(entity);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
hasDocument: DatabaseAdapter["hasDocument"] = async (collectionId, documentId, options) => {
|
|
143
|
+
try {
|
|
144
|
+
return Boolean(await this.getDocument(collectionId, documentId, options));
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
createDocument: DatabaseAdapter["createDocument"] = async (
|
|
151
|
+
collectionId,
|
|
152
|
+
documentData,
|
|
153
|
+
options,
|
|
154
|
+
) => {
|
|
155
|
+
try {
|
|
156
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
157
|
+
const tableClient = this.#tableClientGenerator(tableName);
|
|
158
|
+
await tableClient.createEntity(
|
|
159
|
+
{
|
|
160
|
+
...documentData,
|
|
161
|
+
partitionKey: collectionId,
|
|
162
|
+
rowKey: documentData.id,
|
|
163
|
+
},
|
|
164
|
+
{ abortSignal: options.abortSignal },
|
|
165
|
+
);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(
|
|
168
|
+
collectionId,
|
|
169
|
+
documentData.id,
|
|
170
|
+
error,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
deleteDocument: DatabaseAdapter["deleteDocument"] = async (collectionId, documentId, options) => {
|
|
176
|
+
try {
|
|
177
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
178
|
+
const tableClient = this.#tableClientGenerator(tableName);
|
|
179
|
+
await tableClient.deleteEntity(collectionId, documentId, {
|
|
180
|
+
abortSignal: options.abortSignal,
|
|
181
|
+
});
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// oxlint-disable-next-line max-params
|
|
188
|
+
updateDocument: DatabaseAdapter["updateDocument"] = async (
|
|
189
|
+
collectionId,
|
|
190
|
+
documentId,
|
|
191
|
+
documentData,
|
|
192
|
+
options,
|
|
193
|
+
) => {
|
|
194
|
+
try {
|
|
195
|
+
const tableName = genTableNameFromCollectionId(collectionId);
|
|
196
|
+
const tableClient = this.#tableClientGenerator(tableName);
|
|
197
|
+
await tableClient.updateEntity(
|
|
198
|
+
{ ...documentData, partitionKey: collectionId, rowKey: documentId },
|
|
199
|
+
"Merge",
|
|
200
|
+
{ abortSignal: options.abortSignal },
|
|
201
|
+
);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function genTableNameFromCollectionId(collectionId: string): string {
|
|
209
|
+
if (/^[A-Za-z][A-Za-z0-9]{2,62}$/.test(collectionId)) {
|
|
210
|
+
return collectionId;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return collectionId.replaceAll(/\W/g, "").slice(0, 63).padEnd(3, "X");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function entityToItem<Item extends { id: string }>(
|
|
217
|
+
entity: TableEntityResult<Record<string, unknown>>,
|
|
218
|
+
): Item {
|
|
219
|
+
return {
|
|
220
|
+
...entity,
|
|
221
|
+
id: entity.rowKey ?? entity.partitionKey ?? entity.etag,
|
|
222
|
+
} as unknown as Item;
|
|
223
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// oxlint-disable class-methods-use-this
|
|
2
|
+
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import {
|
|
5
|
+
StoryBookerPermissionsList,
|
|
6
|
+
StoryBookerPermissionsAllEnabled,
|
|
7
|
+
type AuthAdapter,
|
|
8
|
+
type AuthAdapterOptions,
|
|
9
|
+
type StoryBookerPermissionAction,
|
|
10
|
+
type StoryBookerPermissionResource,
|
|
11
|
+
type StoryBookerPermissionWithKey,
|
|
12
|
+
type StoryBookerUser,
|
|
13
|
+
} from "./_internal/auth.ts";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
StoryBookerPermission,
|
|
17
|
+
StoryBookerPermissionAction,
|
|
18
|
+
StoryBookerPermissionKey,
|
|
19
|
+
StoryBookerPermissionResource,
|
|
20
|
+
StoryBookerPermissionWithKey,
|
|
21
|
+
} from "./_internal/auth.ts";
|
|
22
|
+
|
|
23
|
+
export interface AzureEasyAuthClientPrincipal {
|
|
24
|
+
claims: { typ: string; val: string }[];
|
|
25
|
+
auth_typ: string;
|
|
26
|
+
name_typ: string;
|
|
27
|
+
role_typ: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AzureEasyAuthUser extends StoryBookerUser {
|
|
31
|
+
roles: string[] | null;
|
|
32
|
+
type: "application" | "user";
|
|
33
|
+
clientPrincipal?: AzureEasyAuthClientPrincipal;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type AuthAdapterAuthorise<AuthUser extends StoryBookerUser = StoryBookerUser> = (
|
|
37
|
+
permission: StoryBookerPermissionWithKey,
|
|
38
|
+
user: Omit<AuthUser, "permissions">,
|
|
39
|
+
) => boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Modify the final user details object created from EasyAuth Client Principal.
|
|
43
|
+
*/
|
|
44
|
+
export type ModifyUserDetails = <User extends Omit<AzureEasyAuthUser, "permissions">>(
|
|
45
|
+
user: User,
|
|
46
|
+
options: AuthAdapterOptions,
|
|
47
|
+
) => User | Promise<User>;
|
|
48
|
+
|
|
49
|
+
const DEFAULT_AUTHORISE: AuthAdapterAuthorise<AzureEasyAuthUser> = (permission, user) => {
|
|
50
|
+
if (!user) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (permission.action === "read") {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Boolean(user.roles && user.roles.length > 0);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const DEFAULT_MODIFY_USER: ModifyUserDetails = (user) => user;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* StoryBooker Auth adapter for Azure EasyAuth.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const auth = new AzureEasyAuthService();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class AzureEasyAuthService implements AuthAdapter<AzureEasyAuthUser> {
|
|
72
|
+
#authorise: AuthAdapterAuthorise<AzureEasyAuthUser>;
|
|
73
|
+
#modifyUserDetails: ModifyUserDetails;
|
|
74
|
+
|
|
75
|
+
metadata: AuthAdapter["metadata"] = { name: "Azure Easy Auth" };
|
|
76
|
+
|
|
77
|
+
constructor(options?: {
|
|
78
|
+
/**
|
|
79
|
+
* Custom function to authorise permission for user
|
|
80
|
+
*/
|
|
81
|
+
authorise?: AuthAdapterAuthorise<AzureEasyAuthUser>;
|
|
82
|
+
/**
|
|
83
|
+
* Modify the final user details object created from EasyAuth Client Principal.
|
|
84
|
+
*/
|
|
85
|
+
modifyUserDetails?: ModifyUserDetails;
|
|
86
|
+
}) {
|
|
87
|
+
this.#authorise = options?.authorise ?? DEFAULT_AUTHORISE;
|
|
88
|
+
this.#modifyUserDetails = options?.modifyUserDetails ?? DEFAULT_MODIFY_USER;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getUserDetails: AuthAdapter<AzureEasyAuthUser>["getUserDetails"] = async (options) => {
|
|
92
|
+
const principalHeader = options.request.headers.get("x-ms-client-principal");
|
|
93
|
+
if (!principalHeader) {
|
|
94
|
+
throw new Response(`Unauthorized access. Please provide a valid EasyAuth principal header.`, {
|
|
95
|
+
status: 401,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Decode and parse the claims
|
|
100
|
+
const decodedPrincipal = Buffer.from(principalHeader, "base64").toString("utf8");
|
|
101
|
+
|
|
102
|
+
const clientPrincipal: AzureEasyAuthClientPrincipal = JSON.parse(decodedPrincipal);
|
|
103
|
+
const claims = clientPrincipal?.claims ?? [];
|
|
104
|
+
|
|
105
|
+
const azpToken = claims.find((claim) => claim.typ === "azp")?.val;
|
|
106
|
+
if (azpToken) {
|
|
107
|
+
const user: AzureEasyAuthUser = {
|
|
108
|
+
clientPrincipal,
|
|
109
|
+
displayName: "App",
|
|
110
|
+
id: azpToken,
|
|
111
|
+
permissions: StoryBookerPermissionsAllEnabled,
|
|
112
|
+
roles: null,
|
|
113
|
+
type: "application",
|
|
114
|
+
};
|
|
115
|
+
return user;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const name = claims.find((claim) => claim.typ === "name")?.val;
|
|
119
|
+
const email = claims.find((claim) => claim.typ === clientPrincipal.name_typ)?.val;
|
|
120
|
+
const roles = claims
|
|
121
|
+
.filter((claim) => claim.typ === clientPrincipal.role_typ || claim.typ === "roles")
|
|
122
|
+
.map((claim) => claim.val);
|
|
123
|
+
|
|
124
|
+
const userWithoutPermissions: Omit<AzureEasyAuthUser, "permissions"> = {
|
|
125
|
+
clientPrincipal,
|
|
126
|
+
displayName: name ?? "",
|
|
127
|
+
id: email ?? "",
|
|
128
|
+
roles,
|
|
129
|
+
title: roles.join(", "),
|
|
130
|
+
type: "user",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...(await this.#modifyUserDetails(userWithoutPermissions, options)),
|
|
135
|
+
permissions: authoriseUserPermissions(this.#authorise, userWithoutPermissions),
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
login: AuthAdapter<AzureEasyAuthUser>["login"] = ({ request }) => {
|
|
140
|
+
const url = new URL("/.auth/login", request.url);
|
|
141
|
+
|
|
142
|
+
return new Response(null, {
|
|
143
|
+
headers: { Location: url.toString() },
|
|
144
|
+
status: 302,
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
logout: AuthAdapter<AzureEasyAuthUser>["logout"] = (_user, { request }) => {
|
|
149
|
+
const url = new URL("/.auth/logout", request.url);
|
|
150
|
+
|
|
151
|
+
return new Response(null, {
|
|
152
|
+
headers: { Location: url.toString() },
|
|
153
|
+
status: 302,
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function authoriseUserPermissions(
|
|
159
|
+
authorise: AuthAdapterAuthorise<AzureEasyAuthUser>,
|
|
160
|
+
user: Omit<AzureEasyAuthUser, "permissions">,
|
|
161
|
+
): AzureEasyAuthUser["permissions"] {
|
|
162
|
+
const permissions: AzureEasyAuthUser["permissions"] = {};
|
|
163
|
+
|
|
164
|
+
for (const key of StoryBookerPermissionsList) {
|
|
165
|
+
const [resource, action] = key.split(":") as [
|
|
166
|
+
StoryBookerPermissionResource,
|
|
167
|
+
StoryBookerPermissionAction,
|
|
168
|
+
];
|
|
169
|
+
const permission: StoryBookerPermissionWithKey = { action, key, resource };
|
|
170
|
+
permissions[key] = authorise(permission, user);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return permissions;
|
|
174
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// oxlint-disable max-lines-per-function
|
|
2
|
+
|
|
3
|
+
import type { RestError } from "@azure/core-rest-pipeline";
|
|
4
|
+
import type {
|
|
5
|
+
Cookie,
|
|
6
|
+
HttpFunctionOptions,
|
|
7
|
+
HttpRequest,
|
|
8
|
+
HttpResponseInit,
|
|
9
|
+
HttpTriggerOptions,
|
|
10
|
+
InvocationContext,
|
|
11
|
+
SetupOptions,
|
|
12
|
+
TimerFunctionOptions,
|
|
13
|
+
} from "@azure/functions";
|
|
14
|
+
import { createHonoRouter, createPurgeHandler } from "../index.ts";
|
|
15
|
+
import type { ErrorParser, RouterOptions, StoryBookerUser } from "../types.ts";
|
|
16
|
+
import { generatePrefixFromBaseRoute, SERVICE_NAME, urlJoin } from "../utils/index.ts";
|
|
17
|
+
import type { AuthAdapter } from "./_internal/auth.ts";
|
|
18
|
+
import type { LoggerAdapter } from "./_internal/logger.ts";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_PURGE_SCHEDULE_CRON = "0 0 0 * * *";
|
|
21
|
+
|
|
22
|
+
export type * from "storybooker/types";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal representation of Azure Functions App namespace
|
|
26
|
+
* to register HTTP and Timer functions.
|
|
27
|
+
*/
|
|
28
|
+
interface FunctionsApp {
|
|
29
|
+
http(name: string, options: HttpFunctionOptions): void;
|
|
30
|
+
setup?(options: SetupOptions): void;
|
|
31
|
+
timer?(name: string, options: TimerFunctionOptions): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options to register the storybooker router
|
|
36
|
+
*/
|
|
37
|
+
export interface RegisterStorybookerRouterOptions<User extends StoryBookerUser> extends Omit<
|
|
38
|
+
RouterOptions<User>,
|
|
39
|
+
"auth"
|
|
40
|
+
> {
|
|
41
|
+
/**
|
|
42
|
+
* For authenticating routes, either use an AuthAdapter or Functions auth-level property.
|
|
43
|
+
*
|
|
44
|
+
* - AuthAdapter allows full customization of authentication logic. This will set the function to "anonymous" auth-level.
|
|
45
|
+
* - Auth-level is a simpler way to set predefined authentication levels ("function", "admin").
|
|
46
|
+
* This is a good option to set if the service is used in
|
|
47
|
+
* Headless mode and requires single token authentication for all the requests.
|
|
48
|
+
*
|
|
49
|
+
* @see https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-javascript#http-auth (AuthLevels)
|
|
50
|
+
*/
|
|
51
|
+
auth?: AuthAdapter<User> | Exclude<HttpTriggerOptions["authLevel"], "anonymous">;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Define the route on which all router is placed.
|
|
55
|
+
* Can be a sub-path of the main API route.
|
|
56
|
+
*
|
|
57
|
+
* @default ''
|
|
58
|
+
*/
|
|
59
|
+
route?: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Modify the cron-schedule of timer function
|
|
63
|
+
* which purge outdated storybooks.
|
|
64
|
+
*
|
|
65
|
+
* Pass `null` to disable auto-purge functionality.
|
|
66
|
+
*
|
|
67
|
+
* @default "0 0 0 * * *" // Every midnight
|
|
68
|
+
*/
|
|
69
|
+
purgeScheduleCron?: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register the Storybooker router with the Azure Functions App.
|
|
74
|
+
*
|
|
75
|
+
* - It enabled streaming responses for HTTP functions.
|
|
76
|
+
* - It registers the HTTP function with provided route and auth-level.
|
|
77
|
+
* - It registers the Timer function for purge if `purgeScheduleCron` is not `null`.
|
|
78
|
+
*
|
|
79
|
+
* @param app Azure Functions App instance
|
|
80
|
+
* @param options Options for registering the router
|
|
81
|
+
*/
|
|
82
|
+
export function registerStoryBookerRouter<User extends StoryBookerUser>(
|
|
83
|
+
app: FunctionsApp,
|
|
84
|
+
options: RegisterStorybookerRouterOptions<User>,
|
|
85
|
+
): void {
|
|
86
|
+
app.setup?.({ enableHttpStream: true });
|
|
87
|
+
const { auth, purgeScheduleCron, route, ...rest } = options;
|
|
88
|
+
|
|
89
|
+
const routerOptions: RouterOptions<User> = rest;
|
|
90
|
+
routerOptions.config ??= {};
|
|
91
|
+
routerOptions.config.errorParser ??= parseAzureRestError;
|
|
92
|
+
|
|
93
|
+
if (route) {
|
|
94
|
+
routerOptions.config.prefix = generatePrefixFromBaseRoute(route);
|
|
95
|
+
}
|
|
96
|
+
if (typeof auth === "object") {
|
|
97
|
+
routerOptions.auth = auth;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const router = createHonoRouter(routerOptions);
|
|
101
|
+
|
|
102
|
+
app.http(SERVICE_NAME, {
|
|
103
|
+
authLevel: typeof auth === "string" ? auth : "anonymous",
|
|
104
|
+
handler: async (httpRequest) => {
|
|
105
|
+
const request = newRequestFromAzureFunctions(httpRequest);
|
|
106
|
+
const response = await router.fetch(request);
|
|
107
|
+
return newAzureFunctionsResponse(response);
|
|
108
|
+
},
|
|
109
|
+
methods: ["GET", "POST", "DELETE", "HEAD", "PATCH", "PUT", "OPTIONS", "TRACE", "CONNECT"],
|
|
110
|
+
route: urlJoin(route ?? "", "{**path}"),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (purgeScheduleCron !== null && app.timer) {
|
|
114
|
+
const schedule = purgeScheduleCron ?? DEFAULT_PURGE_SCHEDULE_CRON;
|
|
115
|
+
const purgeHandler = createPurgeHandler({
|
|
116
|
+
database: routerOptions.database,
|
|
117
|
+
storage: routerOptions.storage,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
app.timer(`${SERVICE_NAME}-timer_purge`, {
|
|
121
|
+
// oxlint-disable-next-line require-await
|
|
122
|
+
handler: async (_timer, context) =>
|
|
123
|
+
purgeHandler({}, { logger: createAzureContextLogger(context) }),
|
|
124
|
+
runOnStartup: false,
|
|
125
|
+
schedule,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parseAzureRestError: ErrorParser = (error) => {
|
|
131
|
+
if (error instanceof Error && error.name === "RestError") {
|
|
132
|
+
const restError = error as RestError;
|
|
133
|
+
const details = (restError.details ?? {}) as Record<string, string>;
|
|
134
|
+
const message: string = details["errorMessage"] ?? restError.message;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
errorMessage: `${details["errorCode"] ?? restError.name} (${
|
|
138
|
+
restError.code ?? restError.statusCode
|
|
139
|
+
}): ${message}`,
|
|
140
|
+
errorStatus: restError.statusCode,
|
|
141
|
+
errorType: "AzureRest",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// oxlint-disable-next-line no-useless-return
|
|
146
|
+
return;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function createAzureContextLogger(context: InvocationContext): LoggerAdapter {
|
|
150
|
+
return {
|
|
151
|
+
debug: context.debug.bind(context),
|
|
152
|
+
error: context.error.bind(context),
|
|
153
|
+
log: context.log.bind(context),
|
|
154
|
+
metadata: { name: "Azure Functions" },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Utils (@refer https://github.com/Marplex/hono-azurefunc-adapter/)
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
/** */
|
|
163
|
+
function newRequestFromAzureFunctions(request: HttpRequest): Request {
|
|
164
|
+
const hasBody = !["GET", "HEAD"].includes(request.method);
|
|
165
|
+
|
|
166
|
+
return new Request(request.url, {
|
|
167
|
+
headers: headersToObject(request.headers),
|
|
168
|
+
method: request.method,
|
|
169
|
+
...(hasBody ? { body: request.body as ReadableStream, duplex: "half" } : {}),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function newAzureFunctionsResponse(response: Response): HttpResponseInit {
|
|
174
|
+
let headers = headersToObject(response.headers);
|
|
175
|
+
let cookies = cookiesFromHeaders(response.headers);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
body: streamToAsyncIterator(response.body),
|
|
179
|
+
cookies,
|
|
180
|
+
headers,
|
|
181
|
+
status: response.status,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function headersToObject(input: HttpRequest["headers"]): Record<string, string> {
|
|
186
|
+
const headers: Record<string, string> = {};
|
|
187
|
+
// oxlint-disable-next-line no-array-for-each
|
|
188
|
+
input.forEach((value, key) => (headers[key] = value));
|
|
189
|
+
return headers;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cookiesFromHeaders(headers: Headers): Cookie[] | undefined {
|
|
193
|
+
const cookies = headers.getSetCookie();
|
|
194
|
+
if (cookies.length === 0) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return cookies.map((cookie) => parseCookieString(cookie));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseCookieString(cookieString: string): Cookie {
|
|
202
|
+
const [first, ...attributesArray] = cookieString
|
|
203
|
+
.split(";")
|
|
204
|
+
.map((item) => item.split("="))
|
|
205
|
+
.map(([key, value]) => [key?.trim().toLowerCase(), value ?? "true"]);
|
|
206
|
+
|
|
207
|
+
const [name, encodedValue] = first ?? [];
|
|
208
|
+
const attrs: Record<string, string> = Object.fromEntries(attributesArray);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
domain: attrs["domain"],
|
|
212
|
+
expires: attrs["expires"] ? new Date(attrs["expires"]) : undefined,
|
|
213
|
+
httpOnly: attrs["httponly"] === "true",
|
|
214
|
+
maxAge: attrs["max-age"] ? Number.parseInt(attrs["max-age"], 10) : undefined,
|
|
215
|
+
name: name ?? "",
|
|
216
|
+
path: attrs["path"],
|
|
217
|
+
sameSite: attrs["samesite"] as "Strict" | "Lax" | "None" | undefined,
|
|
218
|
+
secure: attrs["secure"] === "true",
|
|
219
|
+
value: encodedValue ? decodeURIComponent(encodedValue) : "",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function streamToAsyncIterator(
|
|
224
|
+
readable: Response["body"],
|
|
225
|
+
): AsyncIterableIterator<Uint8Array> | null {
|
|
226
|
+
if (readable === null || !readable) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const reader = readable.getReader();
|
|
231
|
+
return {
|
|
232
|
+
async next() {
|
|
233
|
+
return await reader.read();
|
|
234
|
+
},
|
|
235
|
+
return() {
|
|
236
|
+
reader.releaseLock();
|
|
237
|
+
},
|
|
238
|
+
[Symbol.asyncIterator]() {
|
|
239
|
+
return this;
|
|
240
|
+
},
|
|
241
|
+
} as AsyncIterableIterator<Uint8Array>;
|
|
242
|
+
}
|