@withstudiocms/sdk 0.0.0-beta.0 → 0.1.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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/cache.d.ts +109 -0
- package/dist/cache.js +94 -0
- package/dist/consts.d.ts +37 -0
- package/dist/consts.js +35 -0
- package/dist/context.d.ts +208 -0
- package/dist/context.js +40 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +1024 -0
- package/dist/index.js +24 -0
- package/dist/lib/diff.d.ts +39 -0
- package/dist/lib/diff.js +29 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.js +131 -0
- package/dist/lib/pluginUtils.d.ts +222 -0
- package/dist/lib/pluginUtils.js +87 -0
- package/dist/lib/storage-manager.d.ts +10 -0
- package/dist/lib/storage-manager.js +17 -0
- package/dist/migrations/20251025T040912_init.d.ts +17 -0
- package/dist/migrations/20251025T040912_init.js +260 -0
- package/dist/migrations/20251130T150847_drop_deprecated.d.ts +13 -0
- package/dist/migrations/20251130T150847_drop_deprecated.js +262 -0
- package/dist/migrations/20251221T002125_url-mapping.d.ts +13 -0
- package/dist/migrations/20251221T002125_url-mapping.js +228 -0
- package/dist/migrator.d.ts +25 -0
- package/dist/migrator.js +21 -0
- package/dist/modules/auth/index.d.ts +419 -0
- package/dist/modules/auth/index.js +436 -0
- package/dist/modules/clear/index.d.ts +72 -0
- package/dist/modules/clear/index.js +52 -0
- package/dist/modules/config/consts.d.ts +32 -0
- package/dist/modules/config/consts.js +18 -0
- package/dist/modules/config/index.d.ts +100 -0
- package/dist/modules/config/index.js +224 -0
- package/dist/modules/config/templates/mailer.d.ts +36 -0
- package/dist/modules/config/templates/mailer.js +218 -0
- package/dist/modules/config/type-utils.d.ts +13 -0
- package/dist/modules/config/type-utils.js +11 -0
- package/dist/modules/delete/index.d.ts +141 -0
- package/dist/modules/delete/index.js +279 -0
- package/dist/modules/diffTracking/index.d.ts +188 -0
- package/dist/modules/diffTracking/index.js +277 -0
- package/dist/modules/get/index.d.ts +372 -0
- package/dist/modules/get/index.js +579 -0
- package/dist/modules/index.d.ts +883 -0
- package/dist/modules/index.js +37 -0
- package/dist/modules/init/index.d.ts +60 -0
- package/dist/modules/init/index.js +38 -0
- package/dist/modules/middleware/index.d.ts +56 -0
- package/dist/modules/middleware/index.js +50 -0
- package/dist/modules/notificationSettings/index.d.ts +57 -0
- package/dist/modules/notificationSettings/index.js +39 -0
- package/dist/modules/plugins/index.d.ts +167 -0
- package/dist/modules/plugins/index.js +272 -0
- package/dist/modules/post/index.d.ts +306 -0
- package/dist/modules/post/index.js +337 -0
- package/dist/modules/resetTokenBucket/index.d.ts +91 -0
- package/dist/modules/resetTokenBucket/index.js +96 -0
- package/dist/modules/rest_api/index.d.ts +92 -0
- package/dist/modules/rest_api/index.js +117 -0
- package/dist/modules/update/index.d.ts +184 -0
- package/dist/modules/update/index.js +192 -0
- package/dist/modules/util/collectors.d.ts +125 -0
- package/dist/modules/util/collectors.js +168 -0
- package/dist/modules/util/folderTree.d.ts +100 -0
- package/dist/modules/util/folderTree.js +176 -0
- package/dist/modules/util/generators.d.ts +83 -0
- package/dist/modules/util/generators.js +106 -0
- package/dist/modules/util/getFromNPM.d.ts +199 -0
- package/dist/modules/util/getFromNPM.js +106 -0
- package/dist/modules/util/index.d.ts +100 -0
- package/dist/modules/util/index.js +20 -0
- package/dist/modules/util/parsers.d.ts +60 -0
- package/dist/modules/util/parsers.js +43 -0
- package/dist/modules/util/slugify.d.ts +22 -0
- package/dist/modules/util/slugify.js +19 -0
- package/dist/modules/util/users.d.ts +99 -0
- package/dist/modules/util/users.js +78 -0
- package/dist/tables.d.ts +433 -0
- package/dist/tables.js +169 -0
- package/dist/types.d.ts +359 -0
- package/dist/types.js +10 -0
- package/package.json +67 -7
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from "@withstudiocms/effect";
|
|
2
|
+
import { DBClientLive, StorageManagerResolver } from "../../context.js";
|
|
3
|
+
import { resolveStorageManagerUrls } from "../../lib/storage-manager.js";
|
|
4
|
+
import {
|
|
5
|
+
StudioCMSOAuthAccounts,
|
|
6
|
+
StudioCMSPageContent,
|
|
7
|
+
StudioCMSPageDataCategories,
|
|
8
|
+
StudioCMSPageDataTags,
|
|
9
|
+
StudioCMSPermissions,
|
|
10
|
+
StudioCMSUsersTable
|
|
11
|
+
} from "../../tables.js";
|
|
12
|
+
import { SDKFolderTree } from "./folderTree.js";
|
|
13
|
+
import { SDKParsers } from "./parsers.js";
|
|
14
|
+
import { slugify } from "./slugify.js";
|
|
15
|
+
class CollectorError extends Data.TaggedError("CollectorError") {
|
|
16
|
+
}
|
|
17
|
+
const useCollectorError = (_try) => Effect.try({
|
|
18
|
+
try: _try,
|
|
19
|
+
catch: (error) => new CollectorError({ cause: error })
|
|
20
|
+
});
|
|
21
|
+
const SDKCollectors = Effect.gen(function* () {
|
|
22
|
+
const [
|
|
23
|
+
{ withCodec },
|
|
24
|
+
{ findNodesAlongPathToId },
|
|
25
|
+
{ parseIdNumberArray, parseIdStringArray },
|
|
26
|
+
smResolver
|
|
27
|
+
] = yield* Effect.all([DBClientLive, SDKFolderTree, SDKParsers, StorageManagerResolver]);
|
|
28
|
+
const resolveUrls = resolveStorageManagerUrls(smResolver);
|
|
29
|
+
const _getUserData = withCodec({
|
|
30
|
+
encoder: Schema.String,
|
|
31
|
+
decoder: Schema.UndefinedOr(StudioCMSUsersTable.Select),
|
|
32
|
+
callbackFn: (query, id) => query(
|
|
33
|
+
(db) => db.selectFrom("StudioCMSUsersTable").selectAll().where("id", "=", id).executeTakeFirst()
|
|
34
|
+
)
|
|
35
|
+
});
|
|
36
|
+
const _getPageContent = withCodec({
|
|
37
|
+
encoder: Schema.String,
|
|
38
|
+
decoder: Schema.Array(StudioCMSPageContent.Select),
|
|
39
|
+
callbackFn: (query, id) => query(
|
|
40
|
+
(db) => db.selectFrom("StudioCMSPageContent").selectAll().where("contentId", "=", id).execute()
|
|
41
|
+
)
|
|
42
|
+
});
|
|
43
|
+
const _getOAuthAccountData = withCodec({
|
|
44
|
+
encoder: Schema.String,
|
|
45
|
+
decoder: Schema.Array(StudioCMSOAuthAccounts.Select),
|
|
46
|
+
callbackFn: (query, id) => query(
|
|
47
|
+
(db) => db.selectFrom("StudioCMSOAuthAccounts").selectAll().where("userId", "=", id).execute()
|
|
48
|
+
)
|
|
49
|
+
});
|
|
50
|
+
const _getUserPermissionsData = withCodec({
|
|
51
|
+
encoder: Schema.String,
|
|
52
|
+
decoder: Schema.UndefinedOr(StudioCMSPermissions.Select),
|
|
53
|
+
callbackFn: (query, id) => query(
|
|
54
|
+
(db) => db.selectFrom("StudioCMSPermissions").selectAll().where("user", "=", id).executeTakeFirst()
|
|
55
|
+
)
|
|
56
|
+
});
|
|
57
|
+
const _transformPageDataToMetaOnly = (data) => useCollectorError(() => {
|
|
58
|
+
if (Array.isArray(data)) {
|
|
59
|
+
return data.map(
|
|
60
|
+
({ defaultContent, multiLangContent, ...rest2 }) => rest2
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const {
|
|
64
|
+
defaultContent: _dump1,
|
|
65
|
+
multiLangContent: _dump2,
|
|
66
|
+
...rest
|
|
67
|
+
} = data;
|
|
68
|
+
return rest;
|
|
69
|
+
});
|
|
70
|
+
const _collectContributorData = Effect.fn(
|
|
71
|
+
(ids) => Effect.all(ids.map((id) => _getUserData(id))).pipe(
|
|
72
|
+
Effect.map((results) => results.filter((user) => !!user))
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
const collectCategories = withCodec({
|
|
76
|
+
encoder: Schema.Array(Schema.Number),
|
|
77
|
+
decoder: Schema.Array(StudioCMSPageDataCategories.Select),
|
|
78
|
+
callbackFn: (db, ids) => db(
|
|
79
|
+
(c) => c.selectFrom("StudioCMSPageDataCategories").selectAll().where("id", "in", ids).execute()
|
|
80
|
+
)
|
|
81
|
+
});
|
|
82
|
+
const collectTags = withCodec({
|
|
83
|
+
encoder: Schema.Array(Schema.Number),
|
|
84
|
+
decoder: Schema.Array(StudioCMSPageDataTags.Select),
|
|
85
|
+
callbackFn: (db, ids) => db((c) => c.selectFrom("StudioCMSPageDataTags").selectAll().where("id", "in", ids).execute())
|
|
86
|
+
});
|
|
87
|
+
function collectPageData(page, tree, metaOnly = false) {
|
|
88
|
+
return Effect.gen(function* () {
|
|
89
|
+
const [categories, tags, contributorsData, authorData] = yield* Effect.all([
|
|
90
|
+
parseIdNumberArray(page.categories || []).pipe(Effect.flatMap(collectCategories)),
|
|
91
|
+
parseIdNumberArray(page.tags || []).pipe(Effect.flatMap(collectTags)),
|
|
92
|
+
parseIdStringArray(page.contributorIds || []).pipe(Effect.flatMap(_collectContributorData)),
|
|
93
|
+
_getUserData(page.authorId)
|
|
94
|
+
]);
|
|
95
|
+
let multiLangContent = [];
|
|
96
|
+
if (!metaOnly) {
|
|
97
|
+
multiLangContent = yield* _getPageContent(page.id);
|
|
98
|
+
}
|
|
99
|
+
const defaultContent = multiLangContent?.find(
|
|
100
|
+
(content) => content.contentLang === page.contentLang
|
|
101
|
+
);
|
|
102
|
+
const safeSlug = page.slug === "index" ? "/" : slugify(page.slug);
|
|
103
|
+
let urlRoute = safeSlug.startsWith("/") ? safeSlug : `/${safeSlug}`;
|
|
104
|
+
if (page.parentFolder) {
|
|
105
|
+
const urlParts = yield* findNodesAlongPathToId(tree, page.parentFolder);
|
|
106
|
+
const folderPath = urlParts.map(({ name }) => slugify(name)).join("/");
|
|
107
|
+
urlRoute = folderPath.length > 0 ? `/${folderPath}${safeSlug === "/" ? "" : `/${safeSlug}`}` : safeSlug;
|
|
108
|
+
}
|
|
109
|
+
let authorDataTyped;
|
|
110
|
+
if (authorData) {
|
|
111
|
+
const { email, password, ...rest } = authorData;
|
|
112
|
+
authorDataTyped = {
|
|
113
|
+
...rest
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
let contributorsDataTyped;
|
|
117
|
+
if (contributorsData) {
|
|
118
|
+
contributorsDataTyped = contributorsData.map(({ email, password, ...rest }) => ({
|
|
119
|
+
...rest
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
const returnData = yield* resolveUrls(
|
|
123
|
+
{
|
|
124
|
+
...page,
|
|
125
|
+
urlRoute,
|
|
126
|
+
categories,
|
|
127
|
+
tags,
|
|
128
|
+
authorData: authorDataTyped,
|
|
129
|
+
contributorsData: contributorsDataTyped,
|
|
130
|
+
multiLangContent,
|
|
131
|
+
defaultContent
|
|
132
|
+
},
|
|
133
|
+
["heroImage"]
|
|
134
|
+
).pipe(Effect.catchTag("UnknownException", (e) => new CollectorError({ cause: e })));
|
|
135
|
+
if (!returnData) {
|
|
136
|
+
return yield* new CollectorError({
|
|
137
|
+
cause: "Unknown error occurred while resolving storage manager URL"
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (metaOnly) {
|
|
141
|
+
return yield* _transformPageDataToMetaOnly(returnData);
|
|
142
|
+
}
|
|
143
|
+
return returnData;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const collectUserData = Effect.fn(
|
|
147
|
+
(user) => Effect.all([_getOAuthAccountData(user.id), _getUserPermissionsData(user.id)]).pipe(
|
|
148
|
+
Effect.map(
|
|
149
|
+
([oAuthData, permissionsData]) => ({
|
|
150
|
+
...user,
|
|
151
|
+
oAuthData,
|
|
152
|
+
permissionsData
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
return {
|
|
158
|
+
collectCategories,
|
|
159
|
+
collectTags,
|
|
160
|
+
collectPageData,
|
|
161
|
+
collectUserData
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
export {
|
|
165
|
+
CollectorError,
|
|
166
|
+
SDKCollectors,
|
|
167
|
+
useCollectorError
|
|
168
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Effect } from '@withstudiocms/effect';
|
|
2
|
+
import { DBClientLive } from '../../context.js';
|
|
3
|
+
import type { FolderListItem, FolderNode } from '../../types.js';
|
|
4
|
+
declare const FolderTreeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
5
|
+
readonly _tag: "FolderTreeError";
|
|
6
|
+
} & Readonly<A>;
|
|
7
|
+
/**
|
|
8
|
+
* Custom error class for folder tree related errors.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FolderTreeError extends FolderTreeError_base<{
|
|
11
|
+
cause: unknown;
|
|
12
|
+
}> {
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Utility function to wrap operations with FolderTreeError handling.
|
|
16
|
+
*
|
|
17
|
+
* @param _try - A function that performs the operation to be wrapped.
|
|
18
|
+
* @returns An Effect that either yields the result of the operation or a FolderTreeError.
|
|
19
|
+
*/
|
|
20
|
+
export declare const useFolderTreeError: <T>(_try: () => T) => Effect.Effect<T, FolderTreeError, never>;
|
|
21
|
+
/**
|
|
22
|
+
* SDKFolderTree
|
|
23
|
+
*
|
|
24
|
+
* An Effect.Gen that provides utilities for constructing and querying an in-memory folder tree
|
|
25
|
+
* representation derived from the StudioCMSPageFolderStructure table.
|
|
26
|
+
*
|
|
27
|
+
* This effect:
|
|
28
|
+
* - Fetches the current folder records from the database using DBClientLive and a typed decoder.
|
|
29
|
+
* - Exposes pure and effectful helper functions for building a hierarchical tree, searching nodes
|
|
30
|
+
* by path or id, and adding page nodes to the tree.
|
|
31
|
+
*
|
|
32
|
+
* Exposed functions and behavior:
|
|
33
|
+
* - generateFolderTree(folders: readonly tsPageFolderSelect[]): FolderNode[]
|
|
34
|
+
* Builds a hierarchical FolderNode[] from a flat list of folder records. Each FolderNode
|
|
35
|
+
* contains id, name, pageData, page (boolean), and children (FolderNode[]).
|
|
36
|
+
*
|
|
37
|
+
* - getFullPath(tree: FolderNode[], path: string[]): string[]
|
|
38
|
+
* Returns the sequence of folder names from the root down to the specified path of folder names.
|
|
39
|
+
* If the supplied path is not found, returns the longest matched prefix (or an empty array).
|
|
40
|
+
*
|
|
41
|
+
* - findNodeByPath(tree: FolderNode[], path: string[]): FolderNode | null
|
|
42
|
+
* Locates and returns the FolderNode that exactly matches the supplied path of folder names.
|
|
43
|
+
* Returns null if the full path does not exist.
|
|
44
|
+
*
|
|
45
|
+
* - findNodesAlongPath(tree: FolderNode[], path: string[]): FolderNode[]
|
|
46
|
+
* Returns the array of FolderNode objects encountered along the specified path of names.
|
|
47
|
+
* Useful for obtaining each node in the chain (root → ... → target). Returns an empty array
|
|
48
|
+
* when no match is found.
|
|
49
|
+
*
|
|
50
|
+
* - findNodesAlongPathToId(tree: FolderNode[], id: string): FolderNode[]
|
|
51
|
+
* Searches the tree for a node by id and returns all FolderNode objects along the path from
|
|
52
|
+
* the root to that node. If the id is not found, an empty array is returned.
|
|
53
|
+
*
|
|
54
|
+
* - findNodeById(tree: FolderNode[], id: string): FolderNode | null
|
|
55
|
+
* Recursively searches and returns the FolderNode with the matching id, or null when not found.
|
|
56
|
+
*
|
|
57
|
+
* - addPageToFolderTree(tree: FolderNode[], folderId: string, newPage: FolderNode): FolderNode[]
|
|
58
|
+
* Effectful helper that inserts a new page node under the folder with folderId. If no folder
|
|
59
|
+
* with folderId exists, the newPage is appended at the root level. Returns the updated tree.
|
|
60
|
+
*
|
|
61
|
+
* - buildFolderTree: Effect<unknown, Error, FolderNode[]>
|
|
62
|
+
* Effect that fetches current folders from the DB and maps them to a hierarchical tree using
|
|
63
|
+
* generateFolderTree.
|
|
64
|
+
*
|
|
65
|
+
* - getAvailableFolders: Effect<unknown, Error, FolderListItem[]>
|
|
66
|
+
* Effect that fetches current folders from the DB and returns a light-weight list of folder
|
|
67
|
+
* descriptors { id, name, parent } for UI lists or selection controls.
|
|
68
|
+
*
|
|
69
|
+
* Notes and guarantees:
|
|
70
|
+
* - All helpers wrap their logic in a folder-tree specific error boundary via useFolderTreeError,
|
|
71
|
+
* so callers receive consistent errors for tree operations.
|
|
72
|
+
* - The internal representation assumes unique folder ids and uses the parent id to assemble
|
|
73
|
+
* the hierarchy; folders with a null/undefined parent are treated as root nodes.
|
|
74
|
+
* - The effect depends on DBClientLive and a decoder for StudioCMSPageFolderStructure to ensure
|
|
75
|
+
* type-safe database reads.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* Intended for use by SDK consumers that need to present, navigate, or mutate a folder hierarchy
|
|
79
|
+
* derived from persistent storage. Consumers should treat returned FolderNode[] as a mutable
|
|
80
|
+
* structure when using addPageToFolderTree; other helpers are pure and deterministic.
|
|
81
|
+
*
|
|
82
|
+
* @returns An Effect that yields an object exposing the functions described above for building
|
|
83
|
+
* and querying folder trees.
|
|
84
|
+
*/
|
|
85
|
+
export declare const SDKFolderTree: Effect.Effect<{
|
|
86
|
+
generateFolderTree: (folders: readonly {
|
|
87
|
+
readonly id: string;
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly parent?: string | null | undefined;
|
|
90
|
+
}[]) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
91
|
+
getFullPath: (tree: FolderNode[], path: string[]) => Effect.Effect<string[], FolderTreeError, never>;
|
|
92
|
+
findNodeByPath: (tree: FolderNode[], path: string[]) => Effect.Effect<FolderNode | null, FolderTreeError, never>;
|
|
93
|
+
findNodesAlongPath: (tree: FolderNode[], path: string[]) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
94
|
+
findNodesAlongPathToId: (tree: FolderNode[], id: string) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
95
|
+
findNodeById: (tree: FolderNode[], id: string) => Effect.Effect<FolderNode | null, FolderTreeError, never>;
|
|
96
|
+
addPageToFolderTree: (tree: FolderNode[], folderId: string, newPage: FolderNode) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
97
|
+
buildFolderTree: Effect.Effect<FolderNode[], import("@withstudiocms/kysely/client").DBCallbackFailure | import("@withstudiocms/kysely/core/errors").DatabaseError | FolderTreeError, never>;
|
|
98
|
+
getAvailableFolders: Effect.Effect<FolderListItem[], import("@withstudiocms/kysely/client").DBCallbackFailure | import("@withstudiocms/kysely/core/errors").DatabaseError, never>;
|
|
99
|
+
}, never, DBClientLive>;
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from "@withstudiocms/effect";
|
|
2
|
+
import { DBClientLive } from "../../context.js";
|
|
3
|
+
import { StudioCMSPageFolderStructure } from "../../tables.js";
|
|
4
|
+
class FolderTreeError extends Data.TaggedError("FolderTreeError") {
|
|
5
|
+
}
|
|
6
|
+
const useFolderTreeError = (_try) => Effect.try({
|
|
7
|
+
try: _try,
|
|
8
|
+
catch: (error) => new FolderTreeError({ cause: error })
|
|
9
|
+
});
|
|
10
|
+
const SDKFolderTree = Effect.gen(function* () {
|
|
11
|
+
const { withDecoder } = yield* DBClientLive;
|
|
12
|
+
const _getCurrentFolders = withDecoder({
|
|
13
|
+
decoder: Schema.Array(StudioCMSPageFolderStructure.Select),
|
|
14
|
+
callbackFn: (query) => query((db) => db.selectFrom("StudioCMSPageFolderStructure").selectAll().execute())
|
|
15
|
+
});
|
|
16
|
+
const generateFolderTree = Effect.fn(
|
|
17
|
+
(folders) => useFolderTreeError(() => {
|
|
18
|
+
const folderMap = {};
|
|
19
|
+
for (const folder of folders) {
|
|
20
|
+
folderMap[folder.id] = {
|
|
21
|
+
id: folder.id,
|
|
22
|
+
name: folder.name,
|
|
23
|
+
pageData: null,
|
|
24
|
+
page: false,
|
|
25
|
+
children: []
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const rootFolders = [];
|
|
29
|
+
for (const folder of folders) {
|
|
30
|
+
const childFolder = folderMap[folder.id];
|
|
31
|
+
if (!childFolder) continue;
|
|
32
|
+
if (folder.parent === null || folder.parent === void 0) {
|
|
33
|
+
rootFolders.push(childFolder);
|
|
34
|
+
} else {
|
|
35
|
+
const parentFolder = folderMap[folder.parent];
|
|
36
|
+
if (parentFolder) {
|
|
37
|
+
parentFolder.children.push(childFolder);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return rootFolders;
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
const getFullPath = Effect.fn(
|
|
45
|
+
(tree, path) => useFolderTreeError(() => {
|
|
46
|
+
const result = [];
|
|
47
|
+
function helper(nodes, pathParts) {
|
|
48
|
+
if (pathParts.length === 0) return false;
|
|
49
|
+
const [current, ...rest] = pathParts;
|
|
50
|
+
for (const node of nodes) {
|
|
51
|
+
if (node.name === current) {
|
|
52
|
+
result.push(node.name);
|
|
53
|
+
if (rest.length === 0 || helper(node.children, rest)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
result.pop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
helper(tree, path);
|
|
62
|
+
return result;
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
const findNodeByPath = Effect.fn(
|
|
66
|
+
(tree, path) => useFolderTreeError(() => {
|
|
67
|
+
function _findNodeByPath(tree2, path2) {
|
|
68
|
+
if (path2.length === 0) return null;
|
|
69
|
+
const [current, ...rest] = path2;
|
|
70
|
+
for (const node of tree2) {
|
|
71
|
+
if (node.name === current) {
|
|
72
|
+
if (rest.length === 0) return node;
|
|
73
|
+
return _findNodeByPath(node.children, rest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return _findNodeByPath(tree, path);
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
const findNodesAlongPath = Effect.fn(
|
|
82
|
+
(tree, path) => useFolderTreeError(() => {
|
|
83
|
+
const result = [];
|
|
84
|
+
function helper(nodes, pathParts) {
|
|
85
|
+
if (pathParts.length === 0) return false;
|
|
86
|
+
const [current, ...rest] = pathParts;
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
if (node.name === current) {
|
|
89
|
+
result.push(node);
|
|
90
|
+
if (rest.length === 0 || helper(node.children, rest)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
result.pop();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
helper(tree, path);
|
|
99
|
+
return result;
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
const findNodesAlongPathToId = Effect.fn(
|
|
103
|
+
(tree, id) => useFolderTreeError(() => {
|
|
104
|
+
const path = [];
|
|
105
|
+
function helper(nodes, targetId) {
|
|
106
|
+
for (const node of nodes) {
|
|
107
|
+
path.push(node);
|
|
108
|
+
if (node.id === targetId) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (helper(node.children, targetId)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
path.pop();
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
helper(tree, id);
|
|
119
|
+
return path;
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
const findNodeById = Effect.fn(
|
|
123
|
+
(tree, id) => useFolderTreeError(() => {
|
|
124
|
+
function _findNodeById(tree2, id2) {
|
|
125
|
+
for (const node of tree2) {
|
|
126
|
+
if (node.id === id2) {
|
|
127
|
+
return node;
|
|
128
|
+
}
|
|
129
|
+
const found = _findNodeById(node.children, id2);
|
|
130
|
+
if (found) {
|
|
131
|
+
return found;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return _findNodeById(tree, id);
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
const addPageToFolderTree = Effect.fn(function* (tree, folderId, newPage) {
|
|
140
|
+
const parentFolder = yield* findNodeById(tree, folderId);
|
|
141
|
+
if (!parentFolder) {
|
|
142
|
+
tree.push(newPage);
|
|
143
|
+
return tree;
|
|
144
|
+
}
|
|
145
|
+
parentFolder.children.push(newPage);
|
|
146
|
+
return tree;
|
|
147
|
+
});
|
|
148
|
+
const buildFolderTree = _getCurrentFolders().pipe(Effect.flatMap(generateFolderTree));
|
|
149
|
+
const getAvailableFolders = _getCurrentFolders().pipe(
|
|
150
|
+
Effect.map(
|
|
151
|
+
(folders) => folders.map(
|
|
152
|
+
(folder) => ({
|
|
153
|
+
id: folder.id,
|
|
154
|
+
name: folder.name,
|
|
155
|
+
parent: folder.parent
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
generateFolderTree,
|
|
162
|
+
getFullPath,
|
|
163
|
+
findNodeByPath,
|
|
164
|
+
findNodesAlongPath,
|
|
165
|
+
findNodesAlongPathToId,
|
|
166
|
+
findNodeById,
|
|
167
|
+
addPageToFolderTree,
|
|
168
|
+
buildFolderTree,
|
|
169
|
+
getAvailableFolders
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
export {
|
|
173
|
+
FolderTreeError,
|
|
174
|
+
SDKFolderTree,
|
|
175
|
+
useFolderTreeError
|
|
176
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Effect } from '@withstudiocms/effect';
|
|
2
|
+
import type { JwtVerificationResult } from '../../types.js';
|
|
3
|
+
declare const GeneratorError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
4
|
+
readonly _tag: "GeneratorError";
|
|
5
|
+
} & Readonly<A>;
|
|
6
|
+
/**
|
|
7
|
+
* Represents errors that occur during generator operations.
|
|
8
|
+
*/
|
|
9
|
+
export declare class GeneratorError extends GeneratorError_base<{
|
|
10
|
+
cause: unknown;
|
|
11
|
+
}> {
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A helper function to wrap generator operations in an Effect with error handling.
|
|
15
|
+
*
|
|
16
|
+
* @param _try - A function that performs the generator operation.
|
|
17
|
+
* @returns An Effect that either yields the result of the operation or fails with a GeneratorError.
|
|
18
|
+
*/
|
|
19
|
+
export declare const useGeneratorError: <A>(_try: () => A) => Effect.Effect<A, GeneratorError, never>;
|
|
20
|
+
/**
|
|
21
|
+
* SDKGenerators
|
|
22
|
+
*
|
|
23
|
+
* Effect generator that builds a collection of utility functions used across the SDK.
|
|
24
|
+
*
|
|
25
|
+
* This generator:
|
|
26
|
+
* - Reads the CMS encryption key from configuration (CMS_ENCRYPTION_KEY). The key's value is kept redacted for
|
|
27
|
+
* safety in logs and debug output; the generator uses the redacted value for cryptographic operations where needed.
|
|
28
|
+
* - Exposes utilities for creating random IDs, secure passwords, issuing JWTs for users, and verifying JWTs.
|
|
29
|
+
*
|
|
30
|
+
* Exposed utilities:
|
|
31
|
+
* - generateRandomIDNumber(length: number): number
|
|
32
|
+
* - Generates a numeric identifier of the specified length.
|
|
33
|
+
* - Uses Math.random() and therefore is suitable for non-cryptographic identifiers.
|
|
34
|
+
*
|
|
35
|
+
* - generateRandomPassword(length: number): string
|
|
36
|
+
* - Produces a password of the requested length using the character set
|
|
37
|
+
* "A-Z a-z 0-9".
|
|
38
|
+
* - Uses crypto.getRandomValues and rejection sampling to avoid modulo bias, providing cryptographically
|
|
39
|
+
* strong random selection from the character set.
|
|
40
|
+
*
|
|
41
|
+
* - generateToken(userId: string, noExpire?: boolean): string
|
|
42
|
+
* - Creates a signed JSON Web Token for the supplied userId.
|
|
43
|
+
* - By default the token is time-limited (expires in a short timeframe — implementation uses a 3 hour default).
|
|
44
|
+
* Passing `noExpire = true` will produce a token without the automatic expiry.
|
|
45
|
+
* - Token signing uses the CMS encryption key (redacted) via the SDK's internal JWT generation routine.
|
|
46
|
+
*
|
|
47
|
+
* - testToken(token: string): Effect.Effect<JwtVerificationResult, GeneratorError, never>
|
|
48
|
+
* - Validates the provided JWT using the CMS encryption key.
|
|
49
|
+
* - Verification performs:
|
|
50
|
+
* - header algorithm check (expects HS256),
|
|
51
|
+
* - expiration check (tokens with exp in the past are rejected),
|
|
52
|
+
* - signature verification (HMAC SHA-256 using the configured secret).
|
|
53
|
+
* - Returns a JwtVerificationResult indicating whether the token is valid and, if so, the userId extracted from the token.
|
|
54
|
+
*
|
|
55
|
+
* Error handling and effects:
|
|
56
|
+
* - Many operations are wrapped with the generator's error handling helpers and may fail with GeneratorError when
|
|
57
|
+
* JSON parsing, cryptographic operations, or other wrapped computations error.
|
|
58
|
+
* - Sensitive values (CMS encryption key) are handled in redacted form to avoid accidental exposure.
|
|
59
|
+
*
|
|
60
|
+
* Security notes:
|
|
61
|
+
* - generateRandomPassword uses cryptographically secure randomness; generateRandomIDNumber does not and should not
|
|
62
|
+
* be used where cryptographic unpredictability is required.
|
|
63
|
+
* - Tokens are signed and validated using HMAC-SHA256 (HS256). Ensure the configured CMS_ENCRYPTION_KEY is strong and
|
|
64
|
+
* kept secret.
|
|
65
|
+
*
|
|
66
|
+
* Returns:
|
|
67
|
+
* - An object containing the four utilities: { generateRandomIDNumber, generateRandomPassword, generateToken, testToken }.
|
|
68
|
+
*
|
|
69
|
+
* Example:
|
|
70
|
+
* @example
|
|
71
|
+
* const sdk = yield* SDKGenerators;
|
|
72
|
+
* const token = yield* sdk.generateToken('user-123');
|
|
73
|
+
* const result = yield* sdk.testToken(token);
|
|
74
|
+
*
|
|
75
|
+
* @throws {GeneratorError} if any underlying generator-wrapped operation (parsing, crypto, config retrieval) fails.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SDKGenerators: Effect.Effect<{
|
|
78
|
+
generateRandomIDNumber: (length: number) => Effect.Effect<number, GeneratorError, never>;
|
|
79
|
+
generateRandomPassword: (length: number) => Effect.Effect<string, GeneratorError, never>;
|
|
80
|
+
generateToken: (userId: string, noExpire?: boolean | undefined) => Effect.Effect<string, GeneratorError, never>;
|
|
81
|
+
testToken: (token: string) => Effect.Effect<JwtVerificationResult, GeneratorError, never>;
|
|
82
|
+
}, import("effect/ConfigError").ConfigError, never>;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import { Config, Data, Effect, Redacted } from "@withstudiocms/effect";
|
|
3
|
+
class GeneratorError extends Data.TaggedError("GeneratorError") {
|
|
4
|
+
}
|
|
5
|
+
const useGeneratorError = (_try) => Effect.try({
|
|
6
|
+
try: _try,
|
|
7
|
+
catch: (error) => new GeneratorError({ cause: error })
|
|
8
|
+
});
|
|
9
|
+
const SDKGenerators = Effect.gen(function* () {
|
|
10
|
+
const redactedCMSEncryptionKey = yield* Config.redacted("CMS_ENCRYPTION_KEY");
|
|
11
|
+
const cmsEncryptionKey = Redacted.value(redactedCMSEncryptionKey);
|
|
12
|
+
const base64UrlEncode = (input) => Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13
|
+
const base64UrlDecode = (input) => {
|
|
14
|
+
let newInput = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
15
|
+
while (newInput.length % 4 !== 0) {
|
|
16
|
+
newInput += "=";
|
|
17
|
+
}
|
|
18
|
+
return Buffer.from(newInput, "base64").toString();
|
|
19
|
+
};
|
|
20
|
+
const generateJwt = Effect.fn(
|
|
21
|
+
(secret, payload, noExpire) => useGeneratorError(() => {
|
|
22
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
23
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
24
|
+
const thirtyYearsFromToday = Math.floor(
|
|
25
|
+
currentDate.setFullYear(currentDate.getFullYear() + 30) / 1e3
|
|
26
|
+
);
|
|
27
|
+
const exp = noExpire ? thirtyYearsFromToday : Math.floor(Date.now() / 1e3) + 86400;
|
|
28
|
+
const payloadObj = {
|
|
29
|
+
...payload,
|
|
30
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
31
|
+
// Corrected iat
|
|
32
|
+
exp
|
|
33
|
+
};
|
|
34
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
35
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payloadObj));
|
|
36
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
37
|
+
const signature = Buffer.from(
|
|
38
|
+
crypto.createHmac("sha256", secret + secret).update(signatureInput).digest()
|
|
39
|
+
).toString("base64url");
|
|
40
|
+
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
const verifyJwt = (token, secret) => Effect.gen(function* () {
|
|
44
|
+
const [encodedHeader, encodedPayload, encodedSignature] = token.split(".");
|
|
45
|
+
if (!encodedHeader || !encodedPayload || !encodedSignature) {
|
|
46
|
+
yield* Effect.logDebug("Invalid token format");
|
|
47
|
+
return { isValid: false };
|
|
48
|
+
}
|
|
49
|
+
const [header, payload] = yield* Effect.all([
|
|
50
|
+
useGeneratorError(() => JSON.parse(base64UrlDecode(encodedHeader))),
|
|
51
|
+
useGeneratorError(() => JSON.parse(base64UrlDecode(encodedPayload)))
|
|
52
|
+
]);
|
|
53
|
+
if (header.alg !== "HS256") {
|
|
54
|
+
yield* Effect.logDebug("Invalid algorithm");
|
|
55
|
+
return { isValid: false };
|
|
56
|
+
}
|
|
57
|
+
const currentTime = Math.floor(Date.now() / 1e3);
|
|
58
|
+
if (payload.exp && currentTime > payload.exp) {
|
|
59
|
+
yield* Effect.logDebug("Token has expired");
|
|
60
|
+
return { isValid: false };
|
|
61
|
+
}
|
|
62
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
63
|
+
const generatedSignature = yield* useGeneratorError(() => {
|
|
64
|
+
return Buffer.from(
|
|
65
|
+
crypto.createHmac("sha256", secret + secret).update(signatureInput).digest()
|
|
66
|
+
).toString("base64url");
|
|
67
|
+
});
|
|
68
|
+
if (generatedSignature !== encodedSignature) {
|
|
69
|
+
yield* Effect.logDebug("Invalid signature");
|
|
70
|
+
return { isValid: false };
|
|
71
|
+
}
|
|
72
|
+
return { isValid: true, userId: payload.userId };
|
|
73
|
+
});
|
|
74
|
+
const generateRandomIDNumber = Effect.fn(
|
|
75
|
+
(length) => useGeneratorError(() => Math.floor(Math.random() * 10 ** length))
|
|
76
|
+
);
|
|
77
|
+
const generateRandomPassword = Effect.fn(
|
|
78
|
+
(length) => useGeneratorError(() => {
|
|
79
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
80
|
+
let password = "";
|
|
81
|
+
const maxValidValue = Math.floor((2 ** 32 - 1) / characters.length) * characters.length;
|
|
82
|
+
while (password.length < length) {
|
|
83
|
+
const n = crypto.getRandomValues(new Uint32Array(1))[0];
|
|
84
|
+
if (n < maxValidValue) {
|
|
85
|
+
password += characters[n % characters.length];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return password;
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
const generateToken = Effect.fn(
|
|
92
|
+
(userId, noExpire) => generateJwt(cmsEncryptionKey, { userId }, noExpire)
|
|
93
|
+
);
|
|
94
|
+
const testToken = Effect.fn((token) => verifyJwt(token, cmsEncryptionKey));
|
|
95
|
+
return {
|
|
96
|
+
generateRandomIDNumber,
|
|
97
|
+
generateRandomPassword,
|
|
98
|
+
generateToken,
|
|
99
|
+
testToken
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
export {
|
|
103
|
+
GeneratorError,
|
|
104
|
+
SDKGenerators,
|
|
105
|
+
useGeneratorError
|
|
106
|
+
};
|