@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.
Files changed (52) hide show
  1. package/dist/_audit.js +71 -0
  2. package/dist/builder.d.ts +406 -0
  3. package/dist/builder.js +706 -0
  4. package/dist/client.d.ts +10 -0
  5. package/dist/client.js +42 -6
  6. package/dist/credentials.d.ts +61 -0
  7. package/dist/credentials.js +200 -0
  8. package/dist/ergonomic/preset_resolver.d.ts +75 -0
  9. package/dist/ergonomic/preset_resolver.js +568 -0
  10. package/dist/ergonomic/presets/_translate.d.ts +11 -0
  11. package/dist/ergonomic/presets/_translate.js +35 -0
  12. package/dist/ergonomic/presets/audio_compress.d.ts +16 -0
  13. package/dist/ergonomic/presets/audio_compress.js +45 -0
  14. package/dist/ergonomic/presets/document_epub_compress.d.ts +14 -0
  15. package/dist/ergonomic/presets/document_epub_compress.js +34 -0
  16. package/dist/ergonomic/presets/document_odf_compress.d.ts +14 -0
  17. package/dist/ergonomic/presets/document_odf_compress.js +34 -0
  18. package/dist/ergonomic/presets/document_office_compress.d.ts +16 -0
  19. package/dist/ergonomic/presets/document_office_compress.js +40 -0
  20. package/dist/ergonomic/presets/document_pdf_compress.d.ts +14 -0
  21. package/dist/ergonomic/presets/document_pdf_compress.js +35 -0
  22. package/dist/ergonomic/presets/image_compress.d.ts +43 -0
  23. package/dist/ergonomic/presets/image_compress.js +95 -0
  24. package/dist/ergonomic/presets/index.d.ts +77 -0
  25. package/dist/ergonomic/presets/index.js +216 -0
  26. package/dist/ergonomic/presets/video_compress.d.ts +30 -0
  27. package/dist/ergonomic/presets/video_compress.js +83 -0
  28. package/dist/errors.d.ts +196 -1
  29. package/dist/errors.js +216 -0
  30. package/dist/file-first.d.ts +284 -0
  31. package/dist/file-first.js +445 -0
  32. package/dist/generated/sdk_spec/enums.d.ts +195 -0
  33. package/dist/generated/sdk_spec/enums.js +127 -0
  34. package/dist/generated/sdk_spec/errors.d.ts +16 -0
  35. package/dist/generated/sdk_spec/errors.js +523 -0
  36. package/dist/generated/sdk_spec/index.d.ts +4 -0
  37. package/dist/generated/sdk_spec/index.js +7 -0
  38. package/dist/generated/sdk_spec/presets.d.ts +6 -0
  39. package/dist/generated/sdk_spec/presets.js +157 -0
  40. package/dist/generated/sdk_spec/version.d.ts +3 -0
  41. package/dist/generated/sdk_spec/version.js +6 -0
  42. package/dist/gisl.d.ts +122 -0
  43. package/dist/gisl.js +283 -0
  44. package/dist/http-downloader.d.ts +9 -0
  45. package/dist/http-downloader.js +55 -0
  46. package/dist/index.d.ts +20 -5
  47. package/dist/index.js +45 -4
  48. package/dist/merge.d.ts +142 -0
  49. package/dist/merge.js +411 -0
  50. package/dist/types.d.ts +12 -14
  51. package/dist/types.js +18 -0
  52. 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,3 @@
1
+ export declare const SDK_SPEC_VERSION: "1.2.0";
2
+ export declare const PRESET_VERSION: "1.0";
3
+ export declare const PRESET_CONFIG_HASH: "sha256:35aeb0b6b86edd9814ace5b75cfeef8a7b1432ecb8e4d5ee8b38d9187a0b45eb";
@@ -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
+ }