@nitida/sdk 0.20.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/AGENTS.md +48 -0
- package/README.md +1158 -0
- package/dist/expo.d.ts +48 -0
- package/dist/expo.js +25 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +790 -0
- package/dist/index.js +802 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +1 -0
- package/dist/native.js +16 -0
- package/dist/native.js.map +1 -0
- package/dist/react.d.ts +47 -0
- package/dist/react.js +96 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +72 -0
- package/dist/server.js +808 -0
- package/dist/server.js.map +1 -0
- package/dist/web.d.ts +147 -0
- package/dist/web.js +864 -0
- package/dist/web.js.map +1 -0
- package/package.json +122 -0
- package/skills/nitida-sdk/SKILL.md +306 -0
- package/src/expo/index.ts +76 -0
- package/src/index.ts +1562 -0
- package/src/native/index.ts +48 -0
- package/src/react/index.ts +169 -0
- package/src/server/index.ts +124 -0
- package/src/web/index.ts +334 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nitida/sdk/native — React Native compression helpers.
|
|
3
|
+
*
|
|
4
|
+
* Thin pass-through to `@nitida/asset-compressor-native`'s
|
|
5
|
+
* `compressImage` / `compressImages`, kept in its own subpath so:
|
|
6
|
+
*
|
|
7
|
+
* 1. Web-only consumers (Next.js, Vite) don't pull react-native into
|
|
8
|
+
* their bundle when they import `@nitida/sdk`.
|
|
9
|
+
* 2. The native bridge stays lazy — `aq.upload(...)` on RN can
|
|
10
|
+
* dynamic-import this module only when `compress: true` is set.
|
|
11
|
+
* 3. Future native helpers (upload session resume, native
|
|
12
|
+
* thumbnailing, etc.) can land here without polluting `/expo`,
|
|
13
|
+
* which is specifically about background upload sessions.
|
|
14
|
+
*
|
|
15
|
+
* Peer deps:
|
|
16
|
+
* - react-native
|
|
17
|
+
* - react-native-compressor (with the matching Expo config plugin)
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
*
|
|
21
|
+
* import { compressImage, compressImages } from "@nitida/sdk/native";
|
|
22
|
+
*
|
|
23
|
+
* const { uri, size, originalSize } = await compressImage({
|
|
24
|
+
* uri: pickerAsset.uri,
|
|
25
|
+
* filename: pickerAsset.fileName ?? "photo.jpg",
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* const upload = createExpoUploader(aq, {
|
|
29
|
+
* file: { uri, mime: "image/jpeg", name: "photo.jpg" },
|
|
30
|
+
* });
|
|
31
|
+
* await upload.start();
|
|
32
|
+
* @module @nitida/sdk/native
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export type {
|
|
36
|
+
CompressedResult,
|
|
37
|
+
CompressionError,
|
|
38
|
+
CompressionOptions,
|
|
39
|
+
CompressionStatusKey,
|
|
40
|
+
} from "@nitida/asset-compressor-native";
|
|
41
|
+
export {
|
|
42
|
+
type CompressInput,
|
|
43
|
+
compressImage,
|
|
44
|
+
compressImages,
|
|
45
|
+
DEFAULT_COMPRESSION_OPTIONS,
|
|
46
|
+
DEFAULT_NATIVE_CONCURRENCY,
|
|
47
|
+
MAX_UPLOAD_DIMENSION,
|
|
48
|
+
} from "@nitida/asset-compressor-native";
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nitida/sdk/react — React hooks layered on top of the universal SDK.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a subpath so the SSR-safe core (`@nitida/sdk`) stays
|
|
5
|
+
* dependency-free of react. Import only what you need:
|
|
6
|
+
*
|
|
7
|
+
* import { useSlot, useSlots, AquienpzProvider } from "@nitida/sdk/react";
|
|
8
|
+
*
|
|
9
|
+
* Pattern: wrap your app in `<AquienpzProvider client={…}>` once at
|
|
10
|
+
* the root; hooks read the client from context. No prop-drilling.
|
|
11
|
+
* @module @nitida/sdk/react
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createContext,
|
|
16
|
+
createElement,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
useContext,
|
|
19
|
+
useEffect,
|
|
20
|
+
useMemo,
|
|
21
|
+
useState,
|
|
22
|
+
} from "react";
|
|
23
|
+
import type { AquienpzClient, ResolveSlotOptions, SlotResolution } from "..";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Provider
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const ClientContext = createContext<AquienpzClient | null>(null);
|
|
30
|
+
|
|
31
|
+
export function AquienpzProvider(props: {
|
|
32
|
+
client: AquienpzClient;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}): ReactNode {
|
|
35
|
+
return createElement(
|
|
36
|
+
ClientContext.Provider,
|
|
37
|
+
{ value: props.client },
|
|
38
|
+
props.children,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useAquienpzClient(): AquienpzClient {
|
|
43
|
+
const client = useContext(ClientContext);
|
|
44
|
+
if (!client) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"useAquienpzClient: wrap your app in <AquienpzProvider client={…}>.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return client;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Slot hooks
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
type SlotState = {
|
|
57
|
+
/** Resolved DTO (`null` while loading or unbound). */
|
|
58
|
+
resolution: SlotResolution | null;
|
|
59
|
+
/** Convenience: the CDN URL, when resolved. */
|
|
60
|
+
url: string | null;
|
|
61
|
+
isLoading: boolean;
|
|
62
|
+
error: Error | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const emptyState: SlotState = {
|
|
66
|
+
resolution: null,
|
|
67
|
+
url: null,
|
|
68
|
+
isLoading: true,
|
|
69
|
+
error: null,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to a single slot. Re-resolves when the key changes or the
|
|
74
|
+
* cache is invalidated. Returns `{resolution, url, isLoading, error}`.
|
|
75
|
+
*/
|
|
76
|
+
export function useSlot(
|
|
77
|
+
slotKey: string,
|
|
78
|
+
options: ResolveSlotOptions = {},
|
|
79
|
+
): SlotState {
|
|
80
|
+
const client = useAquienpzClient();
|
|
81
|
+
const [state, setState] = useState<SlotState>(emptyState);
|
|
82
|
+
|
|
83
|
+
// Stabilize options across renders so the effect only refires on the
|
|
84
|
+
// values that matter.
|
|
85
|
+
const presetKey = options.preset ?? "";
|
|
86
|
+
const ttlMs = options.ttlMs ?? 60_000;
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
let alive = true;
|
|
90
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
91
|
+
client.slots
|
|
92
|
+
.resolve(slotKey, { preset: options.preset, ttlMs })
|
|
93
|
+
.then((resolution) => {
|
|
94
|
+
if (!alive) return;
|
|
95
|
+
setState({
|
|
96
|
+
resolution,
|
|
97
|
+
url: resolution.url,
|
|
98
|
+
isLoading: false,
|
|
99
|
+
error: null,
|
|
100
|
+
});
|
|
101
|
+
})
|
|
102
|
+
.catch((err: unknown) => {
|
|
103
|
+
if (!alive) return;
|
|
104
|
+
setState({
|
|
105
|
+
resolution: null,
|
|
106
|
+
url: null,
|
|
107
|
+
isLoading: false,
|
|
108
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
return () => {
|
|
112
|
+
alive = false;
|
|
113
|
+
};
|
|
114
|
+
}, [client, slotKey, presetKey, ttlMs, options.preset]);
|
|
115
|
+
|
|
116
|
+
return state;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Bulk version — fetches N keys in one round-trip. Returns a map keyed
|
|
121
|
+
* by slot key. Pass a STABLE array reference (memoize with useMemo) to
|
|
122
|
+
* avoid re-fetches on every render.
|
|
123
|
+
*/
|
|
124
|
+
export function useSlots(
|
|
125
|
+
slotKeys: string[],
|
|
126
|
+
options: ResolveSlotOptions = {},
|
|
127
|
+
): {
|
|
128
|
+
resolutions: Record<string, SlotResolution>;
|
|
129
|
+
isLoading: boolean;
|
|
130
|
+
error: Error | null;
|
|
131
|
+
} {
|
|
132
|
+
const client = useAquienpzClient();
|
|
133
|
+
const keysHash = useMemo(() => slotKeys.join("|"), [slotKeys]);
|
|
134
|
+
const presetKey = options.preset ?? "";
|
|
135
|
+
const ttlMs = options.ttlMs ?? 60_000;
|
|
136
|
+
|
|
137
|
+
const [state, setState] = useState<{
|
|
138
|
+
resolutions: Record<string, SlotResolution>;
|
|
139
|
+
isLoading: boolean;
|
|
140
|
+
error: Error | null;
|
|
141
|
+
}>({ resolutions: {}, isLoading: true, error: null });
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
let alive = true;
|
|
145
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
146
|
+
client.slots
|
|
147
|
+
.resolveMany(slotKeys, { preset: options.preset, ttlMs })
|
|
148
|
+
.then((resolutions) => {
|
|
149
|
+
if (!alive) return;
|
|
150
|
+
setState({ resolutions, isLoading: false, error: null });
|
|
151
|
+
})
|
|
152
|
+
.catch((err: unknown) => {
|
|
153
|
+
if (!alive) return;
|
|
154
|
+
setState({
|
|
155
|
+
resolutions: {},
|
|
156
|
+
isLoading: false,
|
|
157
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
return () => {
|
|
161
|
+
alive = false;
|
|
162
|
+
};
|
|
163
|
+
// slotKeys is hashed via keysHash; using it directly here would
|
|
164
|
+
// re-fire on every render (new array identity each render).
|
|
165
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: keysHash captures slotKeys identity
|
|
166
|
+
}, [client, keysHash, presetKey, ttlMs]);
|
|
167
|
+
|
|
168
|
+
return state;
|
|
169
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nitida/sdk/server — server-safe entry point.
|
|
3
|
+
*
|
|
4
|
+
* Use this subpath from Node.js, Bun, Cloud Run, Lambda, Vercel Functions,
|
|
5
|
+
* edge runtimes, agents, cron jobs, BFFs — anywhere there's no `window`
|
|
6
|
+
* and you want a hard guarantee that no browser-only code lands in your
|
|
7
|
+
* bundle. The constructor REQUIRES `apiKey`; the type from `/web` omits
|
|
8
|
+
* it, so the two modes never confuse each other.
|
|
9
|
+
*
|
|
10
|
+
* import { AquienpzClient } from "@nitida/sdk/server";
|
|
11
|
+
*
|
|
12
|
+
* const aq = new AquienpzClient({
|
|
13
|
+
* endpoint: process.env.ASSET_MANAGER_URL!,
|
|
14
|
+
* apiKey: process.env.ASSET_MANAGER_API_KEY!, // <- required
|
|
15
|
+
* tenantCode: "realtyone-cr",
|
|
16
|
+
* tenantId: 1,
|
|
17
|
+
* // signingKey: optional, only for `aq.transform(..., { sign: true })`
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* const asset = await aq.assets.byHash(sha256);
|
|
21
|
+
* const hero = aq.transform(asset, { width: 1920 });
|
|
22
|
+
*
|
|
23
|
+
* // Typical BFF use: proxy a browser request through to aquienpz.
|
|
24
|
+
* // The browser side calls `@nitida/sdk/web` against `/api/am/...`
|
|
25
|
+
* // and your route handler forwards here with the real API key.
|
|
26
|
+
*
|
|
27
|
+
* What you get:
|
|
28
|
+
* - `AquienpzClient` (slots/assets/usage APIs over plain fetch)
|
|
29
|
+
* - URL builders: `getAssetUrl`, `getTransformUrl`, `getTransformSrcSet`,
|
|
30
|
+
* `getHlsStreamingUrl`, `extractAssetSha`, `signTransformUrl`
|
|
31
|
+
* - `aq.upload(bytes)` works with `Uint8Array` (Node 18+ / Bun ship Blob
|
|
32
|
+
* globally; File-API workflows are documented on the /web subpath instead)
|
|
33
|
+
*
|
|
34
|
+
* What's NOT here (use `@nitida/sdk/web` instead):
|
|
35
|
+
* - `createWebUploader` (multipart UploadTask with IndexedDB resume)
|
|
36
|
+
* - `compressImage` (browser-side compressorjs + heic2any)
|
|
37
|
+
*
|
|
38
|
+
* Stripe/Cloudinary historically shipped two separate packages
|
|
39
|
+
* (`stripe` vs `@stripe/stripe-js`, `cloudinary` vs `@cloudinary/url-gen`)
|
|
40
|
+
* for this split. Modern providers (Vercel Blob, Uploadthing, Better
|
|
41
|
+
* Auth, AI SDK) use subpaths within one package — same tree-shaking
|
|
42
|
+
* guarantees, single version, no drift. We follow that pattern.
|
|
43
|
+
* @module @nitida/sdk/server
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
type AquienpzClientOptions,
|
|
48
|
+
AquienpzClient as BaseAquienpzClient,
|
|
49
|
+
} from "..";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Server-side constructor options — `apiKey` is REQUIRED here. Use this
|
|
53
|
+
* type whenever you build a client behind a process boundary (Node, Bun,
|
|
54
|
+
* Cloud Run, Vercel Functions, edge runtimes, BFFs).
|
|
55
|
+
*
|
|
56
|
+
* const aq = new AquienpzClient({
|
|
57
|
+
* endpoint: process.env.ASSET_MANAGER_URL!,
|
|
58
|
+
* apiKey: process.env.ASSET_MANAGER_API_KEY!,
|
|
59
|
+
* tenantCode: "realtyone-cr",
|
|
60
|
+
* tenantId: 1,
|
|
61
|
+
* });
|
|
62
|
+
*/
|
|
63
|
+
export type ServerClientOptions = Required<
|
|
64
|
+
Pick<AquienpzClientOptions, "endpoint" | "apiKey" | "tenantCode" | "tenantId">
|
|
65
|
+
> &
|
|
66
|
+
Pick<AquienpzClientOptions, "cdnBase" | "signingKey">;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Server-safe `AquienpzClient` — same runtime as the root class, but the
|
|
70
|
+
* constructor type enforces `apiKey` so misconfiguration is a TS build
|
|
71
|
+
* error, not a runtime 401.
|
|
72
|
+
*/
|
|
73
|
+
export class AquienpzClient extends BaseAquienpzClient {
|
|
74
|
+
constructor(opts: ServerClientOptions) {
|
|
75
|
+
super(opts);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
type AquienpzClientOptions,
|
|
81
|
+
type AssetDTO,
|
|
82
|
+
type AssetVariant,
|
|
83
|
+
type ComposeMarketingComposition,
|
|
84
|
+
type ComposeMarketingOptions,
|
|
85
|
+
type ComposeMarketingResult,
|
|
86
|
+
type ComposeMarketingSegment,
|
|
87
|
+
type CompressOptions,
|
|
88
|
+
// URL builders & related types (re-export from asset-client via root).
|
|
89
|
+
computeVariantDimensions,
|
|
90
|
+
extractAssetSha,
|
|
91
|
+
getAssetDimensions,
|
|
92
|
+
getAssetSrcSet,
|
|
93
|
+
getAssetUrl,
|
|
94
|
+
getHlsStreamingUrl,
|
|
95
|
+
getSignedTransformUrl,
|
|
96
|
+
getTenantId,
|
|
97
|
+
getTransformSrcSet,
|
|
98
|
+
getTransformUrl,
|
|
99
|
+
getVideoTransformUrl,
|
|
100
|
+
hasPreset,
|
|
101
|
+
type PresignUploadUrlOptions,
|
|
102
|
+
type RegenerateResult,
|
|
103
|
+
type ResolveSlotOptions,
|
|
104
|
+
type SignedTransformOptions,
|
|
105
|
+
type SlotDTO,
|
|
106
|
+
type SlotHistoryEntry,
|
|
107
|
+
type SlotResolution,
|
|
108
|
+
serializeTransform,
|
|
109
|
+
setTenantId,
|
|
110
|
+
signTransformUrl,
|
|
111
|
+
type TransformEffect,
|
|
112
|
+
type TransformFit,
|
|
113
|
+
type TransformFormat,
|
|
114
|
+
type TransformGravity,
|
|
115
|
+
type TransformOptions,
|
|
116
|
+
type UploadOptions,
|
|
117
|
+
type UploadResult,
|
|
118
|
+
type UploadUrlResult,
|
|
119
|
+
type UsageDailyPoint,
|
|
120
|
+
type UsagePerKey,
|
|
121
|
+
type UsageSnapshot,
|
|
122
|
+
type UsageWindow,
|
|
123
|
+
type VariantPreset,
|
|
124
|
+
} from "..";
|
package/src/web/index.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nitida/sdk/web — browser entry point (BFF-proxy mode).
|
|
3
|
+
*
|
|
4
|
+
* Use this subpath from any browser context (Next.js client components,
|
|
5
|
+
* Vite/CRA SPAs, browser extensions, web workers — anywhere `window`
|
|
6
|
+
* exists). Bundles the core `AquienpzClient` PLUS browser-only helpers
|
|
7
|
+
* (client-side image compression).
|
|
8
|
+
*
|
|
9
|
+
* // 1. In a client component (browser):
|
|
10
|
+
* import { AquienpzClient } from "@nitida/sdk/web";
|
|
11
|
+
*
|
|
12
|
+
* const aq = new AquienpzClient({
|
|
13
|
+
* // Point at your BFF route — the SDK calls
|
|
14
|
+
* // `${endpoint}/assets/by-hash/...`, `${endpoint}/slots/...`, etc.
|
|
15
|
+
* endpoint: "/api/am", // relative ⇒ same-origin proxy
|
|
16
|
+
* tenantCode: "realtyone-cr",
|
|
17
|
+
* tenantId: 1,
|
|
18
|
+
* // NO apiKey — the type strips it. Your BFF injects the bearer.
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* const result = await aq.upload(file, { compress: true });
|
|
22
|
+
*
|
|
23
|
+
* // 2. In a Next.js route handler (server) — proxy through to aquienpz:
|
|
24
|
+
* // app/api/am/[...path]/route.ts
|
|
25
|
+
* export async function GET(req: Request, { params }: ...) {
|
|
26
|
+
* const path = params.path.join("/");
|
|
27
|
+
* const url = `${process.env.AQUIENPZ_URL}/${path}${new URL(req.url).search}`;
|
|
28
|
+
* return fetch(url, {
|
|
29
|
+
* headers: {
|
|
30
|
+
* Authorization: `Bearer ${process.env.AQUIENPZ_API_KEY!}`,
|
|
31
|
+
* "X-Tenant-Code": "realtyone-cr",
|
|
32
|
+
* },
|
|
33
|
+
* });
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* Why no `apiKey` in the type: in browser code, a long-lived bearer key
|
|
37
|
+
* would be shipped to every visitor in `NEXT_PUBLIC_*`. The type omits
|
|
38
|
+
* it physically (TS build error at the call site) so security mistakes
|
|
39
|
+
* are loud instead of silent. Same pattern as `@vercel/blob/client`,
|
|
40
|
+
* `better-auth/client`, AI SDK's `/edge` subpath.
|
|
41
|
+
*
|
|
42
|
+
* Need to ship a server-rendered storefront with a long-lived key?
|
|
43
|
+
* Import from `@nitida/sdk/server` instead — that subpath requires
|
|
44
|
+
* `apiKey` and bundles cleanly only in Node/Bun/edge runtimes.
|
|
45
|
+
*
|
|
46
|
+
* Peer deps (auto-installed via npm peer deps — declared optional):
|
|
47
|
+
* - `@aquienpz/asset-uploader-web` for multipart uploads
|
|
48
|
+
* - `@nitida/asset-compressor-web` for client-side compression
|
|
49
|
+
*
|
|
50
|
+
* Both are lazy-imported; bundles that never call into them skip the cost.
|
|
51
|
+
* @module @nitida/sdk/web
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import {
|
|
55
|
+
type AquienpzClientOptions,
|
|
56
|
+
AquienpzClient as BaseAquienpzClient,
|
|
57
|
+
} from "..";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Browser-safe constructor options for `@nitida/sdk/web`.
|
|
61
|
+
*
|
|
62
|
+
* Identical to the root `AquienpzClientOptions` except `apiKey` and
|
|
63
|
+
* `signingKey` are **physically absent** — passing them is a TypeScript
|
|
64
|
+
* build error, not a runtime warning. In BFF-proxy mode your route
|
|
65
|
+
* handler injects the bearer header server-side; the browser never
|
|
66
|
+
* sees the long-lived key.
|
|
67
|
+
*
|
|
68
|
+
* const aq = new AquienpzClient({
|
|
69
|
+
* endpoint: "/api/am", // OK: relative → same-origin BFF
|
|
70
|
+
* tenantCode: "realtyone-cr",
|
|
71
|
+
* tenantId: 1,
|
|
72
|
+
* // apiKey: "amk_rt_...", // ERROR: TS error: not assignable
|
|
73
|
+
* });
|
|
74
|
+
*/
|
|
75
|
+
export type WebClientOptions = Omit<
|
|
76
|
+
AquienpzClientOptions,
|
|
77
|
+
"apiKey" | "signingKey"
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Browser-safe `AquienpzClient` — same runtime as the root class, but
|
|
82
|
+
* the constructor's type rejects `apiKey` / `signingKey`. Calls go
|
|
83
|
+
* through your BFF (typically a same-origin route like `/api/am/...`).
|
|
84
|
+
*
|
|
85
|
+
* For server-side instantiation (Node/Bun/edge), import from
|
|
86
|
+
* `@nitida/sdk/server` instead.
|
|
87
|
+
*/
|
|
88
|
+
export class AquienpzClient extends BaseAquienpzClient {
|
|
89
|
+
constructor(opts: WebClientOptions) {
|
|
90
|
+
super(opts as AquienpzClientOptions);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Re-export the rest of the surface (URL builders, types, etc.) so
|
|
95
|
+
// browser consumers can satisfy 100% of their needs through this
|
|
96
|
+
// single subpath.
|
|
97
|
+
export {
|
|
98
|
+
type AssetDTO,
|
|
99
|
+
type AssetVariant,
|
|
100
|
+
type CompressOptions as ClientCompressOptions,
|
|
101
|
+
computeVariantDimensions,
|
|
102
|
+
extractAssetSha,
|
|
103
|
+
getAssetDimensions,
|
|
104
|
+
getAssetSrcSet,
|
|
105
|
+
getAssetUrl,
|
|
106
|
+
getHlsStreamingUrl,
|
|
107
|
+
getSignedTransformUrl,
|
|
108
|
+
getTransformSrcSet,
|
|
109
|
+
getTransformUrl,
|
|
110
|
+
getVideoTransformUrl,
|
|
111
|
+
hasPreset,
|
|
112
|
+
type RegenerateResult,
|
|
113
|
+
type ResolveSlotOptions,
|
|
114
|
+
type SignedTransformOptions,
|
|
115
|
+
type SlotDTO,
|
|
116
|
+
type SlotHistoryEntry,
|
|
117
|
+
type SlotResolution,
|
|
118
|
+
serializeTransform,
|
|
119
|
+
signTransformUrl,
|
|
120
|
+
type TransformEffect,
|
|
121
|
+
type TransformFit,
|
|
122
|
+
type TransformFormat,
|
|
123
|
+
type TransformGravity,
|
|
124
|
+
type TransformOptions,
|
|
125
|
+
type UploadOptions,
|
|
126
|
+
type UploadResult,
|
|
127
|
+
type UsageDailyPoint,
|
|
128
|
+
type UsagePerKey,
|
|
129
|
+
type UsageSnapshot,
|
|
130
|
+
type UsageWindow,
|
|
131
|
+
type VariantPreset,
|
|
132
|
+
} from "..";
|
|
133
|
+
|
|
134
|
+
// Multipart uploader helpers are NOT re-exported from this subpath.
|
|
135
|
+
//
|
|
136
|
+
// `@aquienpz/asset-uploader-web` is an optional peer-dep that isn't on
|
|
137
|
+
// npm yet, so a static `import` at module top broke every consumer of
|
|
138
|
+
// `@nitida/sdk/web` (the bundler tries to resolve before the optional
|
|
139
|
+
// peer check kicks in). The old `createWebUploader` + `UploadTask`
|
|
140
|
+
// re-exports also don't fit BFF-proxy mode (they need a raw `authToken`
|
|
141
|
+
// the BFF doesn't expose). The cleaner story is:
|
|
142
|
+
//
|
|
143
|
+
// - Files ≤50MB → `aq.upload(file)` (single PUT, atomic, BFF-friendly)
|
|
144
|
+
// - Files >50MB → roadmap item for v0.18 (BFF-minted short-lived token
|
|
145
|
+
// + resumable multipart). Track at
|
|
146
|
+
// https://github.com/espaciofuturoio/aquienpz/issues
|
|
147
|
+
//
|
|
148
|
+
// Apps that need `UploadTask` directly today should depend on
|
|
149
|
+
// `@aquienpz/asset-uploader-web` themselves once it's published.
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Client-side compression (Phase 1.5)
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
//
|
|
155
|
+
// Wraps `@nitida/asset-compressor-web` with the SDK's
|
|
156
|
+
// `DEFAULT_COMPRESSION_OPTIONS` — values derived from the realtyone-cr
|
|
157
|
+
// webapp's production tuning (LISTING_STANDARD_*: quality 0.80, max-edge
|
|
158
|
+
// 3840px, WebP output, 5MB PNG→JPEG threshold, strict mode). Those defaults
|
|
159
|
+
// differ from `@nitida/asset-compressor-web`'s own defaults (0.85 / 2880 /
|
|
160
|
+
// convertSize=0) which target a slightly different audience; SDK callers
|
|
161
|
+
// get the webapp-tuned values, package-direct callers keep theirs.
|
|
162
|
+
|
|
163
|
+
// Types inlined from `@nitida/asset-compressor-web` to avoid even a
|
|
164
|
+
// type-only `import` of the optional peer dep — TypeScript's `import type`
|
|
165
|
+
// is erased at runtime, but some bundlers (Turbopack as of 2026-05) still
|
|
166
|
+
// scan and warn on the path. Keeping these types local makes `/web`
|
|
167
|
+
// importable in any project regardless of whether the compressor is
|
|
168
|
+
// installed.
|
|
169
|
+
type CompressionOptions = {
|
|
170
|
+
quality?: number;
|
|
171
|
+
mimeType?: "image/jpeg" | "image/webp";
|
|
172
|
+
maxWidth?: number;
|
|
173
|
+
maxHeight?: number;
|
|
174
|
+
convertSize?: number;
|
|
175
|
+
keepOriginalDimensions?: boolean;
|
|
176
|
+
};
|
|
177
|
+
type CompressionStatusKey =
|
|
178
|
+
| "convertingHeic"
|
|
179
|
+
| "compressing"
|
|
180
|
+
| "compressingKeepingDimensions"
|
|
181
|
+
| (string & {});
|
|
182
|
+
|
|
183
|
+
export type CompressStage =
|
|
184
|
+
| "convertingHeic"
|
|
185
|
+
| "compressing"
|
|
186
|
+
| "compressingKeepingDimensions";
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Subset of compressorjs options exposed by the SDK. Matches the
|
|
190
|
+
* webapp's `DEFAULT_COMPRESSION_OPTIONS` shape from the realtyone-cr
|
|
191
|
+
* production back-office uploader.
|
|
192
|
+
*/
|
|
193
|
+
export type CompressOptions = {
|
|
194
|
+
/** 0..1. Default 0.80 (`LISTING_STANDARD_IMAGE_QUALITY / 100`). */
|
|
195
|
+
quality?: number;
|
|
196
|
+
/** Output format. Default `"image/webp"` (`DEFAULT_IMAGE_TARGET_FORMAT`). */
|
|
197
|
+
mimeType?: "image/jpeg" | "image/webp";
|
|
198
|
+
/** Max edge in pixels. Default 3840 (`LISTING_STANDARD_IMAGE_WIDTH`). */
|
|
199
|
+
maxWidth?: number;
|
|
200
|
+
/** Default 3840 (`LISTING_STANDARD_IMAGE_HEIGHT`). */
|
|
201
|
+
maxHeight?: number;
|
|
202
|
+
/**
|
|
203
|
+
* compressorjs `convertSize`: PNG > this byte count auto-converts to
|
|
204
|
+
* JPEG before the quality pass. Default 5 MB (5 * 1024 * 1024).
|
|
205
|
+
*/
|
|
206
|
+
convertSize?: number;
|
|
207
|
+
/** compressorjs strict mode. Default true. */
|
|
208
|
+
strict?: boolean;
|
|
209
|
+
/** Skip the resize pass (keep original dimensions). Default false. */
|
|
210
|
+
keepOriginalDimensions?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Convert HEIC/HEIF inputs to JPEG via heic2any first. Default true.
|
|
213
|
+
* Has no effect on non-HEIC inputs.
|
|
214
|
+
*/
|
|
215
|
+
convertHeic?: boolean;
|
|
216
|
+
/** Optional progress hook. Useful for UI status indicators. */
|
|
217
|
+
onProgress?: (stage: CompressStage) => void;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Webapp-tuned defaults (LISTING_STANDARD_*). These intentionally differ
|
|
222
|
+
* from `@nitida/asset-compressor-web`'s package-level defaults — the
|
|
223
|
+
* SDK overrides at call time.
|
|
224
|
+
*/
|
|
225
|
+
export const DEFAULT_COMPRESSION_OPTIONS: Required<
|
|
226
|
+
Omit<CompressOptions, "keepOriginalDimensions" | "convertHeic" | "onProgress">
|
|
227
|
+
> = {
|
|
228
|
+
quality: 0.8,
|
|
229
|
+
mimeType: "image/webp",
|
|
230
|
+
maxWidth: 3840,
|
|
231
|
+
maxHeight: 3840,
|
|
232
|
+
convertSize: 5 * 1024 * 1024,
|
|
233
|
+
strict: true,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export type CompressResult = {
|
|
237
|
+
/** Compressed bytes wrapped as a Blob (browser-native). */
|
|
238
|
+
blob: Blob;
|
|
239
|
+
/** Size of the source before compression, in bytes. */
|
|
240
|
+
originalBytes: number;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Compress a single image. Browser-only; in Node/Bun this is a no-op that
|
|
245
|
+
* returns the input unchanged (with a warning).
|
|
246
|
+
*
|
|
247
|
+
* Dynamically imports `@nitida/asset-compressor-web` so apps that never
|
|
248
|
+
* call `compress: true` don't pay the compressorjs + heic2any bundle cost.
|
|
249
|
+
*/
|
|
250
|
+
export async function compressImage(
|
|
251
|
+
file: File | Blob,
|
|
252
|
+
opts: CompressOptions = {},
|
|
253
|
+
): Promise<CompressResult> {
|
|
254
|
+
if (typeof window === "undefined") {
|
|
255
|
+
console.warn(
|
|
256
|
+
"[@nitida/sdk/web] compressImage called outside a browser — returning input unchanged.",
|
|
257
|
+
);
|
|
258
|
+
return { blob: file, originalBytes: file.size };
|
|
259
|
+
}
|
|
260
|
+
const finalOpts: Required<
|
|
261
|
+
Omit<
|
|
262
|
+
CompressOptions,
|
|
263
|
+
"keepOriginalDimensions" | "convertHeic" | "onProgress"
|
|
264
|
+
>
|
|
265
|
+
> = { ...DEFAULT_COMPRESSION_OPTIONS, ...opts };
|
|
266
|
+
// Map SDK CompressOptions → package CompressionOptions, omitting fields
|
|
267
|
+
// the package doesn't understand (`strict`, `convertHeic`).
|
|
268
|
+
const packageOpts: CompressionOptions = {
|
|
269
|
+
quality: finalOpts.quality,
|
|
270
|
+
mimeType: finalOpts.mimeType,
|
|
271
|
+
maxWidth: finalOpts.maxWidth,
|
|
272
|
+
maxHeight: finalOpts.maxHeight,
|
|
273
|
+
convertSize: finalOpts.convertSize,
|
|
274
|
+
keepOriginalDimensions: opts.keepOriginalDimensions ?? false,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// String indirection defeats bundler static-analysis of dynamic
|
|
278
|
+
// imports — Turbopack/webpack will leave this for the runtime to
|
|
279
|
+
// resolve instead of failing the build when the optional peer dep
|
|
280
|
+
// isn't installed. See feedback_bun_compile_dynamic_imports (same
|
|
281
|
+
// trick used in reverse to opt OUT of bundling).
|
|
282
|
+
const compressorPkg = "@nitida/asset-compressor-web";
|
|
283
|
+
const { compressImage: doCompress } = (await import(
|
|
284
|
+
/* @vite-ignore */ /* webpackIgnore: true */ compressorPkg
|
|
285
|
+
)) as {
|
|
286
|
+
compressImage: (
|
|
287
|
+
args: unknown,
|
|
288
|
+
) => Promise<{ blob: Blob; originalSize: number }>;
|
|
289
|
+
};
|
|
290
|
+
const r = await doCompress({
|
|
291
|
+
blob: file,
|
|
292
|
+
filename: file instanceof File ? file.name : "blob",
|
|
293
|
+
options: packageOpts,
|
|
294
|
+
statusCallback: opts.onProgress
|
|
295
|
+
? (status: CompressionStatusKey) => {
|
|
296
|
+
if (
|
|
297
|
+
status === "convertingHeic" ||
|
|
298
|
+
status === "compressing" ||
|
|
299
|
+
status === "compressingKeepingDimensions"
|
|
300
|
+
) {
|
|
301
|
+
opts.onProgress?.(status as CompressStage);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
: undefined,
|
|
305
|
+
});
|
|
306
|
+
return { blob: r.blob, originalBytes: r.originalSize };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Compress a batch of images with adaptive concurrency
|
|
311
|
+
* (navigator.hardwareConcurrency, capped at 8). Failures throw — pass
|
|
312
|
+
* `try/catch` per file if you want per-file resilience, or call
|
|
313
|
+
* `compressImage` in a loop yourself.
|
|
314
|
+
*/
|
|
315
|
+
export async function compressImages(
|
|
316
|
+
files: Array<File | Blob>,
|
|
317
|
+
opts: CompressOptions = {},
|
|
318
|
+
): Promise<CompressResult[]> {
|
|
319
|
+
if (typeof window === "undefined") {
|
|
320
|
+
console.warn(
|
|
321
|
+
"[@nitida/sdk/web] compressImages called outside a browser — returning inputs unchanged.",
|
|
322
|
+
);
|
|
323
|
+
return files.map((f) => ({ blob: f, originalBytes: f.size }));
|
|
324
|
+
}
|
|
325
|
+
// Sequential per-file via the single-file helper — keeps the cancellation
|
|
326
|
+
// story simple and the API surface narrow. The package's
|
|
327
|
+
// `compressImages` adds an array-of-CompressedResult shape with
|
|
328
|
+
// per-file ids that's overkill for the SDK's stated surface.
|
|
329
|
+
const results: CompressResult[] = [];
|
|
330
|
+
for (const f of files) {
|
|
331
|
+
results.push(await compressImage(f, opts));
|
|
332
|
+
}
|
|
333
|
+
return results;
|
|
334
|
+
}
|