ts / src / ipfs.ts
1,092 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/**
 * IPFS pinning, direct uploads, IPNS names and customer gateway domains
 * (which can serve your pins directly at /ipfs/<cid>). Prepaid hourly
 * billing per pinned MiB plus gateway egress; deletes are always free. The
 * standard IPFS Pinning Service API lives in `pins`.
 *
 * For uploading local content use `addFile`, which drives the whole
 * grant → upload → pin flow.
 */
import type { Client } from './client.js';
import { uploadFailed } from './errors.js';
import { checkGate } from './gate.js';
import * as http from './http.js';
import type { Body, Query, Res } from './types.js';

/** GET /ipfs/pins — List your pins (offset-paginated). */
export const listPins = (client: Client, query?: Query<'/ipfs/pins', 'get'>) =>
  http.get(client, '/ipfs/pins', { query });

/** Every pin, following offset pagination (page `limit` defaults to 100). */
export const listAllPins = (client: Client, query?: Query<'/ipfs/pins', 'get'>) =>
  http.paginateOffset(client, '/ipfs/pins', 'pins', { query });

/** POST /ipfs/pins — Pin by CID (202; billing starts once pinned). */
export const pin = (client: Client, body: Body<'/ipfs/pins', 'post'>) => http.post(client, '/ipfs/pins', { body });

/** GET /ipfs/pins/{id} — Pin detail. */
export const getPin = (client: Client, id: string) => http.get(client, '/ipfs/pins/{id}', { params: { id } });

/** DELETE /ipfs/pins/{id} — Unpin (202; free). */
export const unpin = (client: Client, id: string) => http.del(client, '/ipfs/pins/{id}', { params: { id } });

/**
 * POST /ipfs/uploads — Start a direct upload. Issues a short-lived
 * single-use grant `{ upload: { url, token, ... } }`; POST the raw bytes to
 * that URL (see `http.uploadBytes`) then poll `getUpload`.
 */
export const startUpload = (client: Client, body?: Body<'/ipfs/uploads', 'post'>) =>
  http.post(client, '/ipfs/uploads', { body });

/** GET /ipfs/uploads/{id} — Upload status (issued/completed/failed/expired). */
export const getUpload = (client: Client, id: string) =>
  http.get(client, '/ipfs/uploads/{id}', { params: { id } });

/** GET /ipfs/names — List your IPNS names. */
export const listNames = (client: Client) => http.get(client, '/ipfs/names');

/** POST /ipfs/names — Create an IPNS name. */
export const createName = (client: Client, body?: Body<'/ipfs/names', 'post'>) =>
  http.post(client, '/ipfs/names', { body });

/** GET /ipfs/names/{id} — IPNS name detail. */
export const getName = (client: Client, id: string) => http.get(client, '/ipfs/names/{id}', { params: { id } });

/** DELETE /ipfs/names/{id} — Delete an IPNS name (202). */
export const deleteName = (client: Client, id: string) => http.del(client, '/ipfs/names/{id}', { params: { id } });

/** PUT /ipfs/names/{id}/target — Publish a new target for an IPNS name. */
export const publishTarget = (client: Client, id: string, body: Body<'/ipfs/names/{id}/target', 'put'>) =>
  http.put(client, '/ipfs/names/{id}/target', { params: { id }, body });

/** GET /ipfs/domains — List your gateway domains. */
export const listDomains = (client: Client) => http.get(client, '/ipfs/domains');

/**
 * POST /ipfs/domains — Attach your own domain as an IPFS gateway. Gated on
 * `domain`. The response's `dns_target` is the gateway IPv4 the domain's A
 * record must point at on manually mapped rows.
 */
export const attachDomain = async (client: Client, body: Body<'/ipfs/domains', 'post'>) => {
  await checkGate(client, 'POST /ipfs/domains', { domain: body.domain });
  return http.post(client, '/ipfs/domains', { body });
};

/** GET /ipfs/domains/{id} — Gateway domain detail. */
export const getDomain = (client: Client, id: string) => http.get(client, '/ipfs/domains/{id}', { params: { id } });

