Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ This project keeps root-level configuration files to the minimum required by fra
- `POST /api/exif-tracking/save`
- `POST /exif-viewer/parse`
- `POST /*/exif-viewer/parse`
- `POST /lists/under-construction`
- `POST /*/lists/under-construction`
- `POST /gear/*`
- `POST /*/gear/*`
- Server-side BotID classification is isolated in `src/server/security/botid.ts`, which wraps `checkBotId()` and keeps request-scoped detection logic out of UI code.
Expand Down
78 changes: 78 additions & 0 deletions docs/developer-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Developer API

Sharply’s developer API is a gated beta for published photography gear data. It is free during the beta and currently exposes read-only endpoints under `/api/v1`.

## Access and keys

- An administrator enables developer access for a user from `/admin/developer-api`.
- Approved users manage up to three active keys at `/developer`.
- Approved users can review the in-product endpoint guide at `/developer/docs`.
- Keys use the `sharply_live_` prefix and are displayed only when created. The database stores a SHA-256 hash, not the secret.
- Removing developer access immediately revokes all of the user’s keys.
- Send keys with `Authorization: Bearer sharply_live_...`. Use the API from your server or another trusted backend. Browser calls are not supported because the API does not send CORS headers; never place a key in browser code or a public client bundle.

## Rate limit and usage

Each key is limited to 60 total requests per fixed UTC minute. Successful and application-level failed requests made by an authenticated, non-rate-limited key count toward the daily usage totals shown in the portal. Invalid-key and rate-limited requests do not count.

Responses include `X-Request-Id`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Rate-limit errors return `429` with the standard error response.

## Endpoints

### `GET /api/v1/search`

Required query parameter: `q` (2–200 characters). Optional `page` defaults to 1 and `limit` defaults to 20 (maximum 25).

Returns a ranked page of published results:

```json
{
"data": [],
"pagination": { "page": 1, "limit": 20, "total": 0, "totalPages": 0 },
"meta": { "requestId": "..." }
}
```

### `GET /api/v1/gear/:slug`

Returns the complete currently publishable catalog record, including available related specifications, aliases, media, samples, and colourways. Database primary keys and audit timestamps are omitted at every nesting level. Hidden and rumored gear return `404`.

### `GET /api/v1/specs`

Returns the live catalog of selectable specs. Categories such as `camera.sensor` are stable API groupings and intentionally do not mirror the website’s specs-table sections. The catalog reflects the currently deployed registry and sends `Cache-Control: no-store`.

### `GET /api/v1/gear/:slug/specs`

Requires a `fields` query parameter containing one to 50 comma-separated exact field IDs, categories, or a mix of both. For example: `fields=camera.sensor,lens-optics.focalLength`.

Category selectors expand in API registry order; overlapping selectors are deduplicated. Unknown selectors return `400 invalid_request`. Applicable specs without a value are omitted.

Each row returns durable raw data and a helpful current display string:

```json
{
"id": "camera.sensor.isoRange",
"raw": { "min": 100, "max": 51200 },
"display": "ISO 100–51,200"
}
```

Treat `id` and `raw` as the durable contract. `display` is English current presentation output and can change.

## Error format

```json
{
"error": {
"code": "invalid_api_key",
"message": "The supplied API key is invalid."
},
"meta": { "requestId": "..." }
}
```

Possible v1 codes include `missing_api_key`, `invalid_api_key`, `invalid_request`, `not_found`, and `rate_limit_exceeded`.

## Implementation boundary

