@aurabase/functions 0.5.4 → 0.6.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/README.md CHANGED
@@ -26,6 +26,10 @@ import { createClient } from '@aurabase/aurabase-js'
26
26
  const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx')
27
27
  ```
28
28
 
29
+ ### Qui peut appeler quoi
30
+
31
+ Seule l'**invocation** (`invoke`) est ouverte à la clé publique `anon` et aux JWT utilisateur. Tout le reste (`list`, `get`, `listJobs`, `getJob`, `enqueueJob`, `listCronJobs`, `createCronJob`, `deleteCronJob`) exige une clé `service_role` et s'utilise **côté serveur** : une `FunctionDef` contient le code source et les variables d'environnement de la fonction, donc ces appels répondent `403` à une clé publique. `createClient` refuse d'ailleurs une clé `service_role` dans un navigateur.
32
+
29
33
  ### Invocation
30
34
 
31
35
  `invoke<T>()` décrit la requête que votre fonction doit recevoir (`{ method, path, headers, body }`) et dé-wrap la réponse JSON du backend — tout est bufferisé, pas de streaming (voir Limitations).
@@ -44,6 +48,8 @@ console.log(data.ok)
44
48
 
45
49
  Options complètes : `body`, `method`, `path` (sous-route à l'intérieur de la fonction), `headers` (transmis à la fonction elle-même, pas des headers de transport), `signal`, `timeout`, `responseType`.
46
50
 
51
+ Le `body` doit être sérialisable en JSON (objet, tableau, chaîne, nombre, booléen). Un `Blob`, un `ArrayBuffer`, un tableau typé, un `FormData`, un `URLSearchParams` ou un `ReadableStream` est refusé avant tout appel réseau avec le code `invalid_body` : le runtime ne reçoit que du JSON, et ces objets seraient transmis vides ou déformés. Encodez-les (par exemple en base64) ou passez par Storage.
52
+
47
53
  ```ts
48
54
  const controller = new AbortController()
49
55
 
@@ -85,7 +91,7 @@ Deno.serve(async (req) => {
85
91
  })
86
92
  ```
87
93
 
88
- Défauts si vous ne précisez rien : `POST` sur `/`. `GET` et `HEAD` sont envoyés sans corps (la spec Fetch l'interdit) ; un `body` non-chaîne est sérialisé en JSON et `content-type: application/json` est posé si vous n'en fixez pas un.
94
+ Défauts si vous ne précisez rien : `POST` sur `/`. `GET` et `HEAD` sont envoyés sans corps (la spec Fetch l'interdit) ; un `body` non-chaîne est sérialisé en JSON ; `content-type: application/json` est posé si vous n'en fixez pas un — y compris pour une chaîne, que la fonction reçoit alors telle quelle : lisez-la avec `await req.text()` plutôt que `req.json()`.
89
95
 
90
96
  Sans `body`, la fonction reçoit une requête **sans corps** (parité `@supabase/functions-js`) : un `await req.json()` inconditionnel y échoue. Écrivez `await req.json().catch(() => ({}))` si l'appel peut être fait sans charge utile.
91
97
 
@@ -95,7 +101,7 @@ Les en-têtes de cadrage/transport (`content-length`, `transfer-encoding`, `conn
95
101
 
96
102
  #### Type de réponse (`responseType`)
97
103
 
98
- Défaut `'json'` : le corps est parsé et dé-wrappé (`{data}` → contenu). Les autres valeurs renvoient le corps brut de la fonction, sans dé-wrapping, et `error.status` porte alors le **vrai** statut HTTP de la fonction :
104
+ Défaut `'json'` : le corps est parsé et dé-wrappé (`{data}` → contenu). Une réponse texte (`text/plain`…) arrive telle quelle, en chaîne. Un contenu **binaire** (image, PDF…) ne peut pas passer par ce mode : le SDK renvoie `invalid_response` en vous orientant vers `responseType`. Les autres valeurs renvoient le corps brut de la fonction, sans dé-wrapping, et `error.status` porte alors le **vrai** statut HTTP de la fonction :
99
105
 
