@pikku/core 0.7.0 → 0.7.2

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 (95) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/channel/channel-runner.js +1 -1
  3. package/dist/channel/serverless/serverless-channel-runner.js +1 -1
  4. package/dist/function/function-runner.js +7 -5
  5. package/dist/http/http-runner.js +4 -2
  6. package/dist/http/log-http-routes.js +1 -1
  7. package/lcov.info +4596 -0
  8. package/package.json +1 -1
  9. package/src/channel/channel-handler.ts +165 -0
  10. package/src/channel/channel-runner.ts +153 -0
  11. package/src/channel/channel-store.ts +29 -0
  12. package/src/channel/channel.types.ts +133 -0
  13. package/src/channel/eventhub-service.ts +30 -0
  14. package/src/channel/eventhub-store.ts +8 -0
  15. package/src/channel/index.ts +8 -0
  16. package/src/channel/local/index.ts +3 -0
  17. package/src/channel/local/local-channel-handler.ts +50 -0
  18. package/src/channel/local/local-channel-runner.test.ts +157 -0
  19. package/src/channel/local/local-channel-runner.ts +139 -0
  20. package/src/channel/local/local-eventhub-service.test.ts +95 -0
  21. package/src/channel/local/local-eventhub-service.ts +95 -0
  22. package/src/channel/log-channels.ts +20 -0
  23. package/src/channel/pikku-abstract-channel-handler.test.ts +52 -0
  24. package/src/channel/pikku-abstract-channel-handler.ts +38 -0
  25. package/src/channel/serverless/index.ts +1 -0
  26. package/src/channel/serverless/serverless-channel-runner.ts +220 -0
  27. package/src/errors/error-handler.ts +61 -0
  28. package/src/errors/error.test.ts +54 -0
  29. package/src/errors/errors.ts +339 -0
  30. package/src/errors/index.ts +2 -0
  31. package/src/function/function-runner.ts +92 -0
  32. package/src/function/functions.types.ts +77 -0
  33. package/src/function/index.ts +2 -0
  34. package/src/handle-error.ts +73 -0
  35. package/src/http/http-runner.test.ts +140 -0
  36. package/src/http/http-runner.ts +493 -0
  37. package/src/http/http.types.ts +250 -0
  38. package/src/http/incomingmessage-to-request-convertor.ts +0 -0
  39. package/src/http/index.ts +8 -0
  40. package/src/http/log-http-routes.ts +22 -0
  41. package/src/http/pikku-fetch-http-request.test.ts +237 -0
  42. package/src/http/pikku-fetch-http-request.ts +163 -0
  43. package/src/http/pikku-fetch-http-response.test.ts +82 -0
  44. package/src/http/pikku-fetch-http-response.ts +138 -0
  45. package/src/index.ts +20 -0
  46. package/src/middleware/auth-apikey.ts +66 -0
  47. package/src/middleware/auth-bearer.ts +66 -0
  48. package/src/middleware/auth-cookie.ts +103 -0
  49. package/src/middleware/index.ts +3 -0
  50. package/src/middleware/timeout.ts +13 -0
  51. package/src/middleware-runner.ts +43 -0
  52. package/src/permissions.test.ts +58 -0
  53. package/src/permissions.ts +43 -0
  54. package/src/pikku-function.ts +1 -0
  55. package/src/pikku-request.ts +23 -0
  56. package/src/pikku-response.ts +5 -0
  57. package/src/pikku-state.ts +87 -0
  58. package/src/scheduler/index.ts +5 -0
  59. package/src/scheduler/log-schedulers.ts +20 -0
  60. package/src/scheduler/scheduler-runner.ts +105 -0
  61. package/src/scheduler/scheduler.types.ts +33 -0
  62. package/src/schema.test.ts +57 -0
  63. package/src/schema.ts +99 -0
  64. package/src/services/content-service.ts +68 -0
  65. package/src/services/index.ts +18 -0
  66. package/src/services/jwt-service.ts +30 -0
  67. package/src/services/local-content.ts +105 -0
  68. package/src/services/local-secrets.ts +43 -0
  69. package/src/services/local-variables.ts +17 -0
  70. package/src/services/logger-console.ts +97 -0
  71. package/src/services/logger.ts +57 -0
  72. package/src/services/schema-service.ts +26 -0
  73. package/src/services/secret-service.ts +17 -0
  74. package/src/services/user-session-service.ts +53 -0
  75. package/src/services/variables-service.ts +6 -0
  76. package/src/time-utils.test.ts +56 -0
  77. package/src/time-utils.ts +32 -0
  78. package/src/types/core.types.ts +178 -0
  79. package/src/utils.ts +40 -0
  80. package/tsconfig.json +13 -0
  81. package/tsconfig.tsbuildinfo +1 -0
  82. package/dist/http/http-route-runner.d.ts +0 -90
  83. package/dist/http/http-route-runner.js +0 -325
  84. package/dist/http/http-routes.types.d.ts +0 -179
  85. package/dist/http/http-routes.types.js +0 -1
  86. package/dist/http/pikku-http-request.d.ts +0 -56
  87. package/dist/http/pikku-http-request.js +0 -95
  88. package/dist/http/pikku-http-response.d.ts +0 -13
  89. package/dist/http/pikku-http-response.js +0 -60
  90. package/dist/parse-relative-time-offset.d.ts +0 -12
  91. package/dist/parse-relative-time-offset.js +0 -20
  92. package/dist/pikku-func.d.ts +0 -12
  93. package/dist/pikku-func.js +0 -39
  94. package/dist/types/functions.types.d.ts +0 -37
  95. package/dist/types/functions.types.js +0 -1
