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.
Files changed (215) hide show
  1. package/README.md +40 -18
  2. package/dist/adapters/_internal/queue.d.mts +127 -0
  3. package/dist/aws-dynamodb.d.mts +22 -0
  4. package/dist/aws-dynamodb.mjs +118 -0
  5. package/dist/aws-dynamodb.mjs.map +1 -0
  6. package/dist/aws-s3.d.mts +20 -0
  7. package/dist/aws-s3.mjs +96 -0
  8. package/dist/aws-s3.mjs.map +1 -0
  9. package/dist/azure-blob-storage.d.mts +20 -0
  10. package/dist/azure-blob-storage.mjs +126 -0
  11. package/dist/azure-blob-storage.mjs.map +1 -0
  12. package/dist/azure-cosmos-db.d.mts +23 -0
  13. package/dist/azure-cosmos-db.mjs +87 -0
  14. package/dist/azure-cosmos-db.mjs.map +1 -0
  15. package/dist/azure-data-tables.d.mts +23 -0
  16. package/dist/azure-data-tables.mjs +127 -0
  17. package/dist/azure-data-tables.mjs.map +1 -0
  18. package/dist/azure-easy-auth.d.mts +50 -0
  19. package/dist/azure-easy-auth.mjs +88 -0
  20. package/dist/azure-easy-auth.mjs.map +1 -0
  21. package/dist/azure-functions.d.mts +62 -0
  22. package/dist/azure-functions.mjs +147 -0
  23. package/dist/azure-functions.mjs.map +1 -0
  24. package/dist/fs.d.mts +37 -0
  25. package/dist/fs.mjs +240 -0
  26. package/dist/fs.mjs.map +1 -0
  27. package/dist/gcp-big-table.d.mts +23 -0
  28. package/dist/gcp-big-table.mjs +92 -0
  29. package/dist/gcp-big-table.mjs.map +1 -0
  30. package/dist/gcp-firestore.d.mts +22 -0
  31. package/dist/gcp-firestore.mjs +87 -0
  32. package/dist/gcp-firestore.mjs.map +1 -0
  33. package/dist/gcp-storage.d.mts +20 -0
  34. package/dist/gcp-storage.mjs +96 -0
  35. package/dist/gcp-storage.mjs.map +1 -0
  36. package/dist/handlers/handle-process-zip.mjs +90 -0
  37. package/dist/handlers/handle-process-zip.mjs.map +1 -0
  38. package/dist/handlers/handle-purge.d.mts +12 -0
  39. package/dist/handlers/handle-purge.mjs +36 -0
  40. package/dist/handlers/handle-purge.mjs.map +1 -0
  41. package/dist/handlers/handle-serve-storybook.mjs +94 -0
  42. package/dist/handlers/handle-serve-storybook.mjs.map +1 -0
  43. package/dist/index.d.mts +28 -0
  44. package/dist/index.mjs +62 -0
  45. package/dist/index.mjs.map +1 -0
  46. package/dist/models/builds-model.mjs +248 -0
  47. package/dist/models/builds-model.mjs.map +1 -0
  48. package/dist/models/builds-schema.d.mts +171 -0
  49. package/dist/models/builds-schema.mjs +67 -0
  50. package/dist/models/builds-schema.mjs.map +1 -0
  51. package/dist/models/projects-model.mjs +122 -0
  52. package/dist/models/projects-model.mjs.map +1 -0
  53. package/dist/models/projects-schema.d.mts +70 -0
  54. package/dist/models/projects-schema.mjs +37 -0
  55. package/dist/models/projects-schema.mjs.map +1 -0
  56. package/dist/models/tags-model.mjs +110 -0
  57. package/dist/models/tags-model.mjs.map +1 -0
  58. package/dist/models/tags-schema.d.mts +76 -0
  59. package/dist/models/tags-schema.mjs +34 -0
  60. package/dist/models/tags-schema.mjs.map +1 -0
  61. package/dist/models/~model.mjs +43 -0
  62. package/dist/models/~model.mjs.map +1 -0
  63. package/dist/models/~shared-schema.d.mts +1 -0
  64. package/dist/models/~shared-schema.mjs +20 -0
  65. package/dist/models/~shared-schema.mjs.map +1 -0
  66. package/dist/mysql.d.mts +39 -0
  67. package/dist/mysql.mjs +151 -0
  68. package/dist/mysql.mjs.map +1 -0
  69. package/dist/redis.d.mts +33 -0
  70. package/dist/redis.mjs +118 -0
  71. package/dist/redis.mjs.map +1 -0
  72. package/dist/routers/account-router.mjs +91 -0
  73. package/dist/routers/account-router.mjs.map +1 -0
  74. package/dist/routers/builds-router.mjs +347 -0
  75. package/dist/routers/builds-router.mjs.map +1 -0
  76. package/dist/routers/projects-router.mjs +236 -0
  77. package/dist/routers/projects-router.mjs.map +1 -0
  78. package/dist/routers/root-router.mjs +108 -0
  79. package/dist/routers/root-router.mjs.map +1 -0
  80. package/dist/routers/tags-router.mjs +269 -0
  81. package/dist/routers/tags-router.mjs.map +1 -0
  82. package/dist/routers/tasks-router.mjs +71 -0
  83. package/dist/routers/tasks-router.mjs.map +1 -0
  84. package/dist/urls.d.mts +47 -0
  85. package/dist/urls.mjs +208 -0
  86. package/dist/urls.mjs.map +1 -0
  87. package/dist/utils/adapter-utils.d.mts +14 -0
  88. package/dist/utils/adapter-utils.mjs +14 -0
  89. package/dist/utils/adapter-utils.mjs.map +1 -0
  90. package/dist/utils/auth.mjs +25 -0
  91. package/dist/utils/auth.mjs.map +1 -0
  92. package/dist/utils/error.d.mts +21 -0
  93. package/dist/utils/error.mjs +109 -0
  94. package/dist/utils/error.mjs.map +1 -0
  95. package/dist/utils/file-utils.mjs +16 -0
  96. package/dist/utils/file-utils.mjs.map +1 -0
  97. package/dist/utils/openapi-utils.mjs +45 -0
  98. package/dist/utils/openapi-utils.mjs.map +1 -0
  99. package/dist/utils/request.mjs +35 -0
  100. package/dist/utils/request.mjs.map +1 -0
  101. package/dist/utils/response.mjs +24 -0
  102. package/dist/utils/response.mjs.map +1 -0
  103. package/dist/utils/store.mjs +54 -0
  104. package/dist/utils/store.mjs.map +1 -0
  105. package/dist/utils/ui-utils.mjs +38 -0
  106. package/dist/utils/ui-utils.mjs.map +1 -0
  107. package/dist/utils/url-utils.d.mts +10 -0
  108. package/dist/utils/url-utils.mjs +54 -0
  109. package/dist/utils/url-utils.mjs.map +1 -0
  110. package/dist/~internal/adapter/auth.d.mts +123 -0
  111. package/dist/~internal/adapter/auth.mjs +20 -0
  112. package/dist/~internal/adapter/auth.mjs.map +1 -0
  113. package/dist/~internal/adapter/database.d.mts +240 -0
  114. package/dist/~internal/adapter/database.mjs +63 -0
  115. package/dist/~internal/adapter/database.mjs.map +1 -0
  116. package/dist/~internal/adapter/logger.d.mts +34 -0
  117. package/dist/~internal/adapter/logger.mjs +13 -0
  118. package/dist/~internal/adapter/logger.mjs.map +1 -0
  119. package/dist/~internal/adapter/storage.d.mts +208 -0
  120. package/dist/~internal/adapter/storage.mjs +63 -0
  121. package/dist/~internal/adapter/storage.mjs.map +1 -0
  122. package/dist/~internal/adapter/ui.d.mts +109 -0
  123. package/dist/~internal/adapter/ui.mjs +1 -0
  124. package/dist/~internal/adapter.d.mts +8 -0
  125. package/dist/~internal/adapter.mjs +6 -0
  126. package/dist/~internal/constants.d.mts +24 -0
  127. package/dist/~internal/constants.mjs +32 -0
  128. package/dist/~internal/constants.mjs.map +1 -0
  129. package/dist/~internal/mimes.d.mts +449 -0
  130. package/dist/~internal/mimes.mjs +454 -0
  131. package/dist/~internal/mimes.mjs.map +1 -0
  132. package/dist/~internal/router.d.mts +1651 -0
  133. package/dist/~internal/router.mjs +39 -0
  134. package/dist/~internal/router.mjs.map +1 -0
  135. package/dist/~internal/types.d.mts +77 -0
  136. package/dist/~internal/types.mjs +1 -0
  137. package/dist/~internal/utils.d.mts +4 -0
  138. package/dist/~internal/utils.mjs +5 -0
  139. package/openapi.json +3162 -0
  140. package/package.json +148 -27
  141. package/src/adapters/_internal/auth.ts +135 -0
  142. package/src/adapters/_internal/database.ts +241 -0
  143. package/src/adapters/_internal/index.ts +8 -0
  144. package/src/adapters/_internal/logger.ts +41 -0
  145. package/src/adapters/_internal/queue.ts +151 -0
  146. package/src/adapters/_internal/storage.ts +197 -0
  147. package/src/adapters/_internal/ui.ts +103 -0
  148. package/src/adapters/aws-dynamodb.ts +201 -0
  149. package/src/adapters/aws-s3.ts +160 -0
  150. package/src/adapters/azure-blob-storage.ts +223 -0
  151. package/src/adapters/azure-cosmos-db.ts +158 -0
  152. package/src/adapters/azure-data-tables.ts +223 -0
  153. package/src/adapters/azure-easy-auth.ts +174 -0
  154. package/src/adapters/azure-functions.ts +242 -0
  155. package/src/adapters/fs.ts +398 -0
  156. package/src/adapters/gcp-big-table.ts +157 -0
  157. package/src/adapters/gcp-firestore.ts +146 -0
  158. package/src/adapters/gcp-storage.ts +141 -0
  159. package/src/adapters/mysql.ts +296 -0
  160. package/src/adapters/redis.ts +242 -0
  161. package/src/handlers/handle-process-zip.ts +117 -0
  162. package/src/handlers/handle-purge.ts +65 -0
  163. package/src/handlers/handle-serve-storybook.ts +101 -0
  164. package/src/index.ts +81 -16
  165. package/src/mocks/mock-auth-service.ts +51 -0
  166. package/src/mocks/mock-store.ts +26 -0
  167. package/src/models/builds-model.ts +373 -0
  168. package/src/models/builds-schema.ts +84 -0
  169. package/src/models/projects-model.ts +177 -0
  170. package/src/models/projects-schema.ts +69 -0
  171. package/src/models/tags-model.ts +138 -0
  172. package/src/models/tags-schema.ts +45 -0
  173. package/src/models/~model.ts +79 -0
  174. package/src/models/~shared-schema.ts +14 -0
  175. package/src/routers/_app-router.ts +57 -0
  176. package/src/routers/account-router.ts +136 -0
  177. package/src/routers/builds-router.ts +464 -0
  178. package/src/routers/projects-router.ts +309 -0
  179. package/src/routers/root-router.ts +127 -0
  180. package/src/routers/tags-router.ts +339 -0
  181. package/src/routers/tasks-router.ts +75 -0
  182. package/src/types.ts +107 -0
  183. package/src/urls.ts +327 -0
  184. package/src/utils/adapter-utils.ts +26 -0
  185. package/src/utils/auth.test.ts +71 -0
  186. package/src/utils/auth.ts +39 -0
  187. package/src/utils/constants.ts +31 -0
  188. package/src/utils/date-utils.ts +10 -0
  189. package/src/utils/error.test.ts +86 -0
  190. package/src/utils/error.ts +140 -0
  191. package/src/utils/file-utils.test.ts +65 -0
  192. package/src/utils/file-utils.ts +43 -0
  193. package/src/utils/index.ts +3 -0
  194. package/src/utils/mime-utils.ts +457 -0
  195. package/src/utils/openapi-utils.ts +49 -0
  196. package/src/utils/request.ts +97 -0
  197. package/src/utils/response.ts +20 -0
  198. package/src/utils/store.ts +85 -0
  199. package/src/utils/story-utils.ts +42 -0
  200. package/src/utils/text-utils.ts +10 -0
  201. package/src/utils/ui-utils.ts +57 -0
  202. package/src/utils/url-utils.ts +113 -0
  203. package/dist/index.js +0 -554
  204. package/src/commands/create.ts +0 -263
  205. package/src/commands/purge.ts +0 -70
  206. package/src/commands/test.ts +0 -42
  207. package/src/service-schema.d.ts +0 -2023
  208. package/src/utils/auth-utils.ts +0 -31
  209. package/src/utils/pkg-utils.ts +0 -37
  210. package/src/utils/sb-build.ts +0 -55
  211. package/src/utils/sb-test.ts +0 -115
  212. package/src/utils/schema-utils.ts +0 -123
  213. package/src/utils/stream-utils.ts +0 -72
  214. package/src/utils/types.ts +0 -4
  215. 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
+ }