100
106
  ```ts
101
107
  const { data: csv } = await aura.functions.invoke<string>('export', { responseType: 'text' })
@@ -109,27 +115,31 @@ Parité `@supabase/functions-js`. Les deux classes sous-classent `AuraError` (do
109
115
 
110
116
  | Classe | Quand | Statuts |
111
117
  |---|---|---|
112
- | `FunctionsHttpError` | **votre fonction** a répondu en erreur | tout non-2xx hors relais (400, 404, 500…) |
113
- | `FunctionsRelayError` | la réponse de votre fonction n'est jamais arrivée | 0 (réseau), 502, 503, 504 |
118
+ | `FunctionsHttpError` | **votre fonction** a répondu en erreur, ou une route de gestion a échoué | tout non-2xx venant de la fonction (400, 404, 500, **y compris 502/503/504 qu'elle renvoie elle-même**) |
119
+ | `FunctionsRelayError` | la **plateforme** a échoué : la réponse de votre fonction n'est jamais arrivée | 0 (réseau), 502, 503, 504 produits par la passerelle ou le runtime |
120
+
121
+ Le message d'une `FunctionsHttpError` est celui que votre fonction a renvoyé : texte brut, `{ "message": "…" }`, `{ "error": "…" }` ou `{ "error": { "message": "…" } }`. `error.context` conserve le corps d'origine (objet JSON ou texte). Une fonction qui répond 500 sans corps donne « Function returned HTTP 500 ».
114
122
 
115
123
  Deux statuts de relais à ne pas confondre — ils ne se rejouent pas de la même façon :
116
124
 
117
125
  - **503 `SERVICE_UNAVAILABLE`** : l'edge-runtime est injoignable, votre fonction **n'a pas** été exécutée → réessayer est sûr ;
118
- - **504 `TIMEOUT`** : votre fonction a dépassé son budget (`permissions.timeout_secs` + marge). Elle **a été lancée** et a pu produire ses effets de bord ; seule la réponse manque → ne rejouez pas une opération non idempotente à l'aveugle.
126
+ - **504 `TIMEOUT`** : l'attente a dépassé le budget de la fonction (`permissions.timeout_secs` + marge). La fonction **a été lancée** et a pu produire ses effets de bord ; seule la réponse manque → ne rejouez pas une opération non idempotente à l'aveugle.
119
127
 
120
128
  ```ts
121
- import { FunctionsHttpError, FunctionsRelayError, isRelayStatus } from '@aurabase/functions'
129
+ import { FunctionsHttpError, FunctionsRelayError, isRelayStatus } from '@aurabase/aurabase-js'
122
130
 
123
131
  const { error } = await aura.functions.invoke('checkout', { responseType: 'text' })
124
132
  if (error instanceof FunctionsRelayError) {
125
- // rien n'a été exécuté côté fonction → réessayer est sûr
133
+ // panne de plateforme : 503 → rien n'a été exécuté, 504 → peut-être exécuté, vérifiez avant de rejouer
126
134
  } else if (error instanceof FunctionsHttpError) {
127
135
  // la fonction a bien tourné et a répondu en erreur → déboguer VOTRE code
128
136
  }
129
137
  isRelayStatus(error?.status ?? 0) // même décision, sans instanceof
130
138
  ```
131
139
 
132
- Une annulation (`timeout`/`signal`) reste une `AuraError` de code `timeout`, jamais une de ces deux classes.
140
+ Une annulation (`timeout`/`signal`) reste une `AuraError` de code `timeout`, jamais une de ces deux classes. Sans `timeout` ni `signal`, le délai par défaut du client (`requestTimeoutMs`, 30 s) s'applique à tous les modes, `responseType` compris.
141
+
142
+ Les segments passés à `invoke`, `get`, `getJob` et `deleteCronJob` sont encodés (`encodeURIComponent`) : un nom contenant `/`, `?` ou `#` ne peut pas sortir de sa position dans l'URL.
133
143
 
134
144
  ### Fonctions (métadonnées)
135
145
 
@@ -170,7 +180,7 @@ await aura.functions.deleteCronJob('nightly-cleanup')
170
180
  ## Contrat d'invocation et comportement des erreurs
171
181
 
172
182
  - **Déballage transparent :** En mode `responseType: 'json'` (défaut), `data` contient directement le type attendu `T` renvoyé par votre fonction. Pour préserver la compatibilité avec le code existant lisant l'enveloppe, les propriétés `status` et `body` restent accessibles sur l'objet retourné.
