@ironfang/n8n-nodes-ironfang 0.4.2 → 0.5.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
@@ -6,7 +6,13 @@ Use Ironfang Render, Finance, Audit and Rig, and Ironfang's public developer
6
6
  tools, in n8n. The Ironfang action node has **162 operations**, grouped by
7
7
  product. Ironfang Trigger receives signed Audit, Finance and Render events.
8
8
 
9
- **Release: 0.4.2.** The API is now at `api.ironfang.com`; credentials saved with the old `api.ironfang.uk` address keep working unchanged. 0.4.1 made the package installable on n8n releases
9
+ **Release: 0.5.0.** Ironfang billing moved from credits to metered usage, and
10
+ the node follows: a render reports the billing meter and quantity it counted
11
+ instead of credits, Render Usage returns the month per meter, and a billing
12
+ refusal keeps its code, product, meter and renewal date. Workflows that read
13
+ `_ironfang.creditsCharged` need changing; see
14
+ [Permissions and usage](#permissions-and-usage). 0.4.3 reads cache status and
15
+ webhook signatures from the `X-Ironfang-*` and `Ironfang-*` headers. 0.4.2 moved the API to `api.ironfang.com` (credentials saved with the old `api.ironfang.uk` address keep working unchanged), and 0.4.1 made the package installable on n8n releases
10
16
  before 2.33 that use PostgreSQL; see [Installation](#installation-and-credentials).
11
17
  It includes 0.4.0, in which the products are named Ironfang Render, Finance and Audit
12
18
  (formerly Renderwolf, Financewolf and Auditwolf), and the node follows: requests
@@ -137,13 +143,17 @@ also applies to these outputs.
137
143
  Keys created with the earlier `renderwolf:*`, `financewolf:*` and `auditwolf:*`
138
144
  scopes keep working: the platform treats them as the scopes above.
139
145
 
140
- Render includes **250 free credits each month**, with no card required.
141
- A screenshot or template image costs 1 credit, a PDF 2, and QR codes 0. Clip
142
- costs depend on duration and output size. Cache hits consume no credits.
143
- Free renders can carry an Ironfang Render badge; QR codes never carry one. This
144
- allowance belongs to Render; other products have their own usage policies.
145
-
146
- Public tools use no account credits and share a 60/minute/address limit. The
146
+ Usage is billed to your organisation's Ironfang billing account, which every
147
+ Ironfang product shares. There are no plans or credits: each kind of work counts
148
+ against a meter with a monthly free allowance, and usage beyond it is billed
149
+ monthly once paid usage is switched on in Billing in the
150
+ [Ironfang portal](https://portal.ironfang.com). A Render screenshot, PDF or
151
+ template image counts 1 on its own meter, clips and site previews count output
152
+ seconds, and QR codes are counted but never charged. Cache hits count nothing.
153
+ Render Usage returns the month for each meter; limits, payment and statements
154
+ are managed in the portal.
155
+
156
+ Public tools are not billed to an account and share a 60/minute/address limit. The
147
157
  public screenshot service also has a 20/hour/address limit and a shared daily
148
158
  ceiling. Shared n8n egress addresses share these limits. Use authenticated
149
159
  Render rendering for account-based automation.
@@ -164,8 +174,9 @@ The trigger verifies HMAC-SHA256 over the timestamp and exact body bytes using
164
174
  constant-time comparison, accepts timestamps within five minutes, and rejects
165
175
  invalid signatures before starting a workflow. Render and Finance secrets are
166
176
  decoded from hex; Audit secrets are used as returned. Deliveries are signed in
167
- the `Renderwolf-`, `Financewolf-` and `Auditwolf-` headers the products launched
168
- with; the trigger reads the ones for the credential's product. The signed event ID, rather
177
+ the `Ironfang-Signature` and `Ironfang-Timestamp` headers; the trigger still
178
+ reads the `Renderwolf-`, `Financewolf-` or `Auditwolf-` headers the products
179
+ launched with when those are all a delivery carries. The signed event ID, rather
169
180
  than the unsigned header ID, is used for duplicate detection.
170
181
 
171
182
  Duplicate state covers seven days/up to 10,000 IDs in n8n workflow static data.
@@ -201,8 +212,10 @@ fields. Version 1.1 uses numeric `bytes` and structured errors; version 1.2 adds
201
212
  paging to the Audit lists named above and is otherwise identical. Version 2,
202
213
  which new nodes use, behaves as 1.2 does. All expose exact
203
214
  `byteLength`, item links and `_ironfang` response metadata when available:
204
- request ID, HTTP status, cache status and charged credits. Continued errors
205
- include available problem codes and retry information. New file operations use
215
+ request ID, HTTP status, cache status, and the billing `meter` and `quantity`
216
+ the request counted (0 for a cache hit). Continued errors include available
217
+ problem codes and retry information; a billing refusal also carries `product`,
218
+ `meter` and `resetAt` (when the free allowance renews) when the API states them. New file operations use
206
219
  numeric sizes; the output binary field defaults to `data`.
207
220
 
208
221
  Requests have explicit timeouts: 30 seconds for JSON (100 for a Rig wait) and
@@ -80,7 +80,7 @@ exports.renderwolf = {
80
80
  {
81
81
  name: 'Usage',
82
82
  value: 'usage',
83
- description: 'Current period usage against your plan cap',
83
+ description: "This month's usage of each Render meter, from your billing account",
84
84
  action: 'Get usage',
85
85
  }, {
86
86
  name: 'Video Clip',
@@ -167,7 +167,7 @@ exports.renderwolf = {
167
167
  name: 'no_cache',
168
168
  type: 'boolean',
169
169
  default: false,
170
- description: 'Whether to force a live capture instead of reusing an identical recent render. Use when the page must be captured exactly as it is right now, such as evidence or change detection. Counts against your quota.',
170
+ description: 'Whether to force a live capture instead of reusing an identical recent render. Use when the page must be captured exactly as it is right now, such as evidence or change detection. A fresh render is always counted.',
171
171
  },
172
172
  {
173
173
  displayName: 'Quality',
@@ -204,7 +204,7 @@ exports.renderwolf = {
204
204
  name: 'no_cache',
205
205
  type: 'boolean',
206
206
  default: false,
207
- description: 'Whether to force a live capture instead of reusing an identical recent render. Use when the page must be captured exactly as it is right now, such as evidence or change detection. Counts against your quota.',
207
+ description: 'Whether to force a live capture instead of reusing an identical recent render. Use when the page must be captured exactly as it is right now, such as evidence or change detection. A fresh render is always counted.',
208
208
  },
209
209
  {
210
210
  displayName: 'Print Background',
@@ -330,7 +330,7 @@ exports.renderwolf = {
330
330
  type: 'options',
331
331
  default: 'vertical',
332
332
  displayOptions: { show: { operation: ['video'] } },
333
- description: 'Credits scale with pixels and seconds, so the smaller sizes cost less per second',
333
+ description: 'Counted in output seconds: vertical and landscape as 1080p, square and 720p as HD',
334
334
  options: [
335
335
  { name: '720p 1280x720', value: '720p' },
336
336
  { name: 'Landscape 1920x1080', value: 'landscape' },
@@ -1338,7 +1338,7 @@ exports.operations = [
1338
1338
  "product": "auditwolf",
1339
1339
  "id": "getUsage",
1340
1340
  "name": "Usage",
1341
- "description": "Credits, quota position, projection and hosted evidence",
1341
+ "description": "This month's metered use, capacity and hosted evidence",
1342
1342
  "method": "GET",
1343
1343
  "path": "/v1/usage",
1344
1344
  "scope": "audit:read",
@@ -2011,27 +2011,18 @@ exports.operations = [
2011
2011
  "product": "financewolf",
2012
2012
  "id": "getEinvoiceUsage",
2013
2013
  "name": "Usage",
2014
- "description": "Reconcile Ironfang Finance document usage",
2014
+ "description": "Read Ironfang Finance usage for a month",
2015
2015
  "method": "GET",
2016
2016
  "path": "/v1/einvoices/usage",
2017
2017
  "scope": "finance:billing:manage",
2018
2018
  "query": [
2019
2019
  {
2020
- "name": "start",
2021
- "label": "Start",
2020
+ "name": "period",
2021
+ "label": "Period",
2022
2022
  "type": "string",
2023
- "required": true,
2024
- "default": "",
2025
- "description": "RFC3339 inclusive start / exclusive end; at most 366 days.",
2026
- "enum": []
2027
- },
2028
- {
2029
- "name": "end",
2030
- "label": "End",
2031
- "type": "string",
2032
- "required": true,
2023
+ "required": false,
2033
2024
  "default": "",
2034
- "description": "RFC3339 inclusive start / exclusive end; at most 366 days.",
2025
+ "description": "A past or current UTC calendar month as YYYY-MM; the current month when omitted.",
2035
2026
  "enum": []
2036
2027
  }
2037
2028
  ],
@@ -3327,7 +3318,7 @@ exports.operations = [
3327
3318
  "product": "rig",
3328
3319
  "id": "getUsage",
3329
3320
  "name": "Usage",
3330
- "description": "Runs and events in a window",
3321
+ "description": "Runs, events and metered interactions",
3331
3322
  "method": "GET",
3332
3323
  "path": "/v1/usage",
3333
3324
  "scope": "rig:read",
@@ -25,11 +25,15 @@ async function credentialTest(credential) {
25
25
  });
26
26
  lastStatus = response.statusCode;
27
27
  const body = response.body;
28
- const valid = product === 'renderwolf' ? typeof body?.limit === 'number' || body?.unmetered === true : Array.isArray(body?.[checks[product][1]]);
28
+ // Render usage is a report per billing meter, or {"unmetered": true}.
29
+ const valid = product === 'renderwolf' ? Array.isArray(body?.meters) || body?.unmetered === true : Array.isArray(body?.[checks[product][1]]);
29
30
  if (response.statusCode === 200 && valid)
30
31
  return { status: 'OK', message: `Connected to Ironfang ${name}. Each operation requires its own scope.` };
31
- if (response.statusCode === 403 && (body?.error?.code ?? body?.code) === 'insufficient_scope')
32
+ const code = body?.error?.code ?? body?.code;
33
+ if (response.statusCode === 403 && code === 'insufficient_scope')
32
34
  return { status: 'OK', message: `API key authenticated by Ironfang ${name}. The test read is outside its scopes; choose only the scopes your workflow needs.` };
35
+ if (response.statusCode === 403 && code === 'account_not_linked')
36
+ return { status: 'Error', message: 'API key authenticated, but its account belongs to no Ironfang organisation, so its usage cannot be billed. Contact support.' };
33
37
  if (response.statusCode !== 401 && response.statusCode !== 404)
34
38
  break;
35
39
  }
@@ -32,15 +32,20 @@ function identifier(context, value, itemIndex) {
32
32
  }
33
33
  return encodeURIComponent(id);
34
34
  }
35
+ // A billable response names the billing meter it counted against and the
36
+ // quantity it counted there: 1 for most renders, output seconds for video,
37
+ // 0 for a cache hit or an unmetered key.
35
38
  function responseMetadata(headers, statusCode) {
36
39
  const result = { statusCode };
37
40
  if (headers['x-ironfang-request-id'])
38
41
  result.requestId = headers['x-ironfang-request-id'];
39
- if (headers['x-renderwolf-cache'])
40
- result.cacheStatus = headers['x-renderwolf-cache'];
41
- const credits = headers['x-renderwolf-credits'];
42
- if (credits !== undefined && credits !== '' && Number.isFinite(Number(credits)))
43
- result.creditsCharged = Number(credits);
42
+ if (headers['x-ironfang-cache'])
43
+ result.cacheStatus = headers['x-ironfang-cache'];
44
+ if (headers['x-ironfang-meter'])
45
+ result.meter = headers['x-ironfang-meter'];
46
+ const quantity = headers['x-ironfang-quantity'];
47
+ if (quantity !== undefined && /^\d+$/.test(quantity))
48
+ result.quantity = Number(quantity);
44
49
  return result;
45
50
  }
46
51
  async function request(options, authenticated = true) {
@@ -100,6 +105,16 @@ function errorDetails(error, itemIndex) {
100
105
  const retry = headers['retry-after'];
101
106
  if (typeof retry === 'string' && /^\d+$/.test(retry))
102
107
  details.retryAfterSeconds = Number(retry);
108
+ // A billing refusal names the product and meter it refused and, for an
109
+ // exhausted free allowance, when the allowance renews. Render and Audit put
110
+ // them in the error object; Finance problems carry them in `billing`.
111
+ const billing = Object.keys(object(problem.billing)).length ? object(problem.billing) : problem;
112
+ if (typeof billing.product === 'string' && /^[a-z]{1,32}$/.test(billing.product))
113
+ details.product = billing.product;
114
+ if (typeof billing.meter === 'string' && /^[a-z0-9_.]{1,64}$/.test(billing.meter))
115
+ details.meter = billing.meter;
116
+ if (typeof billing.reset_at === 'string' && !Number.isNaN(Date.parse(billing.reset_at)))
117
+ details.resetAt = billing.reset_at;
103
118
  return details;
104
119
  }
105
120
  function apiError(context, error, itemIndex) {
@@ -4,8 +4,10 @@ exports.signingKey = signingKey;
4
4
  exports.verifyEvent = verifyEvent;
5
5
  exports.webhookCredentialTest = webhookCredentialTest;
6
6
  const crypto_1 = require("crypto");
7
- // Signature headers keep the launch names (Renderwolf-Signature and so on), which
8
- // are also the saved product values. Render and Finance issue hex secrets.
7
+ // Every product signs with Ironfang-Signature and Ironfang-Timestamp. The saved
8
+ // product values keep the launch names (renderwolf and so on), and deliveries
9
+ // from before the Ironfang-* headers carried only <Launch name>-Signature, which
10
+ // is still read when the new header is absent. Render and Finance issue hex secrets.
9
11
  function signingKey(product, secret) {
10
12
  if (['renderwolf', 'financewolf'].includes(product) && /^[a-f0-9]{64}$/i.test(secret))
11
13
  return Buffer.from(secret, 'hex');
@@ -16,8 +18,8 @@ function signingKey(product, secret) {
16
18
  function verifyEvent(raw, headers, product, secret, now) {
17
19
  if (!raw.length || raw.length > 65_536)
18
20
  throw new Error('Webhook payload size is invalid');
19
- const timestamp = headers[`${product}-timestamp`];
20
- const signature = headers[`${product}-signature`];
21
+ const timestamp = headers['ironfang-timestamp'] ?? headers[`${product}-timestamp`];
22
+ const signature = headers['ironfang-signature'] ?? headers[`${product}-signature`];
21
23
  if (typeof timestamp !== 'string' || !/^\d{10,11}$/.test(timestamp) || Math.abs(now / 1000 - Number(timestamp)) > 300)
22
24
  throw new Error('Webhook timestamp is invalid or expired');
23
25
  if (typeof signature !== 'string' || !/^v1=[a-f0-9]{64}$/.test(signature))
@@ -1,6 +1,6 @@
1
1
  # Operation reference
2
2
 
3
- Generated from Ironfang main `de1547b`. Runtime endpoint selection is fixed by this catalogue.
3
+ Generated from Ironfang main `8ec778a0`. Runtime endpoint selection is fixed by this catalogue.
4
4
  Complex request bodies use JSON so all supported schema fields remain available. Replace placeholders with your own values.
5
5
 
6
6
  ## audit / Archive Monitor
@@ -608,7 +608,7 @@ Full request contract: https://api.ironfang.com/audit/openapi.yaml
608
608
 
609
609
  `GET /audit/v1/usage`. Scope: `audit:read`.
610
610
 
611
- Credits, quota position, projection and hosted evidence
611
+ This month's metered use, capacity and hosted evidence
612
612
 
613
613
  Full request contract: https://api.ironfang.com/audit/openapi.yaml
614
614
 
@@ -941,11 +941,11 @@ Full request contract: https://api.ironfang.com/finance/openapi.yaml
941
941
 
942
942
  `GET /finance/v1/einvoices/usage`. Scope: `finance:billing:manage`.
943
943
 
944
- Reconcile Ironfang Finance document usage
944
+ Read Ironfang Finance usage for a month
945
945
 
946
946
  Full request contract: https://api.ironfang.com/finance/openapi.yaml
947
947
 
948
- Query fields: `start` (required), `end` (required).
948
+ Query fields: `period`.
949
949
 
950
950
  ## finance / Validate E-Invoice
951
951
 
@@ -1636,7 +1636,7 @@ Full request contract: https://api.ironfang.com/rig/openapi.yaml
1636
1636
 
1637
1637
  `GET /rig/v1/usage`. Scope: `rig:read`.
1638
1638
 
1639
- Runs and events in a window
1639
+ Runs, events and metered interactions
1640
1640
 
1641
1641
  Full request contract: https://api.ironfang.com/rig/openapi.yaml
1642
1642
 
@@ -45,6 +45,15 @@ SQLite, which records nothing. `scripts/check-postgres-install.sh` now runs in
45
45
  CI before release. 224 tests and all three Docker checks passed locally on
46
46
  21 September.
47
47
 
48
+ The 0.5.0 release follows Ironfang main `8ec778a0` (29 September 2026), where
49
+ Render bills by meter rather than credits. `_ironfang` reports `meter` and
50
+ `quantity` from the `X-Ironfang-Meter` and `X-Ironfang-Quantity` headers,
51
+ billing refusals carry the product, meter and allowance renewal alongside the
52
+ code, and the credential test accepts the per-meter usage report. These were
53
+ checked against the API source and the unit tests, not against a live paid
54
+ render. `npm run check` (229 tests), all three Docker checks and `npm audit`
55
+ (zero reported vulnerabilities) passed locally on 29 September.
56
+
48
57
  ## Automated coverage
49
58
 
50
59
  - 0.4.0 adds cases for routing saved `/renderwolf` and origin bases to the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ironfang/n8n-nodes-ironfang",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "n8n nodes for Ironfang: Render, Finance, Audit, Rig, public tools and signed events",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",