@giveitsmaller/sdk 0.6.0 → 0.8.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/dist/_audit.js +71 -0
- package/dist/builder.d.ts +406 -0
- package/dist/builder.js +706 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +42 -6
- package/dist/credentials.d.ts +61 -0
- package/dist/credentials.js +200 -0
- package/dist/ergonomic/preset_resolver.d.ts +75 -0
- package/dist/ergonomic/preset_resolver.js +568 -0
- package/dist/ergonomic/presets/_translate.d.ts +11 -0
- package/dist/ergonomic/presets/_translate.js +35 -0
- package/dist/ergonomic/presets/audio_compress.d.ts +16 -0
- package/dist/ergonomic/presets/audio_compress.js +45 -0
- package/dist/ergonomic/presets/document_epub_compress.d.ts +14 -0
- package/dist/ergonomic/presets/document_epub_compress.js +34 -0
- package/dist/ergonomic/presets/document_odf_compress.d.ts +14 -0
- package/dist/ergonomic/presets/document_odf_compress.js +34 -0
- package/dist/ergonomic/presets/document_office_compress.d.ts +16 -0
- package/dist/ergonomic/presets/document_office_compress.js +40 -0
- package/dist/ergonomic/presets/document_pdf_compress.d.ts +14 -0
- package/dist/ergonomic/presets/document_pdf_compress.js +35 -0
- package/dist/ergonomic/presets/image_compress.d.ts +43 -0
- package/dist/ergonomic/presets/image_compress.js +95 -0
- package/dist/ergonomic/presets/index.d.ts +77 -0
- package/dist/ergonomic/presets/index.js +216 -0
- package/dist/ergonomic/presets/video_compress.d.ts +30 -0
- package/dist/ergonomic/presets/video_compress.js +83 -0
- package/dist/errors.d.ts +196 -1
- package/dist/errors.js +216 -0
- package/dist/file-first.d.ts +284 -0
- package/dist/file-first.js +445 -0
- package/dist/generated/sdk_spec/enums.d.ts +195 -0
- package/dist/generated/sdk_spec/enums.js +127 -0
- package/dist/generated/sdk_spec/errors.d.ts +16 -0
- package/dist/generated/sdk_spec/errors.js +523 -0
- package/dist/generated/sdk_spec/index.d.ts +4 -0
- package/dist/generated/sdk_spec/index.js +7 -0
- package/dist/generated/sdk_spec/presets.d.ts +6 -0
- package/dist/generated/sdk_spec/presets.js +157 -0
- package/dist/generated/sdk_spec/version.d.ts +3 -0
- package/dist/generated/sdk_spec/version.js +6 -0
- package/dist/gisl.d.ts +122 -0
- package/dist/gisl.js +283 -0
- package/dist/http-downloader.d.ts +9 -0
- package/dist/http-downloader.js +55 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +45 -4
- package/dist/merge.d.ts +142 -0
- package/dist/merge.js +411 -0
- package/dist/types.d.ts +12 -14
- package/dist/types.js +18 -0
- package/package.json +3 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// CODE GENERATED — DO NOT EDIT.
|
|
2
|
+
// Source: compression_contracts/sdk-spec/ (see sdk-spec/README.md).
|
|
3
|
+
// Regenerate with: scripts/generate.py.
|
|
4
|
+
export const PRESETS = Object.freeze({
|
|
5
|
+
"image_compress": Object.freeze({
|
|
6
|
+
Size: Object.freeze({
|
|
7
|
+
"mode": "Lossy",
|
|
8
|
+
"quality": 65,
|
|
9
|
+
"metadata": "All",
|
|
10
|
+
"iccProfile": "Strip",
|
|
11
|
+
"autoOrient": true,
|
|
12
|
+
"progressive": true,
|
|
13
|
+
"outputFormat": "Smallest",
|
|
14
|
+
}),
|
|
15
|
+
Balanced: Object.freeze({
|
|
16
|
+
"mode": "Auto",
|
|
17
|
+
"quality": 80,
|
|
18
|
+
"metadata": "Sensitive",
|
|
19
|
+
"iccProfile": "Preserve",
|
|
20
|
+
"autoOrient": true,
|
|
21
|
+
"progressive": true,
|
|
22
|
+
"outputFormat": "Auto",
|
|
23
|
+
}),
|
|
24
|
+
Quality: Object.freeze({
|
|
25
|
+
"mode": "Lossless",
|
|
26
|
+
"metadata": "None",
|
|
27
|
+
"iccProfile": "Preserve",
|
|
28
|
+
"autoOrient": true,
|
|
29
|
+
"progressive": true,
|
|
30
|
+
"outputFormat": "Original",
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
"audio_compress": Object.freeze({
|
|
34
|
+
Size: Object.freeze({
|
|
35
|
+
"bitrate": "_96",
|
|
36
|
+
"sampleRate": "_44100",
|
|
37
|
+
"normalize": true,
|
|
38
|
+
}),
|
|
39
|
+
Balanced: Object.freeze({
|
|
40
|
+
"bitrate": "_192",
|
|
41
|
+
"sampleRate": "_44100",
|
|
42
|
+
"normalize": true,
|
|
43
|
+
}),
|
|
44
|
+
Quality: Object.freeze({
|
|
45
|
+
"bitrate": "_320",
|
|
46
|
+
"sampleRate": "_48000",
|
|
47
|
+
"normalize": false,
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
"video_compress": Object.freeze({
|
|
51
|
+
Size: Object.freeze({
|
|
52
|
+
"codec": "H265",
|
|
53
|
+
"crf": 30,
|
|
54
|
+
"preset": "Slow",
|
|
55
|
+
"faststart": true,
|
|
56
|
+
"audioCodec": "Aac",
|
|
57
|
+
"audioBitrate": "_96",
|
|
58
|
+
}),
|
|
59
|
+
Balanced: Object.freeze({
|
|
60
|
+
"codec": "H264",
|
|
61
|
+
"crf": 23,
|
|
62
|
+
"preset": "Medium",
|
|
63
|
+
"faststart": true,
|
|
64
|
+
"audioCodec": "Aac",
|
|
65
|
+
"audioBitrate": "_128",
|
|
66
|
+
}),
|
|
67
|
+
Quality: Object.freeze({
|
|
68
|
+
"codec": "H264",
|
|
69
|
+
"crf": 18,
|
|
70
|
+
"preset": "Slow",
|
|
71
|
+
"faststart": true,
|
|
72
|
+
"audioCodec": "Aac",
|
|
73
|
+
"audioBitrate": "_192",
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
"document_pdf_compress": Object.freeze({
|
|
77
|
+
Size: Object.freeze({
|
|
78
|
+
"profile": "Max",
|
|
79
|
+
"colorspace": "Grayscale",
|
|
80
|
+
"flattenForms": false,
|
|
81
|
+
}),
|
|
82
|
+
Balanced: Object.freeze({
|
|
83
|
+
"profile": "Web",
|
|
84
|
+
"colorspace": "Unchanged",
|
|
85
|
+
"flattenForms": false,
|
|
86
|
+
}),
|
|
87
|
+
Quality: Object.freeze({
|
|
88
|
+
"profile": "Archive",
|
|
89
|
+
"colorspace": "Unchanged",
|
|
90
|
+
"flattenForms": false,
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
"document_office_compress": Object.freeze({
|
|
94
|
+
Size: Object.freeze({
|
|
95
|
+
"imageQuality": 60,
|
|
96
|
+
"stripMacros": true,
|
|
97
|
+
"stripHiddenData": true,
|
|
98
|
+
"stripUnusedFonts": true,
|
|
99
|
+
}),
|
|
100
|
+
Balanced: Object.freeze({
|
|
101
|
+
"imageQuality": 80,
|
|
102
|
+
"stripMacros": true,
|
|
103
|
+
"stripHiddenData": false,
|
|
104
|
+
"stripUnusedFonts": false,
|
|
105
|
+
}),
|
|
106
|
+
Quality: Object.freeze({
|
|
107
|
+
"imageQuality": 92,
|
|
108
|
+
"stripMacros": false,
|
|
109
|
+
"stripHiddenData": false,
|
|
110
|
+
"stripUnusedFonts": false,
|
|
111
|
+
}),
|
|
112
|
+
}),
|
|
113
|
+
"document_odf_compress": Object.freeze({
|
|
114
|
+
Size: Object.freeze({
|
|
115
|
+
"imageQuality": 60,
|
|
116
|
+
"stripMetadata": true,
|
|
117
|
+
"stripUnusedStyles": true,
|
|
118
|
+
}),
|
|
119
|
+
Balanced: Object.freeze({
|
|
120
|
+
"imageQuality": 80,
|
|
121
|
+
"stripMetadata": true,
|
|
122
|
+
"stripUnusedStyles": false,
|
|
123
|
+
}),
|
|
124
|
+
Quality: Object.freeze({
|
|
125
|
+
"imageQuality": 92,
|
|
126
|
+
"stripMetadata": false,
|
|
127
|
+
"stripUnusedStyles": false,
|
|
128
|
+
}),
|
|
129
|
+
}),
|
|
130
|
+
"document_epub_compress": Object.freeze({
|
|
131
|
+
Size: Object.freeze({
|
|
132
|
+
"imageQuality": 60,
|
|
133
|
+
"fontSubsetting": true,
|
|
134
|
+
"stripUnusedCss": true,
|
|
135
|
+
}),
|
|
136
|
+
Balanced: Object.freeze({
|
|
137
|
+
"imageQuality": 80,
|
|
138
|
+
"fontSubsetting": true,
|
|
139
|
+
"stripUnusedCss": false,
|
|
140
|
+
}),
|
|
141
|
+
Quality: Object.freeze({
|
|
142
|
+
"imageQuality": 92,
|
|
143
|
+
"fontSubsetting": false,
|
|
144
|
+
"stripUnusedCss": false,
|
|
145
|
+
}),
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
/** Lookup the shipped preset cell for (mediaOp, level). Throws on unknown keys. */
|
|
149
|
+
export function shippedDefaultsFor(mediaOp, level) {
|
|
150
|
+
const group = PRESETS[mediaOp];
|
|
151
|
+
if (!group)
|
|
152
|
+
throw new Error(`Unknown preset mediaOp: ${mediaOp}`);
|
|
153
|
+
const cell = group[level];
|
|
154
|
+
if (!cell)
|
|
155
|
+
throw new Error(`Unknown preset level for ${mediaOp}: ${level}`);
|
|
156
|
+
return cell;
|
|
157
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// CODE GENERATED — DO NOT EDIT.
|
|
2
|
+
// Source: compression_contracts/sdk-spec/ (see sdk-spec/README.md).
|
|
3
|
+
// Regenerate with: scripts/generate.py.
|
|
4
|
+
export const SDK_SPEC_VERSION = "1.2.0";
|
|
5
|
+
export const PRESET_VERSION = "1.0";
|
|
6
|
+
export const PRESET_CONFIG_HASH = "sha256:35aeb0b6b86edd9814ace5b75cfeef8a7b1432ecb8e4d5ee8b38d9187a0b45eb";
|
package/dist/gisl.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ergonomic-layer entrypoint for the GISL SDK. Wraps the low-level
|
|
3
|
+
* `GislClient` (transport, multipart, SSE, downloads) with credential-chain
|
|
4
|
+
* resolution + ergonomic factory functions. Designed to feel native to a
|
|
5
|
+
* developer writing `import { gisl } from '@giveitsmaller/sdk'` and then
|
|
6
|
+
* `const client = await gisl.create();`.
|
|
7
|
+
*
|
|
8
|
+
* Scope of this card (T1, `wVU4xHx3`):
|
|
9
|
+
* - `gisl.create()` — full functionality. Resolves credentials + endpoint
|
|
10
|
+
* via `credentials.ts`, fails early with `GislMissingCredentialsError`
|
|
11
|
+
* when no apiKey is found AND the caller hasn't opted into cookie-mode
|
|
12
|
+
* or anonymous mode.
|
|
13
|
+
* - INTERNAL `_gislAnonymous` capability + `ANONYMOUS_ALLOWLIST` constant
|
|
14
|
+
* are wired but NOT publicly exported until the free-tier launch decides
|
|
15
|
+
* which operations are anonymous-capable (plan §12 open decision). A
|
|
16
|
+
* non-empty allowlist + the named export will arrive in a follow-up PR
|
|
17
|
+
* the moment user picks; this card avoids shipping a dead `gisl.anonymous()`.
|
|
18
|
+
*/
|
|
19
|
+
import { GislClient } from './client.js';
|
|
20
|
+
import { GislConfigError, GislFeatureRequiresAuthError, GislMissingCredentialsError } from './errors.js';
|
|
21
|
+
import { type ResolveCredentialsOptions, type ResolveEndpointOptions } from './credentials.js';
|
|
22
|
+
import type { GislClientConfig } from './types.js';
|
|
23
|
+
import { OperationBuilder } from './builder.js';
|
|
24
|
+
import { MergeBuilder, type Asset, type MergeOptions } from './merge.js';
|
|
25
|
+
import { PresetDefaults } from './ergonomic/presets/index.js';
|
|
26
|
+
import { Recipe, type FileInput } from './file-first.js';
|
|
27
|
+
/**
|
|
28
|
+
* Operations that may be invoked on a `gisl.anonymous()` client without
|
|
29
|
+
* raising `GislFeatureRequiresAuthError`. Empty until the free-tier launch
|
|
30
|
+
* decision lands (plan §12). Typed as a `readonly []` tuple (NOT
|
|
31
|
+
* `readonly string[]`) so the audit-gate compile-time assertion in
|
|
32
|
+
* `_audit.ts` fires if a future PR widens this without flipping the
|
|
33
|
+
* parking-decision + adding the public `gisl.anonymous()` export.
|
|
34
|
+
*
|
|
35
|
+
* Consumers must not depend on its emptiness today — only `package.json`
|
|
36
|
+
* `exports` keeps deep-imports blocked; the marker is internal.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare const ANONYMOUS_ALLOWLIST: readonly [];
|
|
41
|
+
export interface GislCreateOptions extends ResolveCredentialsOptions, ResolveEndpointOptions, Omit<GislClientConfig, 'baseUrl' | 'apiKey' | 'useSessionCookie'> {
|
|
42
|
+
/**
|
|
43
|
+
* Layered ergonomic preset defaults (T4a / VhIj4S7T). Built via
|
|
44
|
+
* `presetDefaults().<cell>(level, overrides?)…`. The resolver wiring
|
|
45
|
+
* that consumes this slot lands in T4b — until then, supplying this
|
|
46
|
+
* field is a no-op at workflow-create time.
|
|
47
|
+
*/
|
|
48
|
+
readonly presetDefaults?: PresetDefaults;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Construct an ergonomic-layer client. Resolves the API key + base URL via
|
|
52
|
+
* the credential chain (see `credentials.ts`) and constructs a low-level
|
|
53
|
+
* `GislClient`. Throws `GislMissingCredentialsError` synchronously before
|
|
54
|
+
* any HTTP I/O when no key is found AND neither `useSessionCookie` nor
|
|
55
|
+
* `allowAnonymous` is set.
|
|
56
|
+
*
|
|
57
|
+
* Cookie-mode (`useSessionCookie: true`) explicitly bypasses the missing-
|
|
58
|
+
* credentials check — browser SPAs that drive auth via `client.login()`
|
|
59
|
+
* legitimately have no apiKey at construction time.
|
|
60
|
+
*/
|
|
61
|
+
export declare function create(opts?: GislCreateOptions): Promise<ErgonomicClient>;
|
|
62
|
+
/**
|
|
63
|
+
* The ergonomic-client surface: `GislClient` (verbatim low-level API)
|
|
64
|
+
* plus three ergonomic op-builder factories. Intersection type — at
|
|
65
|
+
* runtime the Proxy synthesises the three methods on-demand. `input`
|
|
66
|
+
* accepts `string | Blob` matching `GislClient.uploadFile` (codex r1
|
|
67
|
+
* low 89cae59f4f04 — Blob/File uploads were previously rejected by the
|
|
68
|
+
* ergonomic factory's narrower string-only typing).
|
|
69
|
+
*/
|
|
70
|
+
export type ErgonomicClient = GislClient & {
|
|
71
|
+
/**
|
|
72
|
+
* File-first entry point (FF2a). Returns an immutable {@link Recipe} you
|
|
73
|
+
* call operations on (`.compress()` / `.convert()` / `.thumbnail()` /
|
|
74
|
+
* `.textWatermark()`), chaining sequentially. A bare string is a filesystem
|
|
75
|
+
* path; pass a {@link FileInput} (e.g. `fileInput.uploadId(...)`) to reuse a
|
|
76
|
+
* pre-uploaded file. `key` is RESULT-addressing only — never input wiring.
|
|
77
|
+
* Execution (`run()`) lands in FF2b.
|
|
78
|
+
*/
|
|
79
|
+
file(input: string | Blob | FileInput, key?: string): Recipe;
|
|
80
|
+
compress(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
|
81
|
+
convert(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
|
82
|
+
thumbnail(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
|
83
|
+
/**
|
|
84
|
+
* Merge ordered-sequence factory (T3). Accepts a variadic list of assets
|
|
85
|
+
* (strings/Blobs/`handle()`/`asset()`) optionally terminated by a
|
|
86
|
+
* `MergeOptions` object. Returns a `MergeBuilder`; pin the play order
|
|
87
|
+
* with `.sequence(...)`.
|
|
88
|
+
*/
|
|
89
|
+
merge(...args: ReadonlyArray<string | Blob | Asset | MergeOptions>): MergeBuilder;
|
|
90
|
+
/**
|
|
91
|
+
* Immutable scoped derive (T4c — `ULAlOP6j`). Returns a new client
|
|
92
|
+
* with `defaults` layered on top of the parent's scoped defaults.
|
|
93
|
+
* Use for the "next N jobs" pattern — e.g. an evening batch needing
|
|
94
|
+
* higher quality without mutating the long-lived parent client.
|
|
95
|
+
*
|
|
96
|
+
* Identity: the derived client shares the SAME underlying low-level
|
|
97
|
+
* transport (baseUrl, apiKey, headers, timeouts, multipart, session
|
|
98
|
+
* cookie) by reference. Safe for concurrent parent + derived use.
|
|
99
|
+
*
|
|
100
|
+
* Merge semantics (codex r2 #5 — scalar leaf): scoped per-cell fields
|
|
101
|
+
* override the parent's scoped where defined; the parent's
|
|
102
|
+
* `client.presetDefaults` layer is unaffected and still contributes
|
|
103
|
+
* fields the scoped layer doesn't set.
|
|
104
|
+
*
|
|
105
|
+
* Does NOT re-resolve credentials. The derive never calls the
|
|
106
|
+
* credential chain or constructor — it composes new closure values
|
|
107
|
+
* over the existing transport.
|
|
108
|
+
*/
|
|
109
|
+
withPresetDefaults(defaults: PresetDefaults): ErgonomicClient;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The `gisl` namespace — primary ergonomic-layer entry point.
|
|
113
|
+
* Exports `gisl.create()` only for v0.7; `gisl.anonymous()` lands once
|
|
114
|
+
* the anonymous-capable operation allowlist is non-empty (plan §12).
|
|
115
|
+
*/
|
|
116
|
+
export declare const gisl: {
|
|
117
|
+
readonly create: typeof create;
|
|
118
|
+
};
|
|
119
|
+
export type { Environment } from './credentials.js';
|
|
120
|
+
/** @internal */
|
|
121
|
+
export declare function _internalAnonymous(opts?: GislCreateOptions): Promise<GislClient>;
|
|
122
|
+
export { GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError };
|
package/dist/gisl.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ergonomic-layer entrypoint for the GISL SDK. Wraps the low-level
|
|
3
|
+
* `GislClient` (transport, multipart, SSE, downloads) with credential-chain
|
|
4
|
+
* resolution + ergonomic factory functions. Designed to feel native to a
|
|
5
|
+
* developer writing `import { gisl } from '@giveitsmaller/sdk'` and then
|
|
6
|
+
* `const client = await gisl.create();`.
|
|
7
|
+
*
|
|
8
|
+
* Scope of this card (T1, `wVU4xHx3`):
|
|
9
|
+
* - `gisl.create()` — full functionality. Resolves credentials + endpoint
|
|
10
|
+
* via `credentials.ts`, fails early with `GislMissingCredentialsError`
|
|
11
|
+
* when no apiKey is found AND the caller hasn't opted into cookie-mode
|
|
12
|
+
* or anonymous mode.
|
|
13
|
+
* - INTERNAL `_gislAnonymous` capability + `ANONYMOUS_ALLOWLIST` constant
|
|
14
|
+
* are wired but NOT publicly exported until the free-tier launch decides
|
|
15
|
+
* which operations are anonymous-capable (plan §12 open decision). A
|
|
16
|
+
* non-empty allowlist + the named export will arrive in a follow-up PR
|
|
17
|
+
* the moment user picks; this card avoids shipping a dead `gisl.anonymous()`.
|
|
18
|
+
*/
|
|
19
|
+
import { GislClient } from './client.js';
|
|
20
|
+
import { GislConfigError, GislFeatureRequiresAuthError, GislMissingCredentialsError, } from './errors.js';
|
|
21
|
+
import { resolveApiKey, resolveEndpoint, } from './credentials.js';
|
|
22
|
+
import { OperationBuilder } from './builder.js';
|
|
23
|
+
import { MergeBuilder, asset } from './merge.js';
|
|
24
|
+
import { PresetDefaults } from './ergonomic/presets/index.js';
|
|
25
|
+
import { Recipe, fileInput } from './file-first.js';
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Anonymous-capable operation allowlist (internal)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* Operations that may be invoked on a `gisl.anonymous()` client without
|
|
31
|
+
* raising `GislFeatureRequiresAuthError`. Empty until the free-tier launch
|
|
32
|
+
* decision lands (plan §12). Typed as a `readonly []` tuple (NOT
|
|
33
|
+
* `readonly string[]`) so the audit-gate compile-time assertion in
|
|
34
|
+
* `_audit.ts` fires if a future PR widens this without flipping the
|
|
35
|
+
* parking-decision + adding the public `gisl.anonymous()` export.
|
|
36
|
+
*
|
|
37
|
+
* Consumers must not depend on its emptiness today — only `package.json`
|
|
38
|
+
* `exports` keeps deep-imports blocked; the marker is internal.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export const ANONYMOUS_ALLOWLIST = [];
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Factories
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Construct an ergonomic-layer client. Resolves the API key + base URL via
|
|
48
|
+
* the credential chain (see `credentials.ts`) and constructs a low-level
|
|
49
|
+
* `GislClient`. Throws `GislMissingCredentialsError` synchronously before
|
|
50
|
+
* any HTTP I/O when no key is found AND neither `useSessionCookie` nor
|
|
51
|
+
* `allowAnonymous` is set.
|
|
52
|
+
*
|
|
53
|
+
* Cookie-mode (`useSessionCookie: true`) explicitly bypasses the missing-
|
|
54
|
+
* credentials check — browser SPAs that drive auth via `client.login()`
|
|
55
|
+
* legitimately have no apiKey at construction time.
|
|
56
|
+
*/
|
|
57
|
+
export async function create(opts = {}) {
|
|
58
|
+
// Extract presetDefaults BEFORE `_createInternal` destructures and
|
|
59
|
+
// strips it — the resolver needs the value for every operation call,
|
|
60
|
+
// not just construction. `_createInternal` still strips the slot
|
|
61
|
+
// from the low-level `GislClient` config (no leak into transport).
|
|
62
|
+
const presetDefaults = opts.presetDefaults;
|
|
63
|
+
return wrapErgonomic(await _createInternal(opts), presetDefaults);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compose the ergonomic operation surface (`.compress` / `.convert` /
|
|
67
|
+
* `.thumbnail`) on top of a `GislClient` via Proxy — matches the
|
|
68
|
+
* `wrapAnonymous` precedent (no prototype mutation). Layer order is
|
|
69
|
+
* builder-wrap INSIDE, anonymous-wrap OUTSIDE so the allowlist gate
|
|
70
|
+
* runs last in `_internalAnonymous` (see `_createInternal`).
|
|
71
|
+
*/
|
|
72
|
+
function wrapErgonomic(client, presetDefaults, scopedPresetDefaults) {
|
|
73
|
+
return new Proxy(client, {
|
|
74
|
+
get(target, prop, receiver) {
|
|
75
|
+
if (prop === 'file') {
|
|
76
|
+
// File-first entry point — the subject of the file-first surface.
|
|
77
|
+
// A bare string is a filesystem path, a Blob/File an in-memory input;
|
|
78
|
+
// pass a `FileInput` (e.g. `fileInput.uploadId(...)`) to reuse a
|
|
79
|
+
// pre-uploaded file. `key` is RESULT-addressing only. The Proxy's
|
|
80
|
+
// closure forwards the same preset-defaults references the op builders
|
|
81
|
+
// get, so a file-first `compress()` resolves presets identically.
|
|
82
|
+
return (input, key) => {
|
|
83
|
+
const resolved = typeof input === 'string'
|
|
84
|
+
? fileInput.path(input)
|
|
85
|
+
: input instanceof Blob
|
|
86
|
+
? fileInput.blob(input)
|
|
87
|
+
: input;
|
|
88
|
+
return new Recipe(resolved, key, [], presetDefaults, scopedPresetDefaults, target);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (prop === 'compress' || prop === 'convert' || prop === 'thumbnail') {
|
|
92
|
+
return (input, options = {}) => {
|
|
93
|
+
// T4b — pass client-scope presetDefaults into the builder so
|
|
94
|
+
// .run()/.submit() consult the preset resolver. The Proxy's
|
|
95
|
+
// closure carries the same reference for every per-call
|
|
96
|
+
// builder construction.
|
|
97
|
+
// T4c — also forward the scopedPresetDefaults closure (from
|
|
98
|
+
// `withPresetDefaults`); `undefined` on root clients.
|
|
99
|
+
return new OperationBuilder(target, prop, input, options, presetDefaults, scopedPresetDefaults);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (prop === 'withPresetDefaults') {
|
|
103
|
+
// T4c — immutable scoped derive. Computes mergedScoped =
|
|
104
|
+
// (parent.scoped === undefined ? new : PresetDefaults.merge(
|
|
105
|
+
// parent.scoped, new)) and returns a new Proxy wrapping the SAME
|
|
106
|
+
// underlying GislClient `target` (identity preservation —
|
|
107
|
+
// baseUrl / apiKey / headers / timeouts / multipart / session-
|
|
108
|
+
// cookie all by reference). Does NOT re-trigger _createInternal
|
|
109
|
+
// / resolveApiKey (codex r2 invariant — derives never re-read
|
|
110
|
+
// env or profile).
|
|
111
|
+
return (defaults) => {
|
|
112
|
+
const mergedScoped = scopedPresetDefaults === undefined
|
|
113
|
+
? defaults
|
|
114
|
+
: PresetDefaults.merge(scopedPresetDefaults, defaults);
|
|
115
|
+
return wrapErgonomic(target, presetDefaults, mergedScoped);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (prop === 'merge') {
|
|
119
|
+
// merge(...) accepts a mix of:
|
|
120
|
+
// - Asset objects (handle/path) — declared explicitly
|
|
121
|
+
// - string | Blob — wrapped via `asset()`
|
|
122
|
+
// - MergeOptions (always LAST) — sniffed by the absence of asset shape
|
|
123
|
+
return (...args) => {
|
|
124
|
+
let mergeOpts = {};
|
|
125
|
+
let last = args.length > 0 ? args[args.length - 1] : undefined;
|
|
126
|
+
if (isMergeOptions(last)) {
|
|
127
|
+
mergeOpts = last;
|
|
128
|
+
args = args.slice(0, -1);
|
|
129
|
+
}
|
|
130
|
+
const declared = args.map((a) => {
|
|
131
|
+
if (typeof a === 'string' || a instanceof Blob)
|
|
132
|
+
return asset(a);
|
|
133
|
+
// Asset (handle or path).
|
|
134
|
+
return a;
|
|
135
|
+
});
|
|
136
|
+
return new MergeBuilder(target, declared, mergeOpts);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return Reflect.get(target, prop, receiver);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Sniff whether the final argument to `merge(...)` is a `MergeOptions`
|
|
145
|
+
* object rather than an `Asset`. Heuristic: an `Asset` always has a
|
|
146
|
+
* `type` field with `'handle'` or `'path'`; a `MergeOptions` does not.
|
|
147
|
+
*/
|
|
148
|
+
function isMergeOptions(value) {
|
|
149
|
+
if (value === null || typeof value !== 'object')
|
|
150
|
+
return false;
|
|
151
|
+
if (typeof value.then === 'function')
|
|
152
|
+
return false;
|
|
153
|
+
if (value instanceof Blob)
|
|
154
|
+
return false;
|
|
155
|
+
const t = value.type;
|
|
156
|
+
if (t === 'handle' || t === 'path' || t === 'clip')
|
|
157
|
+
return false;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Inner factory shared by `create()` and `_internalAnonymous()` — extracted
|
|
162
|
+
* so the anonymous branch can ENTIRELY skip the credential chain rather
|
|
163
|
+
* than just suppressing its throw (codex r1 high e9e1c1182d56 — without
|
|
164
|
+
* this, env-resolved keys would silently attach an Authorization header
|
|
165
|
+
* to anonymous calls and defeat the parking guarantee).
|
|
166
|
+
*
|
|
167
|
+
* @internal
|
|
168
|
+
*/
|
|
169
|
+
async function _createInternal(opts) {
|
|
170
|
+
const { apiKey: explicitKey, profile, profilePath, useSessionCookie, baseUrl, environment, allowAnonymous,
|
|
171
|
+
// T4a slot — stripped from transportConfig so it does not leak
|
|
172
|
+
// into the low-level `GislClientConfig` spread. The T4b resolver
|
|
173
|
+
// reads `opts.presetDefaults` directly via its own path.
|
|
174
|
+
presetDefaults: _presetDefaults, ...transportConfig } = opts;
|
|
175
|
+
void _presetDefaults;
|
|
176
|
+
const resolvedBaseUrl = resolveEndpoint({ baseUrl, environment });
|
|
177
|
+
// Anonymous mode entirely BYPASSES the credential chain. Any env / profile
|
|
178
|
+
// key that happens to exist on the host MUST NOT leak into the request
|
|
179
|
+
// (codex r1 high e9e1c1182d56). Cookie-mode also bypasses, since the
|
|
180
|
+
// caller authenticates via session cookie later.
|
|
181
|
+
if (allowAnonymous === true) {
|
|
182
|
+
const config = {
|
|
183
|
+
baseUrl: resolvedBaseUrl,
|
|
184
|
+
...transportConfig,
|
|
185
|
+
};
|
|
186
|
+
if (useSessionCookie !== undefined) {
|
|
187
|
+
config.useSessionCookie = useSessionCookie;
|
|
188
|
+
}
|
|
189
|
+
return wrapAnonymous(new GislClient(config));
|
|
190
|
+
}
|
|
191
|
+
// Cookie-mode: skip env / profile resolution entirely UNLESS the caller
|
|
192
|
+
// ALSO passes an explicit `{apiKey}` (the mixed case is legitimate — a
|
|
193
|
+
// cookie-authenticated SPA may also send a server-issued API key). Codex
|
|
194
|
+
// r2 medium 913e4d8073f5 — without this, useSessionCookie=true could
|
|
195
|
+
// silently pick up an ambient GISL_API_KEY or fail on a malformed local
|
|
196
|
+
// profile, neither of which a cookie-auth caller expects.
|
|
197
|
+
let resolvedKey;
|
|
198
|
+
if (useSessionCookie === true && (explicitKey === undefined || explicitKey === '')) {
|
|
199
|
+
resolvedKey = null;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
resolvedKey = await resolveApiKey({
|
|
203
|
+
apiKey: explicitKey,
|
|
204
|
+
profile,
|
|
205
|
+
profilePath,
|
|
206
|
+
useSessionCookie,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (resolvedKey === null && useSessionCookie !== true) {
|
|
210
|
+
throw new GislMissingCredentialsError('No API key found via explicit arg, GISL_API_KEY env, or ~/.gisl/credentials profile. ' +
|
|
211
|
+
'Pass {apiKey} explicitly, set GISL_API_KEY, populate ~/.gisl/credentials, ' +
|
|
212
|
+
'or pass {useSessionCookie: true} for browser session-cookie authentication.');
|
|
213
|
+
}
|
|
214
|
+
const config = {
|
|
215
|
+
baseUrl: resolvedBaseUrl,
|
|
216
|
+
...transportConfig,
|
|
217
|
+
};
|
|
218
|
+
if (resolvedKey !== null) {
|
|
219
|
+
config.apiKey = resolvedKey;
|
|
220
|
+
}
|
|
221
|
+
if (useSessionCookie !== undefined) {
|
|
222
|
+
config.useSessionCookie = useSessionCookie;
|
|
223
|
+
}
|
|
224
|
+
return new GislClient(config);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Wrap a `GislClient` so calls to non-allowlisted operations throw
|
|
228
|
+
* `GislFeatureRequiresAuthError` BEFORE any I/O. This is the internal
|
|
229
|
+
* capability that backs `gisl.anonymous()` once the allowlist is non-empty.
|
|
230
|
+
*
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
function wrapAnonymous(client) {
|
|
234
|
+
// Explicit Set<string> — `ANONYMOUS_ALLOWLIST` is currently typed as the
|
|
235
|
+
// empty tuple `readonly []` (audit-gate parking-invariant in _audit.ts).
|
|
236
|
+
// Without this, `new Set(ANONYMOUS_ALLOWLIST)` would infer `Set<never>`.
|
|
237
|
+
const allowlist = new Set(ANONYMOUS_ALLOWLIST);
|
|
238
|
+
return new Proxy(client, {
|
|
239
|
+
get(target, prop, receiver) {
|
|
240
|
+
const value = Reflect.get(target, prop, receiver);
|
|
241
|
+
if (typeof value !== 'function' || typeof prop !== 'string') {
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
// Allow base infrastructure methods that don't carry user operations.
|
|
245
|
+
if (prop.startsWith('_') ||
|
|
246
|
+
prop === 'login' ||
|
|
247
|
+
prop === 'logout' ||
|
|
248
|
+
prop === 'getSchema' ||
|
|
249
|
+
prop === 'submitContact' ||
|
|
250
|
+
prop === 'constructor') {
|
|
251
|
+
return value.bind(target);
|
|
252
|
+
}
|
|
253
|
+
// Allowlist gate: throw if the op isn't approved for anonymous use.
|
|
254
|
+
if (!allowlist.has(prop)) {
|
|
255
|
+
return () => {
|
|
256
|
+
throw new GislFeatureRequiresAuthError(prop, `Operation '${prop}' is not available on an anonymous client. ` +
|
|
257
|
+
`Use gisl.create({apiKey}) for authenticated access, or wait for the operation to be added to the anonymous allowlist.`);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return value.bind(target);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Public `gisl` namespace
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
/**
|
|
268
|
+
* The `gisl` namespace — primary ergonomic-layer entry point.
|
|
269
|
+
* Exports `gisl.create()` only for v0.7; `gisl.anonymous()` lands once
|
|
270
|
+
* the anonymous-capable operation allowlist is non-empty (plan §12).
|
|
271
|
+
*/
|
|
272
|
+
export const gisl = {
|
|
273
|
+
create,
|
|
274
|
+
};
|
|
275
|
+
// `_internalAnonymous` is the implementation behind the future public
|
|
276
|
+
// `gisl.anonymous()` export. Kept underscore-prefixed so it does not
|
|
277
|
+
// reach the audit gate as a public symbol.
|
|
278
|
+
/** @internal */
|
|
279
|
+
export async function _internalAnonymous(opts = {}) {
|
|
280
|
+
return _createInternal({ ...opts, allowAnonymous: true });
|
|
281
|
+
}
|
|
282
|
+
// Re-export error types for callers that want to `instanceof` them.
|
|
283
|
+
export { GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Downloader } from './file-first.js';
|
|
2
|
+
/**
|
|
3
|
+
* Streams a (typically pre-signed) URL to a local path without buffering the
|
|
4
|
+
* whole body in memory. Pre-signed download URLs require no SDK auth, so this
|
|
5
|
+
* issues a plain unauthenticated `fetch`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class HttpDownloader implements Downloader {
|
|
8
|
+
downloadTo(url: string, destPath: string): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only streaming {@link Downloader} implementation.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own module so the framework-free `file-first.ts` stays
|
|
5
|
+
* Node-import-free; the Node `fs`/`stream` imports are isolated here.
|
|
6
|
+
*/
|
|
7
|
+
import { createWriteStream } from 'node:fs';
|
|
8
|
+
import { Readable } from 'node:stream';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
import { GislNetworkError, GislSinkError } from './errors.js';
|
|
11
|
+
/**
|
|
12
|
+
* Streams a (typically pre-signed) URL to a local path without buffering the
|
|
13
|
+
* whole body in memory. Pre-signed download URLs require no SDK auth, so this
|
|
14
|
+
* issues a plain unauthenticated `fetch`.
|
|
15
|
+
*/
|
|
16
|
+
export class HttpDownloader {
|
|
17
|
+
async downloadTo(url, destPath) {
|
|
18
|
+
// Source-read failures surface as GislNetworkError to match the PHP
|
|
19
|
+
// StreamingDownloader (both raise GislNetworkError when the output URL
|
|
20
|
+
// cannot be fetched); a dest-write failure is GislSinkError(write_failed)
|
|
21
|
+
// on the RunResult sink side. Parity-critical: the FF1 sink contract tells
|
|
22
|
+
// callers to narrow with instanceof, so the source-read error type must
|
|
23
|
+
// match across languages.
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(url);
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
// A rejected fetch (DNS, TCP, TLS, mid-flight disconnect) must surface as
|
|
30
|
+
// GislNetworkError too — not the raw TypeError — so callers can narrow
|
|
31
|
+
// every download-source failure with `instanceof GislNetworkError`
|
|
32
|
+
// (codex review medium).
|
|
33
|
+
throw new GislNetworkError(`Failed to fetch download source: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new GislNetworkError(`Download failed with status ${res.status}`);
|
|
37
|
+
}
|
|
38
|
+
if (res.body === null) {
|
|
39
|
+
throw new GislNetworkError('Download response had no body');
|
|
40
|
+
}
|
|
41
|
+
// `fetch`'s WHATWG ReadableStream and Node's `stream/web` ReadableStream
|
|
42
|
+
// are structurally the same at runtime but typed in two different lib
|
|
43
|
+
// declarations; the cast bridges them for `Readable.fromWeb`.
|
|
44
|
+
try {
|
|
45
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Destination-write failures (unwritable dir, disk full, …) surface as
|
|
49
|
+
// GislSinkError(write_failed) to match the PHP StreamingDownloader — a
|
|
50
|
+
// raw Node ENOENT would otherwise leak through the parity-critical sink
|
|
51
|
+
// contract. Source-read failures are handled above as GislNetworkError.
|
|
52
|
+
throw new GislSinkError(`Failed to stream download to destination: ${destPath}`, { reason: 'write_failed' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|