173
- - **Propagation des erreurs HTTP applicatives :** Si votre fonction répond avec un statut HTTP $\ge 400$, le SDK lève désormais une `FunctionsHttpError` avec le statut réel de votre fonction (`error.status`) et son message d'erreur, même lorsque le transport réseau sous-jacent utilise une enveloppe.
183
+ - **Propagation des erreurs HTTP applicatives :** Si votre fonction répond avec un statut HTTP supérieur ou égal à 400, le SDK renvoie une `FunctionsHttpError` avec le statut réel de votre fonction (`error.status`) et son message d'erreur, même lorsque le transport réseau sous-jacent utilise une enveloppe.
174
184
  - **Relais et disponibilité :** En cas d'indisponibilité du runtime ou de coupure réseau, une `FunctionsRelayError` est renvoyée.
175
185
  - **Propagation d'identité sécurisée :** Les en-têtes d'autorisation (`Authorization: Bearer <token>`, `apikey`) sont validés par la passerelle Aurabase et propagés de façon étanche vers le worker de la fonction (`x-aura-user-id`, `x-aura-role`), protégeant votre backend contre toute falsification client.
176
186
 
package/dist/index.cjs CHANGED
@@ -27,7 +27,31 @@ var FunctionsRelayError = class _FunctionsRelayError extends core.AuraError {
27
27
  };
28
28
  function toFunctionsError(err, context) {
29
29
  const body = err.details != null ? err.details : { error: { code: err.code, message: err.message } };
30
- return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, context ?? err.details) : new FunctionsHttpError(err.status, body, context ?? err.details);
30
+ return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, err.details) : new FunctionsHttpError(err.status, body, err.details);
31
+ }
32
+ var INVOKE_ORIGIN_HEADER = "x-aurabase-invoke-origin";
33
+ var seg = encodeURIComponent;
34
+ function unserializableBodyKind(body) {
35
+ if (body === null || typeof body !== "object") return null;
36
+ const g = globalThis;
37
+ for (const name of ["Blob", "FormData", "URLSearchParams", "ReadableStream"]) {
38
+ const ctor = g[name];
39
+ if (typeof ctor === "function" && body instanceof ctor) return name;
40
+ }
41
+ if (body instanceof ArrayBuffer) return "ArrayBuffer";
42
+ if (ArrayBuffer.isView(body)) return body.constructor.name;
43
+ return null;
44
+ }
45
+ function envelopeContentType(headers) {
46
+ if (headers === null || typeof headers !== "object") return void 0;
47
+ for (const [k, v] of Object.entries(headers)) {
48
+ if (k.toLowerCase() === "content-type" && typeof v === "string") return v.toLowerCase();
49
+ }
50
+ return void 0;
51
+ }
52
+ function isTextual(contentType) {
53
+ if (!contentType) return true;
54
+ return contentType.includes("json") || contentType.startsWith("text/") || contentType.includes("xml");
31
55
  }