All public API-specific logic belongs in `src/server/developer-api/**`. Public routes are thin HTTP adapters and reuse the existing `server/search/service.ts` and `server/gear/service.ts` domain services. They do not call the website’s `/api/*` routes or database layer directly.
4 changes: 4 additions & 0 deletions docs/search-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Helper utilities for URLs live in `src/lib/utils/url.ts` (`buildSearchHref`, `me
- `smart-action`: currently compare actions with `action: "compare"`, compare slugs/titles, and compare href
- Compatibility fields `label` and `type` are still emitted for older consumers.

### Developer API

The key-authenticated developer API has separate public endpoints at `/api/v1/search` and `/api/v1/search/suggestions`. Both reuse `searchGear` and `getSuggestions` from the search service, preserving one ranking implementation. The developer suggestion serializer omits website-specific `href` and smart compare-action data; see `docs/developer-api.md` for its public contract.

## Database prerequisites

- PostgreSQL `pg_trgm` extension is required for similarity-based ranking and fast `ILIKE` searches.
Expand Down
7 changes: 7 additions & 0 deletions docs/server-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ This document explains how to organize server-only code under `src/server/**`.
- `server/metrics/data.ts`: `getGearCount`, `getContributionCount`
- `server/metrics/service.ts`: `fetchGearCount`, `fetchContributionCount`

- Developer API
- `server/developer-api/data.ts`: API-key, rate-bucket, and usage aggregate persistence.
- `server/developer-api/service.ts`: access checks, key lifecycle, rate limiting, and composition of existing search/gear domain reads.
- `server/developer-api/actions.ts`: authenticated portal/admin mutations only.
- `server/developer-api/http.ts`: shared public-route authentication and response handling.
- Developer route handlers must not call the website’s API routes or access Drizzle directly.

## Import Rules

- UI/Client Components → call Server Actions (for mutations) or import from `server/**/service.ts` (for reads).
Expand Down
39 changes: 39 additions & 0 deletions drizzle/0024_tiresome_lilandra.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
CREATE TABLE "app"."developer_api_keys" (
"id" varchar(36) PRIMARY KEY DEFAULT gen_random_uuid()::text NOT NULL,
"user_id" varchar(255) NOT NULL,
"name" varchar(100) NOT NULL,
"key_prefix" varchar(48) NOT NULL,
"key_hash" varchar(64) NOT NULL,
"last_used_at" timestamp with time zone,
"revoked_at" timestamp with time zone,
"revoked_by_user_id" varchar(255),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "developer_api_keys_key_hash_unique" UNIQUE("key_hash")
);
--> statement-breakpoint
CREATE TABLE "app"."developer_api_rate_limit_buckets" (
"api_key_id" varchar(36) NOT NULL,
"window_start" timestamp with time zone NOT NULL,
"request_count" integer DEFAULT 0 NOT NULL,
CONSTRAINT "developer_api_rate_limit_buckets_api_key_id_window_start_pk" PRIMARY KEY("api_key_id","window_start")
);
--> statement-breakpoint
CREATE TABLE "app"."developer_api_usage_daily" (
"api_key_id" varchar(36) NOT NULL,
"usage_date" date NOT NULL,
"total_requests" integer DEFAULT 0 NOT NULL,
"search_requests" integer DEFAULT 0 NOT NULL,
"suggestion_requests" integer DEFAULT 0 NOT NULL,
"gear_requests" integer DEFAULT 0 NOT NULL,
CONSTRAINT "developer_api_usage_daily_api_key_id_usage_date_pk" PRIMARY KEY("api_key_id","usage_date")
);
--> statement-breakpoint
ALTER TABLE "app"."user" ADD COLUMN "developer_access_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "app"."developer_api_keys" ADD CONSTRAINT "developer_api_keys_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "app"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "app"."developer_api_keys" ADD CONSTRAINT "developer_api_keys_revoked_by_user_id_user_id_fk" FOREIGN KEY ("revoked_by_user_id") REFERENCES "app"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "app"."developer_api_rate_limit_buckets" ADD CONSTRAINT "developer_api_rate_limit_buckets_api_key_id_developer_api_keys_id_fk" FOREIGN KEY ("api_key_id") REFERENCES "app"."developer_api_keys"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "app"."developer_api_usage_daily" ADD CONSTRAINT "developer_api_usage_daily_api_key_id_developer_api_keys_id_fk" FOREIGN KEY ("api_key_id") REFERENCES "app"."developer_api_keys"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "developer_api_keys_user_active_idx" ON "app"."developer_api_keys" USING btree ("user_id","revoked_at");--> statement-breakpoint
CREATE INDEX "developer_api_keys_last_used_idx" ON "app"."developer_api_keys" USING btree ("last_used_at");--> statement-breakpoint
CREATE INDEX "developer_api_rate_limit_buckets_window_idx" ON "app"."developer_api_rate_limit_buckets" USING btree ("window_start");--> statement-breakpoint
CREATE INDEX "developer_api_usage_daily_date_idx" ON "app"."developer_api_usage_daily" USING btree ("usage_date");
Loading
Loading