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
19 changes: 19 additions & 0 deletions .changeset/18114-epochms-instants-tranche1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@objectstack/spec": minor
---

Ten wall-clock instants now declare their unit through the shared `EpochMs` schema (`@objectstack/spec/shared`) instead of a bare `z.number()`. No key is renamed and no key is added or removed.

`EpochMs` is `z.number().int()` with the describe "Unix timestamp in milliseconds (epoch)". Adopting it moves each key's published JSON Schema from `{"type":"number"}` to `{"type":"integer"}` and puts the millisecond unit on the contract itself, where a reader of the reference page, the JSON Schema or the TypeScript surface all see the same answer. Before this, the unit lived in a JSDoc block (invisible in every published artifact), in prose that named only the epoch and not the unit, or nowhere at all — the ×1000 ambiguity a `timestamp: number` key carries by default.

The keys, by schema:

- `Data.DocumentVersion.createdAt`, `Data.Document.access.expiresAt`
- `System.SupplierSecurityAssessment.assessedAt`, `.validUntil`, `.remediationItems[].deadline`
- `Identity.Account.expiresAt`
- `Kernel.PluginLoadingEvent.timestamp`, `Kernel.PluginLoadingState.startedAt`, `.completedAt`
- the shared connector OAuth2 auth shape's `tokenExpiry`

**What an author must change: nothing, unless they were writing a fractional millisecond.** Seven of the ten previously accepted any `number` and now accept integers only; `Date.now()` — the value every one of these keys is documented to carry — is already an integer. The three `Kernel.PluginLoading*` keys already declared `.int().min(0)`; they keep that floor (`EpochMs.min(0)`), so their accepted set is byte-for-byte what it was and only their description is new.

`timestamp`, `tokenExpiry`, `deadline` and `validUntil` deliberately keep their names. `EpochMs`'s own docblock recommends spelling an instant `*At`, but a rename of a published key is a retirement with its own ADR-0087 entry and is not part of this change.
8 changes: 4 additions & 4 deletions content/docs/references/data/document.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ const result = DocumentSchema.parse(data);
| **versioning** | `{ enabled: boolean; versions: object[]; majorVersion: number; minorVersion: number }` | optional | Version control |
| **template** | `{ id: string; name: string; description?: string; fileUrl: string; … }` | optional | Document template |
| **eSignature** | `{ provider: Enum<'docusign' \| 'adobe-sign' \| 'hellosign' \| 'custom'>; enabled: boolean; signers: object[] }` | optional | E-signature config |
| **access** | `{ isPublic: boolean; sharedWith?: string[]; expiresAt?: number }` | optional | Access control |
| **access** | `{ isPublic: boolean; sharedWith?: string[]; expiresAt?: integer }` | optional | Access control |
| **metadata** | `Record<string, any>` | optional | Custom metadata |

### Nested Shape: `Document.versioning`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | ✅ | Versioning enabled |
| **versions** | `{ versionNumber: number; createdAt: number; createdBy: string; size: number; … }[]` | ✅ | Version history |
| **versions** | `{ versionNumber: number; createdAt: integer; createdBy: string; size: number; … }[]` | ✅ | Version history |
| **majorVersion** | `number` | ✅ | Major version |
| **minorVersion** | `number` | ✅ | Minor version |

Expand Down Expand Up @@ -76,7 +76,7 @@ const result = DocumentSchema.parse(data);
| :--- | :--- | :--- | :--- |
| **isPublic** | `boolean` | optional (default: `false`) | Public access |
| **sharedWith** | `string[]` | optional | Shared with |
| **expiresAt** | `number` | optional | Access expiration |
| **expiresAt** | `integer` | optional | Access expiration |


---
Expand Down Expand Up @@ -113,7 +113,7 @@ const result = DocumentSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **versionNumber** | `number` | ✅ | Version number |
| **createdAt** | `number` | ✅ | Creation timestamp |
| **createdAt** | `integer` | ✅ | Creation timestamp |
| **createdBy** | `string` | ✅ | Creator user ID |
| **size** | `number` | ✅ | File size in bytes |
| **checksum** | `string` | ✅ | File checksum |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/identity/identity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const result = AccountSchema.parse(data);
| **providerAccountId** | `string` | ✅ | Provider account ID |
| **refreshToken** | `string` | optional | OAuth refresh token |
| **accessToken** | `string` | optional | OAuth access token |
| **expiresAt** | `number` | optional | Token expiry timestamp (Unix) |
| **expiresAt** | `integer` | optional | Token expiry timestamp (Unix milliseconds) |
| **tokenType** | `string` | optional | OAuth token type |
| **scope** | `string` | optional | OAuth scope |
| **idToken** | `string` | optional | OAuth ID token |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/integration/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ Circuit breaker configuration
| **scopes** | `string[]` | optional | Requested OAuth2 scopes |
| **redirectUri** | `string` | optional | OAuth2 redirect URI |
| **refreshToken** | `string` | optional | Refresh token for token renewal |
| **tokenExpiry** | `number` | optional | Token expiry timestamp |
| **tokenExpiry** | `integer` | optional | Token expiry timestamp (Unix milliseconds) |