32
56
  var FunctionsService = class extends core.BaseService {
33
57
  constructor(client) {
@@ -74,11 +98,26 @@ var FunctionsService = class extends core.BaseService {
74
98
  * `signal` produisent le code `timeout`.
75
99
  */
76
100
  async invoke(slug, options) {
101
+ const badBody = unserializableBodyKind(options?.body);
102
+ if (badBody) {
103
+ return {
104
+ data: null,
105
+ error: new core.AuraError(0, {
106
+ error: {
107
+ code: "invalid_body",
108
+ message: `Le corps d'invocation doit \xEAtre s\xE9rialisable en JSON (objet, tableau, cha\xEEne, nombre, bool\xE9en) : un ${badBody} serait transmis vide ou d\xE9form\xE9. Encodez-le (par exemple en base64) ou passez par Storage.`
109
+ }
110
+ })
111
+ };
112
+ }
113
+ const responseType = options?.responseType ?? "json";
77
114
  const controller = new AbortController();
78
115
  let timeoutId;
79
116
  const userSignal = options?.signal;
80
117
  const onAbort = () => controller.abort();
81
- if (options?.timeout) timeoutId = setTimeout(() => controller.abort(), options.timeout);
118
+ const clientTimeout = this.client.requestTimeoutMs ?? core.DEFAULT_REQUEST_TIMEOUT_MS;
119
+ const timeoutMs = options?.timeout || (responseType !== "json" && userSignal === void 0 && clientTimeout > 0 ? clientTimeout : 0);
120
+ if (timeoutMs) timeoutId = setTimeout(() => controller.abort(), timeoutMs);
82
121
  if (userSignal) {
83
122
  if (userSignal.aborted) controller.abort();
84
123
  else userSignal.addEventListener("abort", onAbort, { once: true });
@@ -93,7 +132,6 @@ var FunctionsService = class extends core.BaseService {
93
132
  headers: options?.headers,
94
133
  body: options?.body
95
134
  };
96
- const responseType = options?.responseType ?? "json";
97
135
  const timeoutError = () => ({
98
136
  data: null,
99
137
  error: new core.AuraError(0, {
@@ -103,7 +141,7 @@ var FunctionsService = class extends core.BaseService {
103
141
  try {
104
142
  if (responseType === "json") {
105
143
  const hasCanceller = options?.timeout !== void 0 || userSignal !== void 0;
106
- const res2 = await this.request("POST", `${this.prefix}/${slug}/invoke`, envelope, {
144
+ const res2 = await this.request("POST", `${this.prefix}/${seg(slug)}/invoke`, envelope, {
107
145
  ...hasCanceller ? { signal: controller.signal } : {}
108
146
  });
109
147
  if (res2.error && controller.signal.aborted) return timeoutError();
@@ -113,7 +151,7 @@ var FunctionsService = class extends core.BaseService {
113
151
  const invokeStatus = rawData.status;
114
152
  const invokeBody = rawData.body;
115
153
  if (typeof invokeStatus === "number" && invokeStatus >= 400) {
116
- const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : { message: String(invokeBody) };
154
+ const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : invokeBody === null || invokeBody === void 0 || invokeBody === "" ? {} : { message: typeof invokeBody === "string" ? invokeBody : JSON.stringify(invokeBody) };
117
155
  const errorMsg = typeof errPayload.message === "string" ? errPayload.message : typeof errPayload.error === "string" ? errPayload.error : typeof errPayload.error?.message === "string" ? String(errPayload.error.message) : `Function returned HTTP ${invokeStatus}`;
118
156
  const normalizedBody = {
119
157
  ...errPayload,
@@ -125,10 +163,21 @@ var FunctionsService = class extends core.BaseService {
125
163
  };
126
164
  return {
127
165
  data: null,
128
- error: toFunctionsError(new core.AuraError(invokeStatus, normalizedBody), invokeBody)
166
+ error: new FunctionsHttpError(invokeStatus, normalizedBody, invokeBody)
167
+ };
168
+ }
169
+ if (invokeBody === null && invokeStatus !== 204 && !isTextual(envelopeContentType(rawData.headers))) {
170
+ return {
171
+ data: null,
172
+ error: new core.AuraError(0, {
173
+ error: {
174
+ code: "invalid_response",
175
+ message: `La fonction a r\xE9pondu un contenu binaire (HTTP ${invokeStatus}) que le mode JSON ne peut pas transmettre : utilisez responseType 'blob' ou 'arrayBuffer'.`
176
+ }
177
+ })
129
178
  };
130
179
  }
131
- let unwrappedData = invokeBody;
180
+ const unwrappedData = invokeBody;
132
181
  if (unwrappedData !== null && typeof unwrappedData === "object") {
133
182
  if (!("body" in unwrappedData)) {
134
183
  Object.defineProperty(unwrappedData, "body", {
@@ -166,19 +215,34 @@ var FunctionsService = class extends core.BaseService {
166
215
  };
167
216
  if (store.token) headers["Authorization"] = `Bearer ${store.token}`;
168
217
  if (client.apiKey) headers["apikey"] = client.apiKey;
169
- const res = await client.fetchFn(this.url(`${this.prefix}/${slug}/invoke`), {
218
+ const res = await client.fetchFn(this.url(`${this.prefix}/${seg(slug)}/invoke`), {
170
219
  method: "POST",
171
220
  headers,
172
221
  body: JSON.stringify(envelope),
173
222
  signal: controller.signal
174
223
  });
175
224
  if (!res.ok) {
225
+ let text = "";
226
+ try {
227
+ text = await res.text();
228
+ } catch (e) {
229
+ if (controller.signal.aborted || core.isAbortError(e)) throw e;
230
+ }
176
231
  let parsed = null;
177
232
  try {
178
- parsed = await res.json();
233
+ parsed = text.trim() ? JSON.parse(text) : null;
179
234
  } catch {
180
235
  }
181
- return { data: null, error: toFunctionsError(new core.AuraError(res.status, parsed)) };
236
+ const isObject = parsed !== null && typeof parsed === "object";
237
+ const stringError = isObject && typeof parsed.error === "string" ? parsed.error : void 0;
238
+ const body = isObject ? stringError !== void 0 && typeof parsed.message !== "string" ? { ...parsed, message: stringError } : parsed : text.trim() ? { message: text } : null;
239
+ if (res.headers.get(INVOKE_ORIGIN_HEADER) === "function") {
240
+ return {
241
+ data: null,
242
+ error: new FunctionsHttpError(res.status, body, isObject ? parsed : text || null)
243
+ };
244
+ }
245
+ return { data: null, error: toFunctionsError(new core.AuraError(res.status, body, res.headers)) };
182
246
  }
183
247
  let data;
184
248
  if (responseType === "text") data = await res.text();
@@ -212,7 +276,7 @@ var FunctionsService = class extends core.BaseService {
212
276
  * le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
213
277
  */
214
278
  async get(nameOrId) {
215
- return this.request("GET", `${this.prefix}/${nameOrId}`);
279
+ return this.request("GET", `${this.prefix}/${seg(nameOrId)}`);
216
280
  }
217
281
  // -- Jobs -------------------------------------------------------------------
218
282
  async listJobs(options) {
@@ -223,7 +287,7 @@ var FunctionsService = class extends core.BaseService {
223
287
  return this.request("GET", `${this.prefix}/jobs${qs}`);
224
288
  }
225
289
  async getJob(jobId) {
226
- return this.request("GET", `${this.prefix}/jobs/${jobId}`);
290
+ return this.request("GET", `${this.prefix}/jobs/${seg(jobId)}`);
227
291
  }
228
292
  /**
229
293
  * Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
@@ -261,7 +325,7 @@ var FunctionsService = class extends core.BaseService {
261
325
  });
262
326
  }
263
327
  async deleteCronJob(name) {
264
- return this.request("DELETE", `${this.prefix}/cron/${name}`);
328
+ return this.request("DELETE", `${this.prefix}/cron/${seg(name)}`);
265
329
  }
266
330
  };
267
331
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AuraError, BaseService, isTokenExpired, refreshOnce, isAbortError } from '@aurabase/core';
1
+ import { AuraError, BaseService, DEFAULT_REQUEST_TIMEOUT_MS, isTokenExpired, refreshOnce, isAbortError } from '@aurabase/core';
2
2
 
3
3
  // src/errors.ts
4
4
  var RELAY_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
@@ -25,7 +25,31 @@ var FunctionsRelayError = class _FunctionsRelayError extends AuraError {
25
25
  };
26
26
  function toFunctionsError(err, context) {
27
27
  const body = err.details != null ? err.details : { error: { code: err.code, message: err.message } };
28
- return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, context ?? err.details) : new FunctionsHttpError(err.status, body, context ?? err.details);
28
+ return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, err.details) : new FunctionsHttpError(err.status, body, err.details);
29
+ }
30
+ var INVOKE_ORIGIN_HEADER = "x-aurabase-invoke-origin";
31
+ var seg = encodeURIComponent;
32
+ function unserializableBodyKind(body) {
33
+ if (body === null || typeof body !== "object") return null;
34
+ const g = globalThis;
35
+ for (const name of ["Blob", "FormData", "URLSearchParams", "ReadableStream"]) {
36
+ const ctor = g[name];
37
+ if (typeof ctor === "function" && body instanceof ctor) return name;
38
+ }
39
+ if (body instanceof ArrayBuffer) return "ArrayBuffer";
40
+ if (ArrayBuffer.isView(body)) return body.constructor.name;
41
+ return null;
42
+ }
43
+ function envelopeContentType(headers) {
44
+ if (headers === null || typeof headers !== "object") return void 0;
45
+ for (const [k, v] of Object.entries(headers)) {
46
+ if (k.toLowerCase() === "content-type" && typeof v === "string") return v.toLowerCase();
47
+ }
48
+ return void 0;
49
+ }
50
+ function isTextual(contentType) {
51
+ if (!contentType) return true;
52
+ return contentType.includes("json") || contentType.startsWith("text/") || contentType.includes("xml");
29
53
  }
30
54
  var FunctionsService = class extends BaseService {
31
55
  constructor(client) {
@@ -72,11 +96,26 @@ var FunctionsService = class extends BaseService {
72
96
  * `signal` produisent le code `timeout`.
73
97
  */
74
98
  async invoke(slug, options) {
99
+ const badBody = unserializableBodyKind(options?.body);
100
+ if (badBody) {
101
+ return {
102
+ data: null,
103
+ error: new AuraError(0, {
104
+ error: {
105
+ code: "invalid_body",
106
+ message: `Le corps d'invocation doit \xEAtre s\xE9rialisable en JSON (objet, tableau, cha\xEEne, nombre, bool\xE9en) : un ${badBody} serait transmis vide ou d\xE9form\xE9. Encodez-le (par exemple en base64) ou passez par Storage.`
107
+ }
108
+ })
109
+ };
110
+ }
111
+ const responseType = options?.responseType ?? "json";
75
112
  const controller = new AbortController();
76
113
  let timeoutId;
77
114
  const userSignal = options?.signal;
78
115
  const onAbort = () => controller.abort();
79
- if (options?.timeout) timeoutId = setTimeout(() => controller.abort(), options.timeout);
116
+ const clientTimeout = this.client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
117
+ const timeoutMs = options?.timeout || (responseType !== "json" && userSignal === void 0 && clientTimeout > 0 ? clientTimeout : 0);
118
+ if (timeoutMs) timeoutId = setTimeout(() => controller.abort(), timeoutMs);
80
119
  if (userSignal) {
81
120
  if (userSignal.aborted) controller.abort();
82
121
  else userSignal.addEventListener("abort", onAbort, { once: true });
@@ -91,7 +130,6 @@ var FunctionsService = class extends BaseService {
91
130
  headers: options?.headers,
92
131
  body: options?.body
93
132
  };
94
- const responseType = options?.responseType ?? "json";
95
133
  const timeoutError = () => ({
96
134
  data: null,
97
135
  error: new AuraError(0, {
@@ -101,7 +139,7 @@ var FunctionsService = class extends BaseService {
101
139
  try {
102
140
  if (responseType === "json") {
103
141
  const hasCanceller = options?.timeout !== void 0 || userSignal !== void 0;
104
- const res2 = await this.request("POST", `${this.prefix}/${slug}/invoke`, envelope, {
142
+ const res2 = await this.request("POST", `${this.prefix}/${seg(slug)}/invoke`, envelope, {
105
143
  ...hasCanceller ? { signal: controller.signal } : {}
106
144
  });
107
145
  if (res2.error && controller.signal.aborted) return timeoutError();
@@ -111,7 +149,7 @@ var FunctionsService = class extends BaseService {
111
149
  const invokeStatus = rawData.status;
112
150
  const invokeBody = rawData.body;
113
151
  if (typeof invokeStatus === "number" && invokeStatus >= 400) {
114
- const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : { message: String(invokeBody) };
152
+ const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : invokeBody === null || invokeBody === void 0 || invokeBody === "" ? {} : { message: typeof invokeBody === "string" ? invokeBody : JSON.stringify(invokeBody) };
115
153
  const errorMsg = typeof errPayload.message === "string" ? errPayload.message : typeof errPayload.error === "string" ? errPayload.error : typeof errPayload.error?.message === "string" ? String(errPayload.error.message) : `Function returned HTTP ${invokeStatus}`;
116
154
  const normalizedBody = {
117
155
  ...errPayload,
@@ -123,10 +161,21 @@ var FunctionsService = class extends BaseService {
123
161
  };
124
162
  return {
125
163
  data: null,
126
- error: toFunctionsError(new AuraError(invokeStatus, normalizedBody), invokeBody)
164
+ error: new FunctionsHttpError(invokeStatus, normalizedBody, invokeBody)
165
+ };
166
+ }
167
+ if (invokeBody === null && invokeStatus !== 204 && !isTextual(envelopeContentType(rawData.headers))) {
168
+ return {
169
+ data: null,
170
+ error: new AuraError(0, {
171
+ error: {
172
+ code: "invalid_response",
173
+ message: `La fonction a r\xE9pondu un contenu binaire (HTTP ${invokeStatus}) que le mode JSON ne peut pas transmettre : utilisez responseType 'blob' ou 'arrayBuffer'.`
174
+ }
175
+ })
127
176
  };
128
177
  }
129
- let unwrappedData = invokeBody;
178
+ const unwrappedData = invokeBody;
130
179
  if (unwrappedData !== null && typeof unwrappedData === "object") {
131
180
  if (!("body" in unwrappedData)) {
132
181
  Object.defineProperty(unwrappedData, "body", {
@@ -164,19 +213,34 @@ var FunctionsService = class extends BaseService {
164
213
  };
165
214
  if (store.token) headers["Authorization"] = `Bearer ${store.token}`;
166
215
  if (client.apiKey) headers["apikey"] = client.apiKey;
167
- const res = await client.fetchFn(this.url(`${this.prefix}/${slug}/invoke`), {
216
+ const res = await client.fetchFn(this.url(`${this.prefix}/${seg(slug)}/invoke`), {
168
217
  method: "POST",
169
218
  headers,
170
219
  body: JSON.stringify(envelope),
171
220
  signal: controller.signal
172
221
  });
173
222
  if (!res.ok) {
223
+ let text = "";
224
+ try {
225
+ text = await res.text();
226
+ } catch (e) {
227
+ if (controller.signal.aborted || isAbortError(e)) throw e;
228
+ }
174
229
  let parsed = null;
175
230
  try {
176
- parsed = await res.json();
231
+ parsed = text.trim() ? JSON.parse(text) : null;
177
232
  } catch {
178
233
  }
179
- return { data: null, error: toFunctionsError(new AuraError(res.status, parsed)) };
234
+ const isObject = parsed !== null && typeof parsed === "object";
235
+ const stringError = isObject && typeof parsed.error === "string" ? parsed.error : void 0;
236
+ const body = isObject ? stringError !== void 0 && typeof parsed.message !== "string" ? { ...parsed, message: stringError } : parsed : text.trim() ? { message: text } : null;
237
+ if (res.headers.get(INVOKE_ORIGIN_HEADER) === "function") {
238
+ return {
239
+ data: null,
240
+ error: new FunctionsHttpError(res.status, body, isObject ? parsed : text || null)
241
+ };
242
+ }
243
+ return { data: null, error: toFunctionsError(new AuraError(res.status, body, res.headers)) };
180
244
  }
181
245
  let data;
182
246
  if (responseType === "text") data = await res.text();
@@ -210,7 +274,7 @@ var FunctionsService = class extends BaseService {
210
274
  * le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
211
275
  */
212
276
  async get(nameOrId) {
213
- return this.request("GET", `${this.prefix}/${nameOrId}`);
277
+ return this.request("GET", `${this.prefix}/${seg(nameOrId)}`);
214
278
  }
215
279
  // -- Jobs -------------------------------------------------------------------
216
280
  async listJobs(options) {
@@ -221,7 +285,7 @@ var FunctionsService = class extends BaseService {
221
285
  return this.request("GET", `${this.prefix}/jobs${qs}`);
222
286
  }
223
287
  async getJob(jobId) {
224
- return this.request("GET", `${this.prefix}/jobs/${jobId}`);
288
+ return this.request("GET", `${this.prefix}/jobs/${seg(jobId)}`);
225
289
  }
226
290
  /**
227
291
  * Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
@@ -259,7 +323,7 @@ var FunctionsService = class extends BaseService {
259
323
  });
260
324
  }
261
325
  async deleteCronJob(name) {
262
- return this.request("DELETE", `${this.prefix}/cron/${name}`);
326
+ return this.request("DELETE", `${this.prefix}/cron/${seg(name)}`);
263
327
  }
264
328
  };
265
329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurabase/functions",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Service edge functions du SDK Aurabase (invoke, jobs asynchrones, cron)",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -45,7 +45,7 @@
45
45
  "functions"
46
46
  ],
47
47
  "dependencies": {
48
- "@aurabase/core": "0.5.4"
48
+ "@aurabase/core": "0.6.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "20.14.15",