@@ -0,0 +1,87 @@
1
+ import { ChannelsMeta, CoreAPIChannel } from './channel/channel.types.js'
2
+ import {
3
+ CoreHTTPFunctionRoute,
4
+ HTTPMethod,
5
+ HTTPRoutesMeta,
6
+ } from './http/http.types.js'
7
+ import { FunctionsMeta, PikkuMiddleware } from './types/core.types.js'
8
+ import {
9
+ CoreScheduledTask,
10
+ ScheduledTasksMeta,
11
+ } from './scheduler/scheduler.types.js'
12
+ import { ErrorDetails, PikkuError } from './errors/error-handler.js'
13
+ import {
14
+ CoreAPIFunction,
15
+ CoreAPIFunctionSessionless,
16
+ } from './function/functions.types.js'
17
+
18
+ interface PikkuState {
19
+ functions: {
20
+ meta: FunctionsMeta
21
+ nameToFunction: Map<
22
+ string,
23
+ CoreAPIFunction<any, any> | CoreAPIFunctionSessionless<any, any>
24
+ >
25
+ }
26
+ http: {
27
+ middleware: Array<{ route: string; middleware: PikkuMiddleware[] }>
28
+ routes: Map<HTTPMethod, Map<string, CoreHTTPFunctionRoute<any, any, any>>>
29
+ meta: HTTPRoutesMeta
30
+ }
31
+ channel: {
32
+ channels: Map<string, CoreAPIChannel<any, any>>
33
+ meta: ChannelsMeta
34
+ }
35
+ scheduler: {
36
+ tasks: Map<string, CoreScheduledTask>
37
+ meta: ScheduledTasksMeta
38
+ }
39
+ misc: {
40
+ errors: Map<PikkuError, ErrorDetails>
41
+ schemas: Map<string, any>
42
+ }
43
+ }
44
+
45
+ export const resetPikkuState = () => {
46
+ globalThis.pikkuState = {
47
+ functions: {
48
+ meta: {},
49
+ nameToFunction: new Map(),
50
+ },
51
+ http: {
52
+ middleware: [],
53
+ routes: new Map(),
54
+ meta: [],
55
+ },
56
+ channel: {
57
+ channels: new Map(),
58
+ meta: {},
59
+ },
60
+ scheduler: {
61
+ tasks: new Map(),
62
+ meta: [],
63
+ },
64
+ misc: {
65
+ errors: globalThis.pikkuState?.misc?.errors || new Map(),
66
+ schemas: globalThis.pikkuState?.misc?.schema || new Map(),
67
+ },
68
+ }
69
+ }
70
+
71
+ if (!globalThis.pikkuState) {
72
+ resetPikkuState()
73
+ }
74
+
75
+ export const pikkuState = <
76
+ Type extends keyof PikkuState,
77
+ Content extends keyof PikkuState[Type],
78
+ >(
79
+ type: Type,
80
+ content: Content,
81
+ value?: PikkuState[Type][Content]
82
+ ): PikkuState[Type][Content] => {
83
+ if (value) {
84
+ globalThis.pikkuState[type][content] = value
85
+ }
86
+ return globalThis.pikkuState[type][content]
87
+ }
@@ -0,0 +1,5 @@
1
+ export * from './scheduler.types.js'
2
+
3
+ export { runScheduledTask, addScheduledTask } from './scheduler-runner.js'
4
+
5
+ export * from './log-schedulers.js'
@@ -0,0 +1,20 @@
1
+ import { pikkuState } from '../pikku-state.js'
2
+ import { Logger } from '../services/index.js'
3
+
4
+ /**
5
+ * Logs all the loaded scheduled tasks.
6
+ * @param logger - A logger for logging information.
7
+ */
8
+ export const logSchedulers = (logger: Logger) => {
9
+ const scheduledTasks = pikkuState('scheduler', 'tasks')
10
+ if (scheduledTasks.size === 0) {
11
+ logger.info('No scheduled tasks added')
12
+ return
13
+ }
14
+
15
+ let scheduledTasksMessage = 'Scheduled tasks:'
16
+ scheduledTasks.forEach(({ schedule }, name) => {
17
+ scheduledTasksMessage += `\n\t- ${name} -> ${schedule}`
18
+ })
19
+ logger.info(scheduledTasksMessage)
20
+ }
@@ -0,0 +1,105 @@
1
+ import type {
2
+ CoreServices,
3
+ CoreSingletonServices,
4
+ CoreUserSession,
5
+ CreateSessionServices,
6
+ } from '../types/core.types.js'
7
+ import type { CoreScheduledTask } from './scheduler.types.js'
8
+ import type { CoreAPIFunctionSessionless } from '../function/functions.types.js'
9
+ import { getErrorResponse } from '../errors/error-handler.js'
10
+ import { closeSessionServices } from '../utils.js'
11
+ import { pikkuState } from '../pikku-state.js'
12
+ import { addFunction, runPikkuFunc } from '../function/function-runner.js'
13
+
14
+ export type RunScheduledTasksParams = {
15
+ name: string
16
+ session?: CoreUserSession
17
+ singletonServices: CoreSingletonServices
18
+ createSessionServices?: CreateSessionServices<
19
+ CoreSingletonServices,
20
+ CoreServices<CoreSingletonServices>,
21
+ CoreUserSession
22
+ >
23
+ }
24
+
25
+ export const addScheduledTask = <
26
+ APIFunction extends CoreAPIFunctionSessionless<void, void>,
27
+ >(
28
+ scheduledTask: CoreScheduledTask<APIFunction>
29
+ ) => {
30
+ const meta = pikkuState('scheduler', 'meta')
31
+ const taskMeta = meta[scheduledTask.name]
32
+ if (!taskMeta) {
33
+ throw new Error('Task metadata not found')
34
+ }
35
+ addFunction(taskMeta.pikkuFuncName, scheduledTask.func)
36
+
37
+ const tasks = pikkuState('scheduler', 'tasks')
38
+ if (tasks.has(scheduledTask.name)) {
39
+ throw new Error(`Scheduled task already exists: ${scheduledTask.name}`)
40
+ }
41
+ tasks.set(scheduledTask.name, scheduledTask)
42
+ }
43
+
44
+ class ScheduledTaskNotFoundError extends Error {
45
+ constructor(title: string) {
46
+ super(`Scheduled task not found: ${title}`)
47
+ }
48
+ }
49
+
50
+ export async function runScheduledTask<
51
+ SingletonServices extends CoreSingletonServices = CoreSingletonServices,
52
+ UserSession extends CoreUserSession = CoreUserSession,
53
+ >({
54
+ name,
55
+ session,
56
+ singletonServices,
57
+ createSessionServices,
58
+ }: RunScheduledTasksParams): Promise<void> {
59
+ let sessionServices: CoreServices | undefined
60
+ try {
61
+ const task = pikkuState('scheduler', 'tasks').get(name)
62
+ const meta = pikkuState('scheduler', 'meta')[name]
63
+ if (!task) {
64
+ throw new ScheduledTaskNotFoundError(`Scheduled task not found: ${name}`)
65
+ }
66
+ if (!meta) {
67
+ throw new ScheduledTaskNotFoundError(
68
+ `Scheduled task meta not found: ${name}`
69
+ )
70
+ }
71
+
72
+ singletonServices.logger.info(
73
+ `Running schedule task: ${name} | schedule: ${task.schedule}`
74
+ )
75
+
76
+ const getAllServices = async () => {
77
+ if (createSessionServices) {
78
+ const sessionServices = await createSessionServices(
79
+ singletonServices,
80
+ {},
81
+ session
82
+ )
83
+ return { ...singletonServices, ...sessionServices }
84
+ }
85
+ return singletonServices
86
+ }
87
+
88
+ return await runPikkuFunc(meta.pikkuFuncName, {
89
+ singletonServices,
90
+ getAllServices,
91
+ session,
92
+ data: undefined,
93
+ })
94
+ } catch (e: any) {
95
+ const errorResponse = getErrorResponse(e)
96
+ if (errorResponse != null) {
97
+ singletonServices.logger.error(e)
98
+ }
99
+ throw e
100
+ } finally {
101
+ if (sessionServices) {
102
+ await closeSessionServices(singletonServices.logger, sessionServices)
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,33 @@
1
+ import { APIDocs, CoreUserSession } from '../types/core.types.js'
2
+ import { CoreAPIFunctionSessionless } from '../function/functions.types.js'
3
+
4
+ /**
5
+ * Represents metadata for scheduled tasks, including title, schedule, and documentation.
6
+ */
7
+ export type ScheduledTasksMeta<UserSession extends CoreUserSession = any> =
8
+ Record<
9
+ string,
10
+ {
11
+ pikkuFuncName: string
12
+ name: string
13
+ schedule: string
14
+ session?: UserSession
15
+ docs?: APIDocs
16
+ tags?: string[]
17
+ }
18
+ >
19
+
20
+ /**
21
+ * Represents a core scheduled task.
22
+ */
23
+ export type CoreScheduledTask<
24
+ APIFunction = CoreAPIFunctionSessionless<void, void>,
25
+ UserSession extends CoreUserSession = CoreUserSession,
26
+ > = {
27
+ name: string
28
+ schedule: string
29
+ func: APIFunction
30
+ docs?: APIDocs
31
+ session?: UserSession
32
+ tags?: string[]
33
+ }
@@ -0,0 +1,57 @@
1
+ import { test, describe, before } from 'node:test'
2
+ import * as assert from 'assert'
3
+ import { addSchema, coerceTopLevelDataFromSchema } from './schema.js'
4
+
5
+ describe('Schema', () => {
6
+ describe('coerceQueryStringToArray', () => {
7
+ before(() => {
8
+ // Add schemas dynamically using addSchema
9
+ addSchema('testSchema', {
10
+ properties: {
11
+ tags: { type: 'array' },
12
+ count: { type: 'number' },
13
+ name: { type: 'string' },
14
+ },
15
+ })
16
+
17
+ addSchema('booleanSchema', {
18
+ properties: {
19
+ isActive: true, // Invalid schema definition, but the function should handle it
20
+ tags: { type: 'array' },
21
+ },
22
+ })
23
+ })
24
+
25
+ test('should split a string into an array for properties of type array', () => {
26
+ const data = { tags: 'a,b,c' }
27
+ coerceTopLevelDataFromSchema('testSchema', data)
28
+ assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
29
+ })
30
+
31
+ test('should not modify properties of type array if they are already arrays', () => {
32
+ const data = { tags: ['a', 'b', 'c'] }
33
+ coerceTopLevelDataFromSchema('testSchema', data)
34
+ assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
35
+ })
36
+
37
+ test('should not modify properties that are not type array', () => {
38
+ const data = { count: 5, name: 'example' }
39
+ coerceTopLevelDataFromSchema('testSchema', data)
40
+ assert.strictEqual(data.count, 5)
41
+ assert.strictEqual(data.name, 'example')
42
+ })
43
+
44
+ test('should handle cases where the data object does not have a key present in the schema', () => {
45
+ const data = { unknownKey: 'shouldRemain' }
46
+ coerceTopLevelDataFromSchema('testSchema', data)
47
+ assert.strictEqual(data.unknownKey, 'shouldRemain')
48
+ })
49
+
50
+ test('should handle cases where schema properties contain boolean values', () => {
51
+ const data = { tags: 'a,b,c', isActive: 'true' }
52
+ coerceTopLevelDataFromSchema('booleanSchema', data)
53
+ assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
54
+ assert.strictEqual(data.isActive, 'true') // No coercion should happen
55
+ })
56
+ })
57
+ })
package/src/schema.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { Logger } from './services/logger.js'
2
+ import { SchemaService } from './services/schema-service.js'
3
+ import { UnprocessableContentError } from './errors/errors.js'
4
+ import { pikkuState } from './pikku-state.js'
5
+
6
+ /**
7
+ * Adds a schema to the global schemas map.
8
+ * @param name - The name of the schema.
9
+ * @param value - The schema value.
10
+ * @ignore
11
+ */
12
+ export const addSchema = (name: string, value: any) => {
13
+ pikkuState('misc', 'schemas').set(name, value)
14
+ }
15
+
16
+ /**
17
+ * Loads a schema and compiles it into a validator.
18
+ * @param logger - A logger for logging information.
19
+ */
20
+ export const compileAllSchemas = (
21
+ logger: Logger,
22
+ schemaService?: SchemaService
23
+ ) => {
24
+ if (!schemaService) {
25
+ throw new Error('SchemaService needs to be defined to load schemas')
26
+ }
27
+ const schemas = pikkuState('misc', 'schemas')
28
+ for (const [name, schema] of schemas) {
29
+ schemaService.compileSchema(name, schema)
30
+ }
31
+ validateAllSchemasLoaded(logger, schemaService)
32
+ }
33
+
34
+ const validateAllSchemasLoaded = (
35
+ logger: Logger,
36
+ schemaService: SchemaService
37
+ ) => {
38
+ const routesMeta = pikkuState('http', 'meta')
39
+ const validators = schemaService.getSchemaNames()
40
+
41
+ const missingSchemas: string[] = []
42
+
43
+ for (const route of routesMeta) {
44
+ if (!route.input || validators.has(route.input)) {
45
+ continue
46
+ }
47
+ missingSchemas.push(route.input)
48
+ }
49
+
50
+ if (missingSchemas.length > 0) {
51
+ logger.error(
52
+ `Error: Failed to load schemas:\n.${missingSchemas.join('\n')}`
53
+ )
54
+ logger.error('\tHave you run the schema generation?')
55
+ logger.error('\tnpx @pikku/cli schemas')
56
+ } else {
57
+ logger.info('All schemas loaded')
58
+ }
59
+ }
60
+
61
+ export const coerceTopLevelDataFromSchema = (schemaName: string, data: any) => {
62
+ const schema = pikkuState('misc', 'schemas').get(schemaName)
63
+ for (const key in schema.properties) {
64
+ const property = schema.properties[key]
65
+ if (typeof property === 'boolean') {
66
+ continue
67
+ }
68
+ const type = property.type
69
+ if (typeof type === 'boolean') {
70
+ continue
71
+ }
72
+ if (type === 'array' && typeof data[key] === 'string') {
73
+ data[key] = data[key].split(',')
74
+ } else if (type === 'string' && property.format === 'date-time') {
75
+ data[key] = new Date(data[key])
76
+ }
77
+ }
78
+ }
79
+
80
+ export const validateSchema = async (
81
+ logger: Logger,
82
+ schemaService: SchemaService | undefined,
83
+ schemaName: string | undefined | null,
84
+ data: any
85
+ ) => {
86
+ if (schemaService) {
87
+ if (!schemaName) {
88
+ if (data && (data.length > 0 || Object.keys(data).length > 0)) {
89
+ logger.warn('No schema provided, but data was passed')
90
+ throw new UnprocessableContentError('No data expected')
91
+ } else {
92
+ return
93
+ }
94
+ }
95
+ const schema = pikkuState('misc', 'schemas').get(schemaName)
96
+ await schemaService.compileSchema(schemaName, schema)
97
+ await schemaService.validateSchema(schemaName, data)
98
+ }
99
+ }
@@ -0,0 +1,68 @@
1
+ import { ReadStream } from 'fs'
2
+
3
+ export interface ContentService {
4
+ /**
5
+ * Signs a content key to generate a secure, time-limited access URL.
6
+ * @param contentKey - The key representing the content object.
7
+ * @param dateLessThan - The expiration time for the signed URL.
8
+ * @param dateGreaterThan - (Optional) Start time before which access is denied.
9
+ */
10
+ signContentKey(
11
+ contentKey: string,
12
+ dateLessThan: Date,
13
+ dateGreaterThan?: Date
14
+ ): Promise<string>
15
+
16
+ /**
17
+ * Signs an arbitrary URL to generate a secure, time-limited access URL.
18
+ * @param url - The full URL that needs signing.
19
+ * @param dateLessThan - The expiration time for the signed URL.
20
+ * @param dateGreaterThan - (Optional) Start time before which access is denied.
21
+ */
22
+ signURL(
23
+ url: string,
24
+ dateLessThan: Date,
25
+ dateGreaterThan?: Date
26
+ ): Promise<string>
27
+
28
+ /**
29
+ * Generates a signed URL for uploading a file directly to storage.
30
+ * @param fileKey - The desired key/location of the uploaded file.
31
+ * @param contentType - The MIME type of the file.
32
+ * @returns A signed upload URL and the finalized asset key.
33
+ */
34
+ getUploadURL(
35
+ fileKey: string,
36
+ contentType: string
37
+ ): Promise<{ uploadUrl: string; assetKey: string }>
38
+
39
+ /**
40
+ * Deletes a file from the storage backend.
41
+ * @param fileName - The name or key of the file to delete.
42
+ * @returns A boolean indicating success.
43
+ */
44
+ deleteFile(fileName: string): Promise<boolean>
45
+
46
+ /**
47
+ * Uploads a file stream to storage under a specified asset key.
48
+ * @param assetKey - The key where the file will be saved.
49
+ * @param stream - A readable stream of the file contents.
50
+ * @returns A boolean indicating success.
51
+ */
52
+ writeFile(assetKey: string, stream: ReadStream): Promise<boolean>
53
+
54
+ /**
55
+ * Copies a file from a local absolute path into storage under a new asset key.
56
+ * @param assetKey - The destination key.
57
+ * @param fromAbsolutePath - The local absolute file path.
58
+ * @returns A boolean indicating success.
59
+ */
60
+ copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>
61
+
62
+ /**
63
+ * Reads a file from storage as a readable stream.
64
+ * @param assetKey - The key of the file to read.
65
+ * @returns A readable file stream.
66
+ */
67
+ readFile(assetKey: string): Promise<ReadStream>
68
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @module @pikku/core
3
+ */
4
+
5
+ // Interfaces
6
+ export * from './content-service.js'
7
+ export * from './jwt-service.js'
8
+ export * from './logger.js'
9
+ export * from './logger-console.js'
10
+ export * from './secret-service.js'
11
+ export * from './variables-service.js'
12
+ export * from './schema-service.js'
13
+ export * from './user-session-service.js'
14
+ export * from './local-content.js'
15
+
16
+ // Local implementations
17
+ export * from './local-secrets.js'
18
+ export * from './local-variables.js'
@@ -0,0 +1,30 @@
1
+ import { RelativeTimeInput } from '../time-utils.js'
2
+
3
+ /**
4
+ * Interface for handling JSON Web Tokens (JWT).
5
+ */
6
+ export interface JWTService {
7
+ /**
8
+ * Encodes a payload into a JWT.
9
+ * @param expiresIn - The expiration time of the token.
10
+ * @param payload - The payload to encode.
11
+ * @returns A promise that resolves to the encoded JWT.
12
+ */
13
+ encode: <T extends any>(
14
+ expiresIn: RelativeTimeInput,
15
+ payload: T
16
+ ) => Promise<string>
17
+
18
+ /**
19
+ * Decodes a JWT into its payload.
20
+ * @param hash - The JWT to decode.
21
+ * @param invalidHashError - An optional error to throw if the hash is invalid.
22
+ * @param debug - An optional flag for debugging.
23
+ * @returns A promise that resolves to the decoded payload.
24
+ */
25
+ decode: <T>(
26
+ hash: string,
27
+ invalidHashError?: Error,
28
+ debug?: boolean
29
+ ) => Promise<T>
30
+ }
@@ -0,0 +1,105 @@
1
+ import { createReadStream, createWriteStream, promises, ReadStream } from 'fs'
2
+ import { mkdir } from 'fs/promises'
3
+ import { ContentService, Logger } from '@pikku/core/services'
4
+ import { pipeline } from 'stream/promises'
5
+ import { Readable } from 'stream'
6
+
7
+ export interface LocalContentConfig {
8
+ localFileUploadPath: string
9
+ uploadUrlPrefix: string
10
+ assetUrlPrefix: string
11
+ sizeLimit?: string
12
+ }
13
+
14
+ export class LocalContent implements ContentService {
15
+ constructor(
16
+ private config: LocalContentConfig,
17
+ private logger: Logger
18
+ ) {}
19
+
20
+ public async init() {}
21
+
22
+ public async signURL(url: string): Promise<string> {
23
+ return `${url}?signed=true`
24
+ }
25
+
26
+ public async signContentKey(assetKey: string): Promise<string> {
27
+ return `${this.config.assetUrlPrefix}/${assetKey}?signed=true`
28
+ }
29
+
30
+ public async getUploadURL(assetKey: string) {
31
+ this.logger.debug(`Going to upload with key: ${assetKey}`)
32
+ return {
33
+ uploadUrl: `${this.config.uploadUrlPrefix}/${assetKey}`,
34
+ assetKey,
35
+ }
36
+ }
37
+
38
+ public async writeFile(assetKey: string, stream: Readable): Promise<boolean> {
39
+ this.logger.debug(`Writing file: ${assetKey}`)
40
+
41
+ const path = `${this.config.localFileUploadPath}/${assetKey}`
42
+
43
+ try {
44
+ await this.createDirectoryForFile(path)
45
+ const fileStream = createWriteStream(path)
46
+ // Use pipeline to properly manage stream piping and errors
47
+ await pipeline(stream, fileStream)
48
+ return true
49
+ } catch (e) {
50
+ console.error(e)
51
+ this.logger.error(`Error writing content ${assetKey}`, e)
52
+ return false
53
+ }
54
+ }
55
+
56
+ public async copyFile(
57
+ assetKey: string,
58
+ fromAbsolutePath: string
59
+ ): Promise<boolean> {
60
+ this.logger.debug(`Writing file: ${assetKey}`)
61
+ try {
62
+ const path = `${this.config.localFileUploadPath}/${assetKey}`
63
+ await this.createDirectoryForFile(path)
64
+ await promises.copyFile(fromAbsolutePath, path)
65
+ } catch (e) {
66
+ console.error(e)
67
+ this.logger.error(`Error inserting content ${assetKey}`, e)
68
+ }
69
+ return false
70
+ }
71
+
72
+ public async readFile(assetKey: string): Promise<ReadStream> {
73
+ this.logger.debug(`Getting key: ${assetKey}`)
74
+
75
+ const filePath = `${this.config.localFileUploadPath}/${assetKey}`
76
+
77
+ try {
78
+ const stream = createReadStream(filePath)
79
+ // Handle early stream errors (like file not found, permission denied, etc.)
80
+ stream.on('error', (err) => {
81
+ this.logger.error(`Error getting content ${assetKey}`, err)
82
+ })
83
+
84
+ return stream
85
+ } catch (e) {
86
+ this.logger.error(`Error setting up stream for ${assetKey}`, e)
87
+ throw e
88
+ }
89
+ }
90
+
91
+ public async deleteFile(assetKey: string): Promise<boolean> {
92
+ this.logger.debug(`deleting key: ${assetKey}`)
93
+ try {
94
+ await promises.unlink(`${this.config.localFileUploadPath}/${assetKey}`)
95
+ } catch (e: any) {
96
+ this.logger.error(`Error deleting content ${assetKey}`, e)
97
+ }
98
+ return false
99
+ }
100
+
101
+ private async createDirectoryForFile(path: string): Promise<void> {
102
+ const dir = path.split('/').slice(0, -1).join('/')
103
+ await mkdir(dir, { recursive: true })
104
+ }
105
+ }
@@ -0,0 +1,43 @@
1
+ import { LocalVariablesService } from './local-variables.js'
2
+ import { SecretService } from './secret-service.js'
3
+ import { VariablesService } from './variables-service.js'
4
+
5
+ /**
6
+ * Service for retrieving secrets from environment variables.
7
+ */
8
+ export class LocalSecretService implements SecretService {
9
+ /**
10
+ * Creates an instance of LocalSecretService.
11
+ */
12
+ constructor(
13
+ private variables: VariablesService = new LocalVariablesService()
14
+ ) {}
15
+
16
+ /**
17
+ * Retrieves a secret by key.
18
+ * @param key - The key of the secret to retrieve.
19
+ * @returns A promise that resolves to the secret value.
20
+ * @throws {Error} If the secret is not found.
21
+ */
22
+ public async getSecretJSON<R>(key: string): Promise<R> {
23
+ const value = await this.variables.get(key)
24
+ if (value) {
25
+ return JSON.parse(value)
26
+ }
27
+ throw new Error(`Secret Not Found: ${key}`)
28
+ }
29
+
30
+ /**
31
+ * Retrieves a secret by key.
32
+ * @param key - The key of the secret to retrieve.
33
+ * @returns A promise that resolves to the secret value.
34
+ * @throws {Error} If the secret is not found.
35
+ */
36
+ public async getSecret(key: string): Promise<string> {
37
+ const value = await this.variables.get(key)
38
+ if (value) {
39
+ return value
40
+ }
41
+ throw new Error(`Secret Not Found: ${key}`)
42
+ }
43
+ }
@@ -0,0 +1,17 @@
1
+ import { VariablesService } from './variables-service.js'
2
+
3
+ export class LocalVariablesService implements VariablesService {
4
+ constructor(
5
+ private variables: Record<string, string | undefined> = process.env
6
+ ) {}
7
+
8
+ public getAll():
9
+ | Promise<Record<string, string | undefined>>
10
+ | Record<string, string | undefined> {
11
+ return this.variables || {}
12
+ }
13
+
14
+ public get(name: string): Promise<string | undefined> | string | undefined {
15
+ return this.variables[name]
16
+ }
17
+ }