/** GET /ipfs/domains/{id}/pins — The pins a gateway domain serves, oldest attachment first. */
export const listDomainPins = (client: Client, id: string, query?: Query<'/ipfs/domains/{id}/pins', 'get'>) =>
  http.get(client, '/ipfs/domains/{id}/pins', { params: { id }, query });

/** Every pin a gateway domain serves, following offset pagination. */
export const listAllDomainPins = (client: Client, id: string, query?: Query<'/ipfs/domains/{id}/pins', 'get'>) =>
  http.paginateOffset(client, '/ipfs/domains/{id}/pins', 'pins', { params: { id }, query });

/** Resolve the gateway's domain name (only when gated) and run the gate. */
const checkGatewayGate = async (client: Client, op: string, id: string): Promise<void> => {
  if (!client.gate) return;
  const { domain } = await getDomain(client, id);
  await checkGate(client, op, { domain: domain?.domain });
};

/**
 * POST /ipfs/domains/{id}/pins — Attach or detach the pins a gateway
 * domain serves (`add` / `remove` pin ids; idempotent delta). Gated on the
 * gateway's domain (resolved via `getDomain` when a gate is set).
 */
export const updateDomainPins = async (
  client: Client,
  id: string,
  body: Body<'/ipfs/domains/{id}/pins', 'post'>,
) => {
  await checkGatewayGate(client, 'POST /ipfs/domains/{id}/pins', id);
  return http.post(client, '/ipfs/domains/{id}/pins', { params: { id }, body });
};

/** DELETE /ipfs/domains/{id} — Detach a gateway domain (202). Gated on the gateway's domain. */
export const detachDomain = async (client: Client, id: string) => {
  await checkGatewayGate(client, 'DELETE /ipfs/domains/{id}', id);
  return http.del(client, '/ipfs/domains/{id}', { params: { id } });
};

/** GET /ipfs/usage — Storage usage and cost. */
export const usage = (client: Client) => http.get(client, '/ipfs/usage');

export type Upload = Res<'/ipfs/uploads/{id}', 'get'>['upload'];

export type AddFileOptions = Readonly<{
  /** Display name for the pin. */
  name?: string;
  /** 'file' (default) or 'car' for a client-packed CAR archive. */
  format?: 'file' | 'car';
  /** string → string metadata. */
  meta?: Readonly<Record<string, string>>;
  /** Initial poll interval (default 1000, backing off to an 8 s cap). */
  intervalMs?: number;
  /** Overall deadline (default 300000). */
  timeoutMs?: number;
}>;

const UPLOAD_TERMINAL = new Set(['completed', 'failed', 'expired']);

const toUploadSource = async (source: string | http.UploadSource): Promise<http.UploadSource> => {
  if (typeof source !== 'string') return source;
  const { readFile } = await import('node:fs/promises');
  return new Uint8Array(await readFile(source));
};

/**
 * Upload local content to IPFS end to end: start an upload grant, POST the
 * bytes to the grant URL, poll until the upload completes, and return the
 * final upload (with `cid` and `pin_id`). SOURCE is a file path, bytes, a
 * Blob, or a ReadableStream. Throws an uploadFailed error when the upload
 * fails or expires, or a timeout error when the deadline passes first.
 */
export const addFile = async (
  client: Client,
  source: string | http.UploadSource,
  options: AddFileOptions = {},
): Promise<Upload> => {
  const { name, format, meta } = options;
  const startBody = {
    ...(name === undefined ? {} : { name }),
    ...(format === undefined ? {} : { format }),
    ...(meta === undefined ? {} : { meta: { ...meta } }),
  };
  const { upload } = await startUpload(client, Object.keys(startBody).length === 0 ? undefined : startBody);
  await http.uploadBytes(upload.url, upload.token, await toUploadSource(source), { format, fetch: client.fetch });
  const { upload: final } = await http.pollUntil(
    () => getUpload(client, upload.id),
    (r) => UPLOAD_TERMINAL.has(r.upload.status),
    {
      intervalMs: options.intervalMs,
      timeoutMs: options.timeoutMs ?? 300000,
      message: `Timed out waiting for IPFS upload ${upload.id}`,
      sleep: client.sleep,
    },
  );
  if (final.status === 'completed') return final;
  throw uploadFailed(`IPFS upload ${final.id} ${final.status}`, final);
};

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/ts.git