### Nested Shape: `Connector.authentication[type='api-key']`

Expand Down Expand Up @@ -688,7 +688,7 @@ Connector type
| **scopes** | `string[]` | optional | Requested OAuth2 scopes |
| **redirectUri** | `string` | optional | OAuth2 redirect URI |
| **refreshToken** | `string` | optional | Refresh token for token renewal |
| **tokenExpiry** | `number` | optional | Token expiry timestamp |
| **tokenExpiry** | `integer` | optional | Token expiry timestamp (Unix milliseconds) |

### Nested Shape: `DeclarativeConnectorEntry.authentication[type='api-key']`

Expand Down
6 changes: 3 additions & 3 deletions content/docs/references/kernel/plugin-loading.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Plugin loading lifecycle event
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'load-started' \| 'load-completed' \| 'load-failed' \| 'init-started' \| 'init-completed' \| 'init-failed' \| 'preload-started' \| 'preload-completed' \| 'cache-hit' \| … +5 more>` | ✅ | |
| **pluginId** | `string` | ✅ | |
| **timestamp** | `integer` | ✅ | |
| **timestamp** | `integer` | ✅ | Event timestamp — Unix milliseconds |
| **durationMs** | `integer` | optional | Duration in milliseconds |
| **metadata** | `Record<string, any>` | optional | |
| **error** | `{ message: string; code?: string; stack?: string }` | optional | |
Expand Down Expand Up @@ -78,8 +78,8 @@ Plugin loading state
| **pluginId** | `string` | ✅ | |
| **state** | `Enum<'pending' \| 'loading' \| 'loaded' \| 'initializing' \| 'ready' \| 'failed' \| 'reloading' \| 'unloading' \| 'unloaded'>` | ✅ | |
| **progress** | `number` | optional (default: `0`) | |
| **startedAt** | `integer` | optional | |
| **completedAt** | `integer` | optional | |
| **startedAt** | `integer` | optional | Loading start time — Unix milliseconds |
| **completedAt** | `integer` | optional | Loading completion time — Unix milliseconds |
| **lastError** | `string` | optional | |
| **retryCount** | `integer` | optional (default: `0`) | |

Expand Down
8 changes: 4 additions & 4 deletions content/docs/references/system/supplier-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ Supplier security assessment record per ISO 27001:2022 A.5.19–A.5.21
| **riskLevel** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | Supplier risk classification |
| **status** | `Enum<'pending' \| 'in_progress' \| 'completed' \| 'expired' \| 'failed'>` | ✅ | Assessment status |
| **assessedBy** | `string` | ✅ | Assessor user ID or team |
| **assessedAt** | `number` | ✅ | Assessment timestamp |
| **validUntil** | `number` | ✅ | Assessment validity expiry timestamp |
| **assessedAt** | `integer` | ✅ | Assessment timestamp |
| **validUntil** | `integer` | ✅ | Assessment validity expiry timestamp |
| **requirements** | `{ id: string; description: string; controlReference?: string; mandatory: boolean; … }[]` | ✅ | Security requirements and their compliance status |
| **overallCompliant** | `boolean` | ✅ | Whether supplier meets all mandatory requirements |
| **dataClassificationsShared** | `Enum<'pii' \| 'phi' \| 'pci' \| 'financial' \| 'confidential' \| 'internal' \| 'public'>[]` | optional | Data classifications shared with supplier |
| **servicesProvided** | `string[]` | optional | Services provided by this supplier |
| **certifications** | `string[]` | optional | Supplier certifications (e.g., ISO 27001, SOC 2) |
| **remediationItems** | `{ requirementId: string; action: string; deadline: number; status: Enum<'pending' \| 'in_progress' \| 'completed'> }[]` | optional | Remediation items for non-compliant requirements |
| **remediationItems** | `{ requirementId: string; action: string; deadline: integer; status: Enum<'pending' \| 'in_progress' \| 'completed'> }[]` | optional | Remediation items for non-compliant requirements |
| **metadata** | `Record<string, any>` | optional | Custom metadata key-value pairs |

### Nested Shape: `SupplierSecurityAssessment.requirements[number]`
Expand All @@ -95,7 +95,7 @@ Individual supplier security requirement
| :--- | :--- | :--- | :--- |
| **requirementId** | `string` | ✅ | Non-compliant requirement ID |
| **action** | `string` | ✅ | Required remediation action |
| **deadline** | `number` | ✅ | Remediation deadline timestamp |
| **deadline** | `integer` | ✅ | Remediation deadline timestamp (Unix milliseconds) |
| **status** | `Enum<'pending' \| 'in_progress' \| 'completed'>` | optional (default: `"pending"`) | Remediation status |


Expand Down
5 changes: 3 additions & 2 deletions packages/spec/src/data/document.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { z } from 'zod';
import { retiredKey } from '../shared/retired-key';
import { EpochMs } from '../shared/epoch.zod';

/**
* Document Version Schema
Expand Down Expand Up @@ -32,7 +33,7 @@ export const DocumentVersionSchema = lazySchema(() => z.object({
/**
* Timestamp when this version was created (Unix milliseconds)
*/
createdAt: z.number().describe('Creation timestamp'),
createdAt: EpochMs.describe('Creation timestamp'),

/**
* User ID who created this version
Expand Down Expand Up @@ -399,7 +400,7 @@ export const DocumentSchema = lazySchema(() => z.object({
/**
* Timestamp when access expires (Unix milliseconds)
*/
expiresAt: z.number().optional().describe('Access expiration'),
expiresAt: EpochMs.optional().describe('Access expiration'),
}).optional().describe('Access control'),

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/spec/src/identity/identity.zod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { z } from 'zod';
import { EpochMs } from '../shared/epoch.zod';

/**
* Identity & User Model Specification
Expand Down Expand Up @@ -108,7 +109,7 @@ export const AccountSchema = lazySchema(() => z.object({
/**
* Token expiry timestamp
*/
expiresAt: z.number().optional().describe('Token expiry timestamp (Unix)'),
expiresAt: EpochMs.optional().describe('Token expiry timestamp (Unix milliseconds)'),

/**
* OAuth token type
Expand Down
7 changes: 4 additions & 3 deletions packages/spec/src/kernel/plugin-loading.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { z } from 'zod';
import { lazySchema } from '../shared/lazy-schema';
import { EpochMs } from '../shared/epoch.zod';

/**
* # Plugin Loading Protocol
Expand Down Expand Up @@ -96,7 +97,7 @@ export const PluginLoadingEventSchema = lazySchema(() => z.object({
/**
* Timestamp
*/
timestamp: z.number().int().min(0),
timestamp: EpochMs.min(0).describe('Event timestamp — Unix milliseconds'),

/**
* Duration in milliseconds
Expand Down Expand Up @@ -151,12 +152,12 @@ export const PluginLoadingStateSchema = lazySchema(() => z.object({
/**
* Loading start time
*/
startedAt: z.number().int().min(0).optional(),
startedAt: EpochMs.min(0).optional().describe('Loading start time — Unix milliseconds'),

/**
* Loading completion time
*/
completedAt: z.number().int().min(0).optional(),
completedAt: EpochMs.min(0).optional().describe('Loading completion time — Unix milliseconds'),

/**
* Last error
Expand Down
3 changes: 2 additions & 1 deletion packages/spec/src/shared/connector-auth.zod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { z } from 'zod';
import { EpochMs } from './epoch.zod';

/**
* SHARED CONNECTOR AUTHENTICATION SCHEMAS
Expand Down Expand Up @@ -35,7 +36,7 @@ export const ConnectorOAuth2Schema = lazySchema(() => z.object({
scopes: z.array(z.string()).optional().describe('Requested OAuth2 scopes'),
redirectUri: z.string().url().optional().describe('OAuth2 redirect URI'),
refreshToken: z.string().optional().describe('Refresh token for token renewal'),
tokenExpiry: z.number().optional().describe('Token expiry timestamp'),
tokenExpiry: EpochMs.optional().describe('Token expiry timestamp (Unix milliseconds)'),
}));

/**
Expand Down
7 changes: 4 additions & 3 deletions packages/spec/src/system/supplier-security.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { z } from 'zod';
import { DataClassificationSchema } from './security-context.zod';
import { EpochMs } from '../shared/epoch.zod';

/**
* Supplier Security Protocol — ISO 27001:2022 (A.5.19–A.5.22)
Expand Down Expand Up @@ -143,12 +144,12 @@ export const SupplierSecurityAssessmentSchema = lazySchema(() => z.object({
/**
* Assessment completion timestamp (Unix milliseconds)
*/
assessedAt: z.number().describe('Assessment timestamp'),
assessedAt: EpochMs.describe('Assessment timestamp'),

/**
* Assessment validity expiry (Unix milliseconds)
*/
validUntil: z.number().describe('Assessment validity expiry timestamp'),
validUntil: EpochMs.describe('Assessment validity expiry timestamp'),

/**
* Security requirements assessed
Expand Down Expand Up @@ -185,7 +186,7 @@ export const SupplierSecurityAssessmentSchema = lazySchema(() => z.object({
remediationItems: z.array(z.object({
requirementId: z.string().describe('Non-compliant requirement ID'),
action: z.string().describe('Required remediation action'),
deadline: z.number().describe('Remediation deadline timestamp'),
deadline: EpochMs.describe('Remediation deadline timestamp (Unix milliseconds)'),
status: z.enum(['pending', 'in_progress', 'completed']).default('pending')
.describe('Remediation status'),
})).optional().describe('Remediation items for non-compliant requirements'),
Expand Down
Loading