@darco2903/cdn-api 1.0.7-beta.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.
@@ -0,0 +1,19 @@
1
+ import z from "zod";
2
+
3
+ export const endpointPathSchema = z
4
+ .string()
5
+ .max(256)
6
+ .regex(/^(\/[a-zA-Z0-9]+([_.-][a-zA-Z0-9]+)*)*$/, "Invalid endpoint");
7
+
8
+ export const endpointPublicSchema = z.object({
9
+ endpoint: endpointPathSchema,
10
+ });
11
+
12
+ export type EndpointPublic = z.infer<typeof endpointPublicSchema>;
13
+
14
+ export const endpointSchema = endpointPublicSchema.extend({
15
+ created_at: z.coerce.date(),
16
+ updated_at: z.coerce.date(),
17
+ });
18
+
19
+ export type Endpoint = z.infer<typeof endpointSchema>;
@@ -0,0 +1,6 @@
1
+ export * from "./creds.js";
2
+ export * from "./endpoint.js";
3
+ export * from "./jwt.js";
4
+ export * from "./record.js";
5
+ export * from "./stats.js";
6
+ export * from "./upload.js";
@@ -0,0 +1,78 @@
1
+ import z from "zod";
2
+ import {
3
+ authAssetTypeSchema,
4
+ authServiceSchema,
5
+ userPublicIdSchema,
6
+ } from "@darco2903/auth-api/client";
7
+ import { endpointPathSchema } from "./endpoint.js";
8
+
9
+ export type JWTVerifyError = {
10
+ name:
11
+ | "TokenExpiredError"
12
+ | "JsonWebTokenError"
13
+ | "NotBeforeError"
14
+ | "InvalidToken"
15
+ | "InvalidTokenData";
16
+ message: string;
17
+ };
18
+
19
+ export type JWTSignError = {
20
+ name: "InvalidTokenData" | "JsonWebTokenError";
21
+ message: string;
22
+ };
23
+
24
+ const JWTData = z.object({
25
+ iat: z.number(),
26
+ exp: z.number(),
27
+ });
28
+
29
+ export const jwtSchema = z.string().startsWith("Bearer ");
30
+
31
+ ///////////////////////////////////
32
+
33
+ export const cdnAssetHeaderSchema = z.object({
34
+ "x-cdn-asset": jwtSchema.optional(),
35
+ });
36
+
37
+ export const cdnAssetTokenDataSchema = z.object({
38
+ service: authServiceSchema,
39
+ type: z.literal("avatar"),
40
+ endpoint: endpointPathSchema,
41
+ user_public_id: userPublicIdSchema,
42
+ file_size_max: z.number().min(0).optional(),
43
+ allowed_file_types: z.array(z.string()).optional(),
44
+ callback_url: z.string().url().optional(),
45
+ });
46
+
47
+ export const cdnAssetTokenDataDecodedSchema = z.intersection(
48
+ cdnAssetTokenDataSchema,
49
+ JWTData
50
+ );
51
+
52
+ export type CdnAssetTokenData = z.infer<typeof cdnAssetTokenDataSchema>;
53
+ export type CdnAssetTokenDataDecoded = z.infer<
54
+ typeof cdnAssetTokenDataDecodedSchema
55
+ >;
56
+
57
+ ///////////////////////////////////
58
+
59
+ export const cdnFeedbackHeaderSchema = z.object({
60
+ authorization: jwtSchema.optional(),
61
+ });
62
+
63
+ export const cdnFeedbackTokenDataSchema = z.object({
64
+ service: authServiceSchema,
65
+ type: authAssetTypeSchema,
66
+ endpoint: endpointPathSchema,
67
+ user_public_id: userPublicIdSchema,
68
+ });
69
+
70
+ export const cdnFeedbackTokenDecodedSchema = z.intersection(
71
+ cdnFeedbackTokenDataSchema,
72
+ JWTData
73
+ );
74
+
75
+ export type CdnFeedbackTokenData = z.infer<typeof cdnFeedbackTokenDataSchema>;
76
+ export type CdnFeedbackTokenDataDecoded = z.infer<
77
+ typeof cdnFeedbackTokenDecodedSchema
78
+ >;
@@ -0,0 +1,35 @@
1
+ import z from "zod";
2
+ import { userPublicIdSchema } from "@darco2903/auth-api/client";
3
+ import { endpointPublicSchema, endpointSchema } from "./endpoint.js";
4
+ import { STORAGE_PUBLIC_ID_LENGTH } from "../consts.js";
5
+
6
+ export const storagePublicIdSchema = z
7
+ .string()
8
+ .length(STORAGE_PUBLIC_ID_LENGTH);
9
+
10
+ export const recordPublicSchema = z.object({
11
+ storage_id: storagePublicIdSchema,
12
+ filename: z.string(),
13
+ size: z.number().int().gte(0),
14
+ mime_type: z.string(),
15
+ endpoints: z.array(endpointPublicSchema),
16
+ });
17
+
18
+ export type RecordPublic = z.infer<typeof recordPublicSchema>;
19
+
20
+ export const recordTypeSchema = z.enum(["service", "system", "user"]);
21
+
22
+ export type RecordType = z.infer<typeof recordTypeSchema>;
23
+
24
+ export const recordSchema = recordPublicSchema.extend({
25
+ endpoints: z.array(endpointSchema),
26
+ role: z.number().int().min(-1).max(255),
27
+ user_id: userPublicIdSchema.nullable(),
28
+ type: recordTypeSchema,
29
+ visible: z.boolean(),
30
+ active: z.boolean(),
31
+ created_at: z.coerce.date(),
32
+ updated_at: z.coerce.date(),
33
+ });
34
+
35
+ export type Record = z.infer<typeof recordSchema>;
@@ -0,0 +1,9 @@
1
+ import z from "zod";
2
+
3
+ export const statsGlobalSchema = z.object({
4
+ record_count: z.number().int().gte(0),
5
+ total_size: z.number().int().gte(0),
6
+ max_size: z.number().int().gte(0),
7
+ });
8
+
9
+ export type StatsGlobal = z.infer<typeof statsGlobalSchema>;
@@ -0,0 +1,18 @@
1
+ import z from "zod";
2
+
3
+ export const uploadDataSchema = z.object({
4
+ filename: z.string().min(3).max(100),
5
+ role: z.number().int().min(0).max(255),
6
+ visible: z.boolean(),
7
+ active: z.boolean(),
8
+ });
9
+
10
+ export type UploadData = z.infer<typeof uploadDataSchema>;
11
+
12
+ export const uploadInitSchema = uploadDataSchema.extend({
13
+ size: z.number().int().min(1),
14
+ mimeType: z.string().min(3).max(100),
15
+ parts: z.number().int().min(1),
16
+ });
17
+
18
+ export type UploadInit = z.infer<typeof uploadInitSchema>;
package/src/types.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { z, type ZodType } from "zod";
2
+
3
+ export const apiSuccess = <T>(schema: ZodType<T>) => schema;
4
+
5
+ export const apiError = <T, U>(code: ZodType<T>, error: ZodType<U>) =>
6
+ z.object({
7
+ code,
8
+ error,
9
+ name: z.literal("APIError"),
10
+ });
11
+
12
+ export const apiErrorData = <T, U, V>(
13
+ code: ZodType<T>,
14
+ error: ZodType<U>,
15
+ data: ZodType<V>
16
+ ) =>
17
+ z.object({
18
+ code,
19
+ error,
20
+ name: z.literal("APIError"),
21
+ data,
22
+ });
23
+
24
+ export function jsonStringAs<T extends z.ZodTypeAny>(
25
+ schema: T
26
+ ): z.ZodEffects<z.ZodString, z.infer<T>> {
27
+ return z.string().transform((str, ctx) => {
28
+ try {
29
+ return schema.parse(JSON.parse(str));
30
+ } catch {
31
+ ctx.addIssue({
32
+ code: z.ZodIssueCode.custom,
33
+ message: "Invalid JSON",
34
+ });
35
+ return z.NEVER;
36
+ }
37
+ });
38
+ }
@@ -0,0 +1,99 @@
1
+ import axios, { AxiosInstance } from "axios";
2
+ import type { UploadData } from "./types/upload.js";
3
+ import { createClient } from "./client.js";
4
+
5
+ export default class Uploader {
6
+ static MAX_SINGLE_UPLOAD_SIZE = 100_000_000; // 100MB
7
+
8
+ protected ax: AxiosInstance;
9
+ protected client;
10
+
11
+ constructor(origin: string) {
12
+ this.ax = axios.create({
13
+ baseURL: origin,
14
+ withCredentials: true,
15
+ });
16
+ this.client = createClient(origin);
17
+ }
18
+
19
+ protected getPartEndpoint(uploadId: string, part: number) {
20
+ return `/upload/part/${uploadId}/${part}`;
21
+ }
22
+
23
+ public async upload(
24
+ file: File,
25
+ role: number,
26
+ visible: boolean,
27
+ active: boolean
28
+ ) {
29
+ if (file.size < Uploader.MAX_SINGLE_UPLOAD_SIZE) {
30
+ const formData = new FormData();
31
+ const data: UploadData = {
32
+ filename: file.name,
33
+ role,
34
+ visible,
35
+ active,
36
+ };
37
+ formData.append("data", JSON.stringify(data));
38
+ formData.append("file", file);
39
+ const res = await this.ax.post("/upload", formData, {
40
+ onUploadProgress(e) {
41
+ //
42
+ },
43
+ });
44
+ } else {
45
+ const parts = Math.ceil(
46
+ file.size / Uploader.MAX_SINGLE_UPLOAD_SIZE
47
+ );
48
+ const resInit = await this.client.upload.uploadInit({
49
+ body: {
50
+ filename: file.name,
51
+ role,
52
+ visible,
53
+ active,
54
+ size: file.size,
55
+ mimeType: file.type,
56
+ parts,
57
+ },
58
+ });
59
+
60
+ if (resInit.status !== 200) {
61
+ throw new Error("Failed to initialize upload");
62
+ }
63
+
64
+ const uploadId = resInit.body.uploadId;
65
+
66
+ for (let part = 0; part < parts; part++) {
67
+ const start = part * Uploader.MAX_SINGLE_UPLOAD_SIZE;
68
+ const end = Math.min(
69
+ start + Uploader.MAX_SINGLE_UPLOAD_SIZE,
70
+ file.size
71
+ );
72
+
73
+ const formData = new FormData();
74
+ formData.append("file", file.slice(start, end));
75
+
76
+ const res = await this.ax.post(
77
+ this.getPartEndpoint(uploadId, part + 1),
78
+ formData,
79
+ {
80
+ onUploadProgress(e) {
81
+ //
82
+ },
83
+ }
84
+ );
85
+
86
+ if (res.status !== 200) {
87
+ throw new Error("Failed to upload part " + (part + 1));
88
+ }
89
+ }
90
+
91
+ const res = await this.client.upload.uploadEnd({
92
+ params: { upload_id: uploadId },
93
+ });
94
+ if (res.status !== 200) {
95
+ throw new Error("Failed to finalize upload");
96
+ }
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,5 @@
1
+ -----BEGIN EC PRIVATE KEY-----
2
+ MHcCAQEEIGqV6t//MTxzvRRH46iJbu2zzeOUFOjmF8JQs75lYb61oAoGCCqGSM49
3
+ AwEHoUQDQgAE9MaRmiwH25Dv6y1wKrcEGMCG1EUB9s3zZlxIXsOR7EI5xaTMdedb
4
+ 2J7IDmTacZDD0ZCJ7DSyrS88mcTztm+kWQ==
5
+ -----END EC PRIVATE KEY-----
@@ -0,0 +1,4 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MaRmiwH25Dv6y1wKrcEGMCG1EUB
3
+ 9s3zZlxIXsOR7EI5xaTMdedb2J7IDmTacZDD0ZCJ7DSyrS88mcTztm+kWQ==
4
+ -----END PUBLIC KEY-----
package/tests/keys.ts ADDED
@@ -0,0 +1,11 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export const PRIVATE_KEY = fs.readFileSync(
5
+ path.join(import.meta.dirname, "keys/private.pem"),
6
+ "utf-8"
7
+ );
8
+ export const PUBLIC_KEY = fs.readFileSync(
9
+ path.join(import.meta.dirname, "keys/public.pem"),
10
+ "utf-8"
11
+ );
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": "..",
6
+ "types": ["node"],
7
+ "module": "nodenext",
8
+ "moduleResolution": "nodenext"
9
+ },
10
+ "include": ["**/*.ts", "../src/**/*.ts"],
11
+ "exclude": ["../dist", "../node_modules"]
12
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ type CdnFeedbackTokenData,
4
+ JWTSign,
5
+ JWTVerify,
6
+ } from "../../src/index.js";
7
+ import { Hour } from "@darco2903/secondthought";
8
+ import { PRIVATE_KEY, PUBLIC_KEY } from "../keys.js";
9
+
10
+ //////////////////////////
11
+ // Tests for JWT Sign & Verify
12
+
13
+ describe("JWT Sign & Verify", () => {
14
+ it("should resolve after the specified time", async () => {
15
+ const data: CdnFeedbackTokenData = {
16
+ user_public_id: "abcdefgh",
17
+ endpoint: "/test",
18
+ service: "auth",
19
+ type: "avatar",
20
+ };
21
+
22
+ const res = await JWTSign(data, PRIVATE_KEY, new Hour(1));
23
+ expect(res.isOk()).toBeTruthy();
24
+
25
+ const signedToken = res._unsafeUnwrap();
26
+ expect(signedToken).toBeTypeOf("string");
27
+
28
+ const verifyRes = await JWTVerify(signedToken, PUBLIC_KEY);
29
+ expect(verifyRes.isOk()).toBeTruthy();
30
+
31
+ const decodedData = verifyRes._unsafeUnwrap();
32
+ expect(decodedData).toMatchObject({
33
+ ...data,
34
+ iat: expect.any(Number),
35
+ exp: expect.any(Number),
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,25 @@
1
+ import fs from "fs";
2
+ import { generateOpenApi } from "@ts-rest/open-api";
3
+ import { contract } from "../src/";
4
+
5
+ import { version } from "../package.json";
6
+
7
+ console.log("Generating OpenAPI documentation...");
8
+ console.log(`API version: ${version}`);
9
+
10
+ const openApiDocument = generateOpenApi(contract, {
11
+ info: {
12
+ title: "CDN API",
13
+ version,
14
+ },
15
+ servers: [
16
+ {
17
+ url: "https://cdn.darco2903.fr",
18
+ },
19
+ {
20
+ url: "https://dev-cdn.darco2903.fr",
21
+ },
22
+ ],
23
+ });
24
+
25
+ fs.writeFileSync("openapi.json", JSON.stringify(openApiDocument, null, 2));
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "target": "ES2020",
5
+ "module": "nodenext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "esModuleInterop": true,
9
+ "strict": true,
10
+ "moduleResolution": "node16",
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src"]
